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