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