]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
ad6c461417c29a7fe00e528b2a8fe245a92fc78d
[micro.git] / cmd / micro / micro.go
1 package main
2
3 import (
4         "flag"
5         "fmt"
6         "io/ioutil"
7         "os"
8         "path/filepath"
9         "runtime"
10         "time"
11
12         "github.com/go-errors/errors"
13         "github.com/mattn/go-isatty"
14         "github.com/mitchellh/go-homedir"
15         "github.com/yuin/gopher-lua"
16         "github.com/zyedidia/clipboard"
17         "github.com/zyedidia/tcell"
18         "github.com/zyedidia/tcell/encoding"
19         "layeh.com/gopher-luar"
20 )
21
22 const (
23         doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
24         undoThreshold        = 500 // If two events are less than n milliseconds apart, undo both of them
25         autosaveTime         = 8   // Number of seconds to wait before autosaving
26 )
27
28 var (
29         // The main screen
30         screen tcell.Screen
31
32         // Object to send messages and prompts to the user
33         messenger *Messenger
34
35         // The default highlighting style
36         // This simply defines the default foreground and background colors
37         defStyle tcell.Style
38
39         // Where the user's configuration is
40         // This should be $XDG_CONFIG_HOME/micro
41         // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
42         configDir string
43
44         // Version is the version number or commit hash
45         // These variables should be set by the linker when compiling
46         Version = "0.0.0-unknown"
47         // CommitHash is the commit this version was built on
48         CommitHash = "Unknown"
49         // CompileDate is the date this binary was compiled on
50         CompileDate = "Unknown"
51
52         // The list of views
53         tabs []*Tab
54         // This is the currently open tab
55         // It's just an index to the tab in the tabs array
56         curTab int
57
58         // Channel of jobs running in the background
59         jobs chan JobFunction
60
61         // Event channel
62         events   chan tcell.Event
63         autosave chan bool
64
65         // Channels for the terminal emulator
66         updateterm chan bool
67         closeterm  chan int
68
69         // How many redraws have happened
70         numRedraw uint
71 )
72
73 // LoadInput determines which files should be loaded into buffers
74 // based on the input stored in flag.Args()
75 func LoadInput() []*Buffer {
76         // There are a number of ways micro should start given its input
77
78         // 1. If it is given a files in flag.Args(), it should open those
79
80         // 2. If there is no input file and the input is not a terminal, that means
81         // something is being piped in and the stdin should be opened in an
82         // empty buffer
83
84         // 3. If there is no input file and the input is a terminal, an empty buffer
85         // should be opened
86
87         var filename string
88         var input []byte
89         var err error
90         args := flag.Args()
91         buffers := make([]*Buffer, 0, len(args))
92
93         if len(args) > 0 {
94                 // Option 1
95                 // We go through each file and load it
96                 for i := 0; i < len(args); i++ {
97                         filename = args[i]
98
99                         // Check that the file exists
100                         var input *os.File
101                         if _, e := os.Stat(filename); e == nil {
102                                 // If it exists we load it into a buffer
103                                 input, err = os.Open(filename)
104                                 stat, _ := input.Stat()
105                                 defer input.Close()
106                                 if err != nil {
107                                         TermMessage(err)
108                                         continue
109                                 }
110                                 if stat.IsDir() {
111                                         TermMessage("Cannot read", filename, "because it is a directory")
112                                         continue
113                                 }
114                         }
115                         // If the file didn't exist, input will be empty, and we'll open an empty buffer
116                         if input != nil {
117                                 buffers = append(buffers, NewBuffer(input, FSize(input), filename))
118                         } else {
119                                 buffers = append(buffers, NewBufferFromString("", filename))
120                         }
121                 }
122         } else if !isatty.IsTerminal(os.Stdin.Fd()) {
123                 // Option 2
124                 // The input is not a terminal, so something is being piped in
125                 // and we should read from stdin
126                 input, err = ioutil.ReadAll(os.Stdin)
127                 if err != nil {
128                         TermMessage("Error reading from stdin: ", err)
129                         input = []byte{}
130                 }
131                 buffers = append(buffers, NewBufferFromString(string(input), filename))
132         } else {
133                 // Option 3, just open an empty buffer
134                 buffers = append(buffers, NewBufferFromString(string(input), filename))
135         }
136
137         return buffers
138 }
139
140 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
141 // If no directory is found, it creates one.
142 func InitConfigDir() {
143         xdgHome := os.Getenv("XDG_CONFIG_HOME")
144         if xdgHome == "" {
145                 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
146                 home, err := homedir.Dir()
147                 if err != nil {
148                         TermMessage("Error finding your home directory\nCan't load config files")
149                         return
150                 }
151                 xdgHome = home + "/.config"
152         }
153         configDir = xdgHome + "/micro"
154
155         if len(*flagConfigDir) > 0 {
156                 if _, err := os.Stat(*flagConfigDir); os.IsNotExist(err) {
157                         TermMessage("Error: " + *flagConfigDir + " does not exist. Defaulting to " + configDir + ".")
158                 } else {
159                         configDir = *flagConfigDir
160                         return
161                 }
162         }
163
164         if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
165                 // If the xdgHome doesn't exist we should create it
166                 err = os.Mkdir(xdgHome, os.ModePerm)
167                 if err != nil {
168                         TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
169                 }
170         }
171
172         if _, err := os.Stat(configDir); os.IsNotExist(err) {
173                 // If the micro specific config directory doesn't exist we should create that too
174                 err = os.Mkdir(configDir, os.ModePerm)
175                 if err != nil {
176                         TermMessage("Error creating configuration directory: " + err.Error())
177                 }
178         }
179 }
180
181 // InitScreen creates and initializes the tcell screen
182 func InitScreen() {
183         // Should we enable true color?
184         truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
185
186         // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
187         // initializing tcell, but after that, we can set the TERM back to whatever it was
188         oldTerm := os.Getenv("TERM")
189         if truecolor {
190                 os.Setenv("TERM", "xterm-truecolor")
191         }
192
193         // Initilize tcell
194         var err error
195         screen, err = tcell.NewScreen()
196         if err != nil {
197                 fmt.Println(err)
198                 if err == tcell.ErrTermNotFound {
199                         fmt.Println("Micro does not recognize your terminal:", oldTerm)
200                         fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
201                 }
202                 os.Exit(1)
203         }
204         if err = screen.Init(); err != nil {
205                 fmt.Println(err)
206                 os.Exit(1)
207         }
208
209         // Now we can put the TERM back to what it was before
210         if truecolor {
211                 os.Setenv("TERM", oldTerm)
212         }
213
214         if GetGlobalOption("mouse").(bool) {
215                 screen.EnableMouse()
216         }
217
218         // screen.SetStyle(defStyle)
219 }
220
221 // RedrawAll redraws everything -- all the views and the messenger
222 func RedrawAll() {
223         messenger.Clear()
224
225         w, h := screen.Size()
226         for x := 0; x < w; x++ {
227                 for y := 0; y < h; y++ {
228                         screen.SetContent(x, y, ' ', nil, defStyle)
229                 }
230         }
231
232         for _, v := range tabs[curTab].views {
233                 v.Display()
234         }
235         DisplayTabs()
236         messenger.Display()
237         if globalSettings["keymenu"].(bool) {
238                 DisplayKeyMenu()
239         }
240         screen.Show()
241
242         if numRedraw%50 == 0 {
243                 runtime.GC()
244         }
245         numRedraw++
246 }
247
248 func LoadAll() {
249         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
250         InitConfigDir()
251
252         // Build a list of available Extensions (Syntax, Colorscheme etc.)
253         InitRuntimeFiles()
254
255         // Load the user's settings
256         InitGlobalSettings()
257
258         InitCommands()
259         InitBindings()
260
261         InitColorscheme()
262
263         for _, tab := range tabs {
264                 for _, v := range tab.views {
265                         v.Buf.UpdateRules()
266                 }
267         }
268 }
269
270 // Command line flags
271 var flagVersion = flag.Bool("version", false, "Show the version number and information")
272 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
273 var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
274 var flagOptions = flag.Bool("options", false, "Show all option help")
275
276 func main() {
277         flag.Usage = func() {
278                 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
279                 fmt.Println("-config-dir dir")
280                 fmt.Println("    \tSpecify a custom location for the configuration directory")
281                 fmt.Println("-startpos LINE,COL")
282                 fmt.Println("    \tSpecify a line and column to start the cursor at when opening a buffer")
283                 fmt.Println("-options")
284                 fmt.Println("    \tShow all option help")
285                 fmt.Println("-version")
286                 fmt.Println("    \tShow the version number and information")
287
288                 fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
289                 fmt.Println("-option value")
290                 fmt.Println("    \tSet `option` to `value` for this session")
291                 fmt.Println("    \tFor example: `micro -syntax off file.c`")
292                 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
293         }
294
295         optionFlags := make(map[string]*string)
296
297         for k, v := range DefaultGlobalSettings() {
298                 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
299         }
300
301         flag.Parse()
302
303         if *flagVersion {
304                 // If -version was passed
305                 fmt.Println("Version:", Version)
306                 fmt.Println("Commit hash:", CommitHash)
307                 fmt.Println("Compiled on", CompileDate)
308                 os.Exit(0)
309         }
310
311         if *flagOptions {
312                 // If -options was passed
313                 for k, v := range DefaultGlobalSettings() {
314                         fmt.Printf("-%s value\n", k)
315                         fmt.Printf("    \tThe %s option. Default value: '%v'\n", k, v)
316                 }
317                 os.Exit(0)
318         }
319
320         // Start the Lua VM for running plugins
321         L = lua.NewState()
322         defer L.Close()
323
324         // Some encoding stuff in case the user isn't using UTF-8
325         encoding.Register()
326         tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
327
328         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
329         InitConfigDir()
330
331         // Build a list of available Extensions (Syntax, Colorscheme etc.)
332         InitRuntimeFiles()
333
334         // Load the user's settings
335         InitGlobalSettings()
336
337         InitCommands()
338         InitBindings()
339
340         // Start the screen
341         InitScreen()
342
343         // This is just so if we have an error, we can exit cleanly and not completely
344         // mess up the terminal being worked in
345         // In other words we need to shut down tcell before the program crashes
346         defer func() {
347                 if err := recover(); err != nil {
348                         screen.Fini()
349                         fmt.Println("Micro encountered an error:", err)
350                         // Print the stack trace too
351                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
352                         os.Exit(1)
353                 }
354         }()
355
356         // Create a new messenger
357         // This is used for sending the user messages in the bottom of the editor
358         messenger = new(Messenger)
359         messenger.LoadHistory()
360
361         // Now we load the input
362         buffers := LoadInput()
363         if len(buffers) == 0 {
364                 screen.Fini()
365                 os.Exit(1)
366         }
367
368         for _, buf := range buffers {
369                 // For each buffer we create a new tab and place the view in that tab
370                 tab := NewTabFromView(NewView(buf))
371                 tab.SetNum(len(tabs))
372                 tabs = append(tabs, tab)
373                 for _, t := range tabs {
374                         for _, v := range t.views {
375                                 v.Center(false)
376                         }
377
378                         t.Resize()
379                 }
380         }
381
382         for k, v := range optionFlags {
383                 if *v != "" {
384                         SetOption(k, *v)
385                 }
386         }
387
388         // Load all the plugin stuff
389         // We give plugins access to a bunch of variables here which could be useful to them
390         L.SetGlobal("OS", luar.New(L, runtime.GOOS))
391         L.SetGlobal("tabs", luar.New(L, tabs))
392         L.SetGlobal("curTab", luar.New(L, curTab))
393         L.SetGlobal("messenger", luar.New(L, messenger))
394         L.SetGlobal("GetOption", luar.New(L, GetOption))
395         L.SetGlobal("AddOption", luar.New(L, AddOption))
396         L.SetGlobal("SetOption", luar.New(L, SetOption))
397         L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
398         L.SetGlobal("BindKey", luar.New(L, BindKey))
399         L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
400         L.SetGlobal("CurView", luar.New(L, CurView))
401         L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
402         L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
403         L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
404         L.SetGlobal("ExecCommand", luar.New(L, ExecCommand))
405         L.SetGlobal("RunShellCommand", luar.New(L, RunShellCommand))
406         L.SetGlobal("RunBackgroundShell", luar.New(L, RunBackgroundShell))
407         L.SetGlobal("RunInteractiveShell", luar.New(L, RunInteractiveShell))
408         L.SetGlobal("TermEmuSupported", luar.New(L, TermEmuSupported))
409         L.SetGlobal("RunTermEmulator", luar.New(L, RunTermEmulator))
410         L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
411         L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
412         L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
413         L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
414                 return string(r)
415         }))
416         L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
417                 return Loc{x, y}
418         }))
419         L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
420         L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
421         L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
422         L.SetGlobal("configDir", luar.New(L, configDir))
423         L.SetGlobal("Reload", luar.New(L, LoadAll))
424         L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
425         L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
426
427         // Used for asynchronous jobs
428         L.SetGlobal("JobStart", luar.New(L, JobStart))
429         L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
430         L.SetGlobal("JobSend", luar.New(L, JobSend))
431         L.SetGlobal("JobStop", luar.New(L, JobStop))
432
433         // Extension Files
434         L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
435         L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
436         L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
437         L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
438         L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
439
440         // Access to Go stdlib
441         L.SetGlobal("import", luar.New(L, Import))
442
443         jobs = make(chan JobFunction, 100)
444         events = make(chan tcell.Event, 100)
445         autosave = make(chan bool)
446         updateterm = make(chan bool)
447         closeterm = make(chan int)
448
449         LoadPlugins()
450
451         for _, t := range tabs {
452                 for _, v := range t.views {
453                         GlobalPluginCall("onViewOpen", v)
454                         GlobalPluginCall("onBufferOpen", v.Buf)
455                 }
456         }
457
458         InitColorscheme()
459         messenger.style = defStyle
460
461         // Here is the event loop which runs in a separate thread
462         go func() {
463                 for {
464                         if screen != nil {
465                                 events <- screen.PollEvent()
466                         }
467                 }
468         }()
469
470         go func() {
471                 for {
472                         time.Sleep(autosaveTime * time.Second)
473                         if globalSettings["autosave"].(bool) {
474                                 autosave <- true
475                         }
476                 }
477         }()
478
479         for {
480                 // Display everything
481                 RedrawAll()
482
483                 var event tcell.Event
484
485                 // Check for new events
486                 select {
487                 case f := <-jobs:
488                         // If a new job has finished while running in the background we should execute the callback
489                         f.function(f.output, f.args...)
490                         continue
491                 case <-autosave:
492                         if CurView().Buf.Path != "" {
493                                 CurView().Save(true)
494                         }
495                 case <-updateterm:
496                         continue
497                 case vnum := <-closeterm:
498                         tabs[curTab].views[vnum].CloseTerminal()
499                 case event = <-events:
500                 }
501
502                 for event != nil {
503                         didAction := false
504
505                         switch e := event.(type) {
506                         case *tcell.EventResize:
507                                 for _, t := range tabs {
508                                         t.Resize()
509                                 }
510                         case *tcell.EventMouse:
511                                 if !searching {
512                                         if e.Buttons() == tcell.Button1 {
513                                                 // If the user left clicked we check a couple things
514                                                 _, h := screen.Size()
515                                                 x, y := e.Position()
516                                                 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
517                                                         // If the user clicked in the bottom bar, and there is a message down there
518                                                         // we copy it to the clipboard.
519                                                         // Often error messages are displayed down there so it can be useful to easily
520                                                         // copy the message
521                                                         clipboard.WriteAll(messenger.message, "primary")
522                                                         break
523                                                 }
524
525                                                 if CurView().mouseReleased {
526                                                         // We loop through each view in the current tab and make sure the current view
527                                                         // is the one being clicked in
528                                                         for _, v := range tabs[curTab].views {
529                                                                 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
530                                                                         tabs[curTab].CurView = v.Num
531                                                                 }
532                                                         }
533                                                 }
534                                         } else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown {
535                                                 var view *View
536                                                 x, y := e.Position()
537                                                 for _, v := range tabs[curTab].views {
538                                                         if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
539                                                                 view = tabs[curTab].views[v.Num]
540                                                         }
541                                                 }
542                                                 if view != nil {
543                                                         view.HandleEvent(e)
544                                                         didAction = true
545                                                 }
546                                         }
547                                 }
548                         }
549
550                         if !didAction {
551                                 // This function checks the mouse event for the possibility of changing the current tab
552                                 // If the tab was changed it returns true
553                                 if TabbarHandleMouseEvent(event) {
554                                         break
555                                 }
556
557                                 if searching {
558                                         // Since searching is done in real time, we need to redraw every time
559                                         // there is a new event in the search bar so we need a special function
560                                         // to run instead of the standard HandleEvent.
561                                         HandleSearchEvent(event, CurView())
562                                 } else {
563                                         // Send it to the view
564                                         CurView().HandleEvent(event)
565                                 }
566                         }
567
568                         select {
569                         case event = <-events:
570                         default:
571                                 event = nil
572                         }
573                 }
574         }
575 }