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