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