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