]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
ab654481e78aee13841c8dc74f84602e0b9bd692
[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/layeh/gopher-luar"
15         "github.com/mattn/go-isatty"
16         "github.com/mitchellh/go-homedir"
17         "github.com/yuin/gopher-lua"
18         "github.com/zyedidia/clipboard"
19         "github.com/zyedidia/tcell"
20         "github.com/zyedidia/tcell/encoding"
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                                 defer input.Close()
101                                 if err != nil {
102                                         TermMessage(err)
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, filename))
109                         } else {
110                                 buffers = append(buffers, NewBuffer(strings.NewReader(""), 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, NewBuffer(strings.NewReader(string(input)), filename))
123         } else {
124                 // Option 3, just open an empty buffer
125                 buffers = append(buffers, NewBuffer(strings.NewReader(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 _, err := os.Stat(xdgHome); os.IsNotExist(err) {
147                 // If the xdgHome doesn't exist we should create it
148                 err = os.Mkdir(xdgHome, os.ModePerm)
149                 if err != nil {
150                         TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
151                 }
152         }
153
154         if _, err := os.Stat(configDir); os.IsNotExist(err) {
155                 // If the micro specific config directory doesn't exist we should create that too
156                 err = os.Mkdir(configDir, os.ModePerm)
157                 if err != nil {
158                         TermMessage("Error creating configuration directory: " + err.Error())
159                 }
160         }
161 }
162
163 // InitScreen creates and initializes the tcell screen
164 func InitScreen() {
165         // Should we enable true color?
166         truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
167
168         // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
169         // initializing tcell, but after that, we can set the TERM back to whatever it was
170         oldTerm := os.Getenv("TERM")
171         if truecolor {
172                 os.Setenv("TERM", "xterm-truecolor")
173         }
174
175         // Initilize tcell
176         var err error
177         screen, err = tcell.NewScreen()
178         if err != nil {
179                 fmt.Println(err)
180                 os.Exit(1)
181         }
182         if err = screen.Init(); err != nil {
183                 fmt.Println(err)
184                 os.Exit(1)
185         }
186
187         // Now we can put the TERM back to what it was before
188         if truecolor {
189                 os.Setenv("TERM", oldTerm)
190         }
191
192         screen.SetStyle(defStyle)
193         screen.EnableMouse()
194 }
195
196 // RedrawAll redraws everything -- all the views and the messenger
197 func RedrawAll() {
198         messenger.Clear()
199         for _, v := range tabs[curTab].views {
200                 v.Display()
201         }
202         DisplayTabs()
203         messenger.Display()
204         screen.Show()
205 }
206
207 func LoadAll() {
208         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
209         InitConfigDir()
210
211         // Build a list of available Extensions (Syntax, Colorscheme etc.)
212         InitRuntimeFiles()
213
214         // Load the user's settings
215         InitGlobalSettings()
216
217         InitCommands()
218         InitBindings()
219
220         LoadSyntaxFiles()
221
222         for _, tab := range tabs {
223                 for _, v := range tab.views {
224                         v.Buf.UpdateRules()
225                         if v.Buf.Settings["syntax"].(bool) {
226                                 v.matches = Match(v)
227                         }
228                 }
229         }
230 }
231
232 // Passing -version as a flag will have micro print out the version number
233 var flagVersion = flag.Bool("version", false, "Show the version number and information")
234 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
235
236 func main() {
237         flag.Usage = func() {
238                 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
239                 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")
240                 flag.PrintDefaults()
241         }
242
243         optionFlags := make(map[string]*string)
244
245         for k, v := range DefaultGlobalSettings() {
246                 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
247         }
248
249         flag.Parse()
250
251         if *flagVersion {
252                 // If -version was passed
253                 fmt.Println("Version:", Version)
254                 fmt.Println("Commit hash:", CommitHash)
255                 fmt.Println("Compiled on", CompileDate)
256                 os.Exit(0)
257         }
258
259         // Start the Lua VM for running plugins
260         L = lua.NewState()
261         defer L.Close()
262
263         // Some encoding stuff in case the user isn't using UTF-8
264         encoding.Register()
265         tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
266
267         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
268         InitConfigDir()
269
270         // Build a list of available Extensions (Syntax, Colorscheme etc.)
271         InitRuntimeFiles()
272
273         // Load the user's settings
274         InitGlobalSettings()
275
276         InitCommands()
277         InitBindings()
278
279         // Start the screen
280         InitScreen()
281
282         // This is just so if we have an error, we can exit cleanly and not completely
283         // mess up the terminal being worked in
284         // In other words we need to shut down tcell before the program crashes
285         defer func() {
286                 if err := recover(); err != nil {
287                         screen.Fini()
288                         fmt.Println("Micro encountered an error:", err)
289                         // Print the stack trace too
290                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
291                         os.Exit(1)
292                 }
293         }()
294
295         // Create a new messenger
296         // This is used for sending the user messages in the bottom of the editor
297         messenger = new(Messenger)
298         messenger.history = make(map[string][]string)
299
300         // Now we load the input
301         buffers := LoadInput()
302         if len(buffers) == 0 {
303                 screen.Fini()
304                 os.Exit(1)
305         }
306         for _, buf := range buffers {
307                 // For each buffer we create a new tab and place the view in that tab
308                 tab := NewTabFromView(NewView(buf))
309                 tab.SetNum(len(tabs))
310                 tabs = append(tabs, tab)
311                 for _, t := range tabs {
312                         for _, v := range t.views {
313                                 v.Center(false)
314                         }
315
316                         t.Resize()
317                 }
318         }
319
320         for k, v := range optionFlags {
321                 if *v != "" {
322                         SetOption(k, *v)
323                 }
324         }
325
326         // Load all the plugin stuff
327         // We give plugins access to a bunch of variables here which could be useful to them
328         L.SetGlobal("OS", luar.New(L, runtime.GOOS))
329         L.SetGlobal("tabs", luar.New(L, tabs))
330         L.SetGlobal("curTab", luar.New(L, curTab))
331         L.SetGlobal("messenger", luar.New(L, messenger))
332         L.SetGlobal("GetOption", luar.New(L, GetOption))
333         L.SetGlobal("AddOption", luar.New(L, AddOption))
334         L.SetGlobal("SetOption", luar.New(L, SetOption))
335         L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
336         L.SetGlobal("BindKey", luar.New(L, BindKey))
337         L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
338         L.SetGlobal("CurView", luar.New(L, CurView))
339         L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
340         L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
341         L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
342         L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
343         L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
344         L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
345         L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
346                 return string(r)
347         }))
348         L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
349                 return Loc{x, y}
350         }))
351         L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
352         L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
353         L.SetGlobal("configDir", luar.New(L, configDir))
354         L.SetGlobal("Reload", luar.New(L, LoadAll))
355         L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
356         L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
357
358         // Used for asynchronous jobs
359         L.SetGlobal("JobStart", luar.New(L, JobStart))
360         L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
361         L.SetGlobal("JobSend", luar.New(L, JobSend))
362         L.SetGlobal("JobStop", luar.New(L, JobStop))
363
364         // Extension Files
365         L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
366         L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
367         L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
368         L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
369
370         jobs = make(chan JobFunction, 100)
371         events = make(chan tcell.Event, 100)
372         autosave = make(chan bool)
373
374         LoadPlugins()
375
376         // Load the syntax files, including the colorscheme
377         LoadSyntaxFiles()
378
379         for _, t := range tabs {
380                 for _, v := range t.views {
381                         v.Buf.FindFileType()
382                         v.Buf.UpdateRules()
383                         for _, pl := range loadedPlugins {
384                                 _, err := Call(pl+".onViewOpen", v)
385                                 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
386                                         TermMessage(err)
387                                         continue
388                                 }
389                         }
390                         if v.Buf.Settings["syntax"].(bool) {
391                                 v.matches = Match(v)
392                         }
393                 }
394         }
395
396         // Here is the event loop which runs in a separate thread
397         go func() {
398                 for {
399                         events <- screen.PollEvent()
400                 }
401         }()
402
403         go func() {
404                 for {
405                         time.Sleep(autosaveTime * time.Second)
406                         if globalSettings["autosave"].(bool) {
407                                 autosave <- true
408                         }
409                 }
410         }()
411
412         for {
413                 // Display everything
414                 RedrawAll()
415
416                 var event tcell.Event
417
418                 // Check for new events
419                 select {
420                 case f := <-jobs:
421                         // If a new job has finished while running in the background we should execute the callback
422                         f.function(f.output, f.args...)
423                         continue
424                 case <-autosave:
425                         CurView().Save(true)
426                 case event = <-events:
427                 }
428
429                 for event != nil {
430                         switch e := event.(type) {
431                         case *tcell.EventMouse:
432                                 if e.Buttons() == tcell.Button1 {
433                                         // If the user left clicked we check a couple things
434                                         _, h := screen.Size()
435                                         x, y := e.Position()
436                                         if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
437                                                 // If the user clicked in the bottom bar, and there is a message down there
438                                                 // we copy it to the clipboard.
439                                                 // Often error messages are displayed down there so it can be useful to easily
440                                                 // copy the message
441                                                 clipboard.WriteAll(messenger.message, "primary")
442                                                 break
443                                         }
444
445                                         if CurView().mouseReleased {
446                                                 // We loop through each view in the current tab and make sure the current view
447                                                 // is the one being clicked in
448                                                 for _, v := range tabs[curTab].views {
449                                                         if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
450                                                                 tabs[curTab].CurView = v.Num
451                                                         }
452                                                 }
453                                         }
454                                 }
455                         }
456
457                         // This function checks the mouse event for the possibility of changing the current tab
458                         // If the tab was changed it returns true
459                         if TabbarHandleMouseEvent(event) {
460                                 break
461                         }
462
463                         if searching {
464                                 // Since searching is done in real time, we need to redraw every time
465                                 // there is a new event in the search bar so we need a special function
466                                 // to run instead of the standard HandleEvent.
467                                 HandleSearchEvent(event, CurView())
468                         } else {
469                                 // Send it to the view
470                                 CurView().HandleEvent(event)
471                         }
472
473                         select {
474                         case event = <-events:
475                         default:
476                                 event = nil
477                         }
478
479                 }
480         }
481 }