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