]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
Merge branch 'master' of https://github.com/to-miz/micro into to-miz-master
[micro.git] / cmd / micro / micro.go
1 package main
2
3 import (
4         "flag"
5         "fmt"
6         "io/ioutil"
7         "os"
8         "runtime"
9         "strings"
10
11         "github.com/go-errors/errors"
12         "github.com/layeh/gopher-luar"
13         "github.com/mattn/go-isatty"
14         "github.com/mitchellh/go-homedir"
15         "github.com/yuin/gopher-lua"
16         "github.com/zyedidia/clipboard"
17         "github.com/zyedidia/tcell"
18         "github.com/zyedidia/tcell/encoding"
19 )
20
21 const (
22         synLinesUp           = 75  // How many lines up to look to do syntax highlighting
23         synLinesDown         = 75  // How many lines down to look to do syntax highlighting
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 )
27
28 var (
29         // The main screen
30         screen tcell.Screen
31
32         // Object to send messages and prompts to the user
33         messenger *Messenger
34
35         // The default highlighting style
36         // This simply defines the default foreground and background colors
37         defStyle tcell.Style
38
39         // Where the user's configuration is
40         // This should be $XDG_CONFIG_HOME/micro
41         // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
42         configDir string
43
44         // Version is the version number or commit hash
45         // These variables should be set by the linker when compiling
46         Version     = "Unknown"
47         CommitHash  = "Unknown"
48         CompileDate = "Unknown"
49
50         // L is the lua state
51         // This is the VM that runs the plugins
52         L *lua.LState
53
54         // The list of views
55         tabs []*Tab
56         // This is the currently open tab
57         // It's just an index to the tab in the tabs array
58         curTab int
59
60         // Channel of jobs running in the background
61         jobs chan JobFunction
62         // Event channel
63         events chan tcell.Event
64 )
65
66 // LoadInput determines which files should be loaded into buffers
67 // based on the input stored in flag.Args()
68 func LoadInput() []*Buffer {
69         // There are a number of ways micro should start given its input
70
71         // 1. If it is given a files in flag.Args(), it should open those
72
73         // 2. If there is no input file and the input is not a terminal, that means
74         // something is being piped in and the stdin should be opened in an
75         // empty buffer
76
77         // 3. If there is no input file and the input is a terminal, an empty buffer
78         // should be opened
79
80         var filename string
81         var input []byte
82         var err error
83         var buffers []*Buffer
84
85         if len(flag.Args()) > 0 {
86                 // Option 1
87                 // We go through each file and load it
88                 for i := 0; i < len(flag.Args()); i++ {
89                         filename = flag.Args()[i]
90
91                         // Check that the file exists
92                         if _, e := os.Stat(filename); e == nil {
93                                 // If it exists we load it into a buffer
94                                 input, err = ioutil.ReadFile(filename)
95                                 if err != nil {
96                                         TermMessage(err)
97                                         input = []byte{}
98                                         filename = ""
99                                 }
100                         }
101                         // If the file didn't exist, input will be empty, and we'll open an empty buffer
102                         buffers = append(buffers, NewBuffer(input, filename))
103                 }
104         } else if !isatty.IsTerminal(os.Stdin.Fd()) {
105                 // Option 2
106                 // The input is not a terminal, so something is being piped in
107                 // and we should read from stdin
108                 input, err = ioutil.ReadAll(os.Stdin)
109                 if err != nil {
110                         TermMessage("Error reading from stdin: ", err)
111                         input = []byte{}
112                 }
113                 buffers = append(buffers, NewBuffer(input, filename))
114         } else {
115                 // Option 3, just open an empty buffer
116                 buffers = append(buffers, NewBuffer(input, filename))
117         }
118
119         return buffers
120 }
121
122 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
123 // If no directory is found, it creates one.
124 func InitConfigDir() {
125         xdgHome := os.Getenv("XDG_CONFIG_HOME")
126         if xdgHome == "" {
127                 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
128                 home, err := homedir.Dir()
129                 if err != nil {
130                         TermMessage("Error finding your home directory\nCan't load config files")
131                         return
132                 }
133                 xdgHome = home + "/.config"
134         }
135         configDir = xdgHome + "/micro"
136
137         if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
138                 // If the xdgHome doesn't exist we should create it
139                 err = os.Mkdir(xdgHome, os.ModePerm)
140                 if err != nil {
141                         TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
142                 }
143         }
144
145         if _, err := os.Stat(configDir); os.IsNotExist(err) {
146                 // If the micro specific config directory doesn't exist we should create that too
147                 err = os.Mkdir(configDir, os.ModePerm)
148                 if err != nil {
149                         TermMessage("Error creating configuration directory: " + err.Error())
150                 }
151         }
152 }
153
154 // InitScreen creates and initializes the tcell screen
155 func InitScreen() {
156         // Should we enable true color?
157         truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
158
159         // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
160         // initializing tcell, but after that, we can set the TERM back to whatever it was
161         oldTerm := os.Getenv("TERM")
162         if truecolor {
163                 os.Setenv("TERM", "xterm-truecolor")
164         }
165
166         // Initilize tcell
167         var err error
168         screen, err = tcell.NewScreen()
169         if err != nil {
170                 fmt.Println(err)
171                 os.Exit(1)
172         }
173         if err = screen.Init(); err != nil {
174                 fmt.Println(err)
175                 os.Exit(1)
176         }
177
178         // Now we can put the TERM back to what it was before
179         if truecolor {
180                 os.Setenv("TERM", oldTerm)
181         }
182
183         screen.SetStyle(defStyle)
184         screen.EnableMouse()
185 }
186
187 // RedrawAll redraws everything -- all the views and the messenger
188 func RedrawAll() {
189         messenger.Clear()
190         for _, v := range tabs[curTab].views {
191                 v.Display()
192         }
193         DisplayTabs()
194         messenger.Display()
195         screen.Show()
196 }
197
198 // Passing -version as a flag will have micro print out the version number
199 var flagVersion = flag.Bool("version", false, "Show the version number and information")
200 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
201
202 func main() {
203         flag.Usage = func() {
204                 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
205                 fmt.Println("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")
206                 flag.PrintDefaults()
207         }
208
209         optionFlags := make(map[string]*string)
210
211         for k, v := range DefaultGlobalSettings() {
212                 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
213         }
214
215         flag.Parse()
216
217         if *flagVersion {
218                 // If -version was passed
219                 fmt.Println("Version:", Version)
220                 fmt.Println("Commit hash:", CommitHash)
221                 fmt.Println("Compiled on", CompileDate)
222                 os.Exit(0)
223         }
224
225         // Start the Lua VM for running plugins
226         L = lua.NewState()
227         defer L.Close()
228
229         // Some encoding stuff in case the user isn't using UTF-8
230         encoding.Register()
231         tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
232
233         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
234         InitConfigDir()
235
236         // Load the user's settings
237         InitGlobalSettings()
238
239         InitCommands()
240         InitBindings()
241
242         // Load the syntax files, including the colorscheme
243         LoadSyntaxFiles()
244
245         // Load the help files
246         LoadHelp()
247
248         // Start the screen
249         InitScreen()
250
251         // This is just so if we have an error, we can exit cleanly and not completely
252         // mess up the terminal being worked in
253         // In other words we need to shut down tcell before the program crashes
254         defer func() {
255                 if err := recover(); err != nil {
256                         screen.Fini()
257                         fmt.Println("Micro encountered an error:", err)
258                         // Print the stack trace too
259                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
260                         os.Exit(1)
261                 }
262         }()
263
264         // Create a new messenger
265         // This is used for sending the user messages in the bottom of the editor
266         messenger = new(Messenger)
267         messenger.history = make(map[string][]string)
268
269         // Now we load the input
270         buffers := LoadInput()
271         for _, buf := range buffers {
272                 // For each buffer we create a new tab and place the view in that tab
273                 tab := NewTabFromView(NewView(buf))
274                 tab.SetNum(len(tabs))
275                 tabs = append(tabs, tab)
276                 for _, t := range tabs {
277                         for _, v := range t.views {
278                                 v.Center(false)
279                                 if globalSettings["syntax"].(bool) {
280                                         v.matches = Match(v)
281                                 }
282                         }
283                 }
284         }
285
286         for k, v := range optionFlags {
287                 if *v != "" {
288                         SetOption(k, *v)
289                 }
290         }
291
292         // Load all the plugin stuff
293         // We give plugins access to a bunch of variables here which could be useful to them
294         L.SetGlobal("OS", luar.New(L, runtime.GOOS))
295         L.SetGlobal("tabs", luar.New(L, tabs))
296         L.SetGlobal("curTab", luar.New(L, curTab))
297         L.SetGlobal("messenger", luar.New(L, messenger))
298         L.SetGlobal("GetOption", luar.New(L, GetOption))
299         L.SetGlobal("AddOption", luar.New(L, AddOption))
300         L.SetGlobal("SetOption", luar.New(L, SetOption))
301         L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
302         L.SetGlobal("BindKey", luar.New(L, BindKey))
303         L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
304         L.SetGlobal("CurView", luar.New(L, CurView))
305         L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
306         L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
307         L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
308         L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
309         L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
310         L.SetGlobal("NewBuffer", luar.New(L, NewBuffer))
311
312         // Used for asynchronous jobs
313         L.SetGlobal("JobStart", luar.New(L, JobStart))
314         L.SetGlobal("JobSend", luar.New(L, JobSend))
315         L.SetGlobal("JobStop", luar.New(L, JobStop))
316
317         LoadPlugins()
318
319         jobs = make(chan JobFunction, 100)
320         events = make(chan tcell.Event)
321
322         for _, t := range tabs {
323                 for _, v := range t.views {
324                         for _, pl := range loadedPlugins {
325                                 _, err := Call(pl+".onViewOpen", v)
326                                 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
327                                         TermMessage(err)
328                                         continue
329                                 }
330                         }
331                         if v.Buf.Settings["syntax"].(bool) {
332                                 v.matches = Match(v)
333                         }
334                 }
335         }
336
337         // Here is the event loop which runs in a separate thread
338         go func() {
339                 for {
340                         events <- screen.PollEvent()
341                 }
342         }()
343
344         for {
345                 // Display everything
346                 RedrawAll()
347
348                 var event tcell.Event
349
350                 // Check for new events
351                 select {
352                 case f := <-jobs:
353                         // If a new job has finished while running in the background we should execute the callback
354                         f.function(f.output, f.args...)
355                         continue
356                 case event = <-events:
357                 }
358
359                 switch e := event.(type) {
360                 case *tcell.EventMouse:
361                         if e.Buttons() == tcell.Button1 {
362                                 // If the user left clicked we check a couple things
363                                 _, h := screen.Size()
364                                 x, y := e.Position()
365                                 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
366                                         // If the user clicked in the bottom bar, and there is a message down there
367                                         // we copy it to the clipboard.
368                                         // Often error messages are displayed down there so it can be useful to easily
369                                         // copy the message
370                                         clipboard.WriteAll(messenger.message, "primary")
371                                         continue
372                                 }
373
374                                 if CurView().mouseReleased {
375                                         // We loop through each view in the current tab and make sure the current view
376                                         // is the one being clicked in
377                                         for _, v := range tabs[curTab].views {
378                                                 if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
379                                                         tabs[curTab].curView = v.Num
380                                                 }
381                                         }
382                                 }
383                         }
384                 }
385
386                 // This function checks the mouse event for the possibility of changing the current tab
387                 // If the tab was changed it returns true
388                 if TabbarHandleMouseEvent(event) {
389                         continue
390                 }
391
392                 if searching {
393                         // Since searching is done in real time, we need to redraw every time
394                         // there is a new event in the search bar so we need a special function
395                         // to run instead of the standard HandleEvent.
396                         HandleSearchEvent(event, CurView())
397                 } else {
398                         // Send it to the view
399                         CurView().HandleEvent(event)
400                 }
401         }
402 }