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