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