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