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