]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
Fix replace cursor relocation
[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 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
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         jobs   chan JobFunction
58         events chan tcell.Event
59 )
60
61 // LoadInput loads the file input for the editor
62 func LoadInput() []*Buffer {
63         // There are a number of ways micro should start given its input
64
65         // 1. If it is given a file in os.Args, it should open that
66
67         // 2. If there is no input file and the input is not a terminal, that means
68         // something is being piped in and the stdin should be opened in an
69         // empty buffer
70
71         // 3. If there is no input file and the input is a terminal, an empty buffer
72         // should be opened
73
74         // These are empty by default so if we get to option 3, we can just returns the
75         // default values
76         var filename string
77         var input []byte
78         var err error
79         var buffers []*Buffer
80
81         if len(os.Args) > 1 {
82                 // Option 1
83                 for i := 1; i < len(os.Args); i++ {
84                         filename = os.Args[i]
85                         // Check that the file exists
86                         if _, e := os.Stat(filename); e == nil {
87                                 input, err = ioutil.ReadFile(filename)
88                                 if err != nil {
89                                         TermMessage(err)
90                                         continue
91                                 }
92                         }
93                         buffers = append(buffers, NewBuffer(input, filename))
94                 }
95         } else if !isatty.IsTerminal(os.Stdin.Fd()) {
96                 // Option 2
97                 // The input is not a terminal, so something is being piped in
98                 // and we should read from stdin
99                 input, err = ioutil.ReadAll(os.Stdin)
100                 buffers = append(buffers, NewBuffer(input, filename))
101         } else {
102                 // Option 3, just open an empty buffer
103                 buffers = append(buffers, NewBuffer(input, filename))
104         }
105
106         return buffers
107 }
108
109 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
110 // If no directory is found, it creates one.
111 func InitConfigDir() {
112         xdgHome := os.Getenv("XDG_CONFIG_HOME")
113         if xdgHome == "" {
114                 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
115                 home, err := homedir.Dir()
116                 if err != nil {
117                         TermMessage("Error finding your home directory\nCan't load config files")
118                         return
119                 }
120                 xdgHome = home + "/.config"
121         }
122         configDir = xdgHome + "/micro"
123
124         if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
125                 // If the xdgHome doesn't exist we should create it
126                 err = os.Mkdir(xdgHome, os.ModePerm)
127                 if err != nil {
128                         TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
129                 }
130         }
131
132         if _, err := os.Stat(configDir); os.IsNotExist(err) {
133                 // If the micro specific config directory doesn't exist we should create that too
134                 err = os.Mkdir(configDir, os.ModePerm)
135                 if err != nil {
136                         TermMessage("Error creating configuration directory: " + err.Error())
137                 }
138         }
139 }
140
141 // InitScreen creates and initializes the tcell screen
142 func InitScreen() {
143         // Should we enable true color?
144         truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
145
146         // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
147         // initializing tcell, but after that, we can set the TERM back to whatever it was
148         oldTerm := os.Getenv("TERM")
149         if truecolor {
150                 os.Setenv("TERM", "xterm-truecolor")
151         }
152
153         // Initilize tcell
154         var err error
155         screen, err = tcell.NewScreen()
156         if err != nil {
157                 fmt.Println(err)
158                 os.Exit(1)
159         }
160         if err = screen.Init(); err != nil {
161                 fmt.Println(err)
162                 os.Exit(1)
163         }
164
165         // Now we can put the TERM back to what it was before
166         if truecolor {
167                 os.Setenv("TERM", oldTerm)
168         }
169
170         screen.SetStyle(defStyle)
171         screen.EnableMouse()
172 }
173
174 // RedrawAll redraws everything -- all the views and the messenger
175 func RedrawAll() {
176         messenger.Clear()
177         for _, v := range tabs[curTab].views {
178                 v.Display()
179         }
180         DisplayTabs()
181         messenger.Display()
182         screen.Show()
183 }
184
185 var flagVersion = flag.Bool("version", false, "Show version number")
186
187 func main() {
188         flag.Parse()
189         if *flagVersion {
190                 fmt.Println("Micro version:", Version)
191                 os.Exit(0)
192         }
193
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         // Load the user's settings
204         InitSettings()
205         InitCommands()
206         InitBindings()
207         // Load the syntax files, including the colorscheme
208         LoadSyntaxFiles()
209         // Load the help files
210         LoadHelp()
211
212         InitScreen()
213
214         // This is just so if we have an error, we can exit cleanly and not completely
215         // mess up the terminal being worked in
216         // In other words we need to shut down tcell before the program crashes
217         defer func() {
218                 if err := recover(); err != nil {
219                         screen.Fini()
220                         fmt.Println("Micro encountered an error:", err)
221                         // Print the stack trace too
222                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
223                         os.Exit(1)
224                 }
225         }()
226
227         messenger = new(Messenger)
228         messenger.history = make(map[string][]string)
229
230         buffers := LoadInput()
231         for _, buf := range buffers {
232                 tab := NewTabFromView(NewView(buf))
233                 tab.SetNum(len(tabs))
234                 tabs = append(tabs, tab)
235                 for _, t := range tabs {
236                         for _, v := range t.views {
237                                 v.Resize(screen.Size())
238                         }
239                 }
240         }
241
242         L.SetGlobal("OS", luar.New(L, runtime.GOOS))
243         L.SetGlobal("tabs", luar.New(L, tabs))
244         L.SetGlobal("curTab", luar.New(L, curTab))
245         L.SetGlobal("messenger", luar.New(L, messenger))
246         L.SetGlobal("GetOption", luar.New(L, GetOption))
247         L.SetGlobal("AddOption", luar.New(L, AddOption))
248         L.SetGlobal("BindKey", luar.New(L, BindKey))
249         L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
250         L.SetGlobal("CurView", luar.New(L, CurView))
251         L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
252
253         L.SetGlobal("JobStart", luar.New(L, JobStart))
254         L.SetGlobal("JobSend", luar.New(L, JobSend))
255         L.SetGlobal("JobStop", luar.New(L, JobStop))
256
257         LoadPlugins()
258
259         jobs = make(chan JobFunction, 100)
260         events = make(chan tcell.Event)
261
262         go func() {
263                 for {
264                         events <- screen.PollEvent()
265                 }
266         }()
267
268         for {
269                 // Display everything
270                 RedrawAll()
271
272                 var event tcell.Event
273                 select {
274                 case f := <-jobs:
275                         f.function(f.output, f.args...)
276                         continue
277                 case event = <-events:
278                 }
279
280                 switch e := event.(type) {
281                 case *tcell.EventMouse:
282                         if e.Buttons() == tcell.Button1 {
283                                 _, h := screen.Size()
284                                 _, y := e.Position()
285                                 if y == h-1 && messenger.message != "" {
286                                         clipboard.WriteAll(messenger.message)
287                                         continue
288                                 }
289                         }
290                 }
291
292                 if TabbarHandleMouseEvent(event) {
293                         continue
294                 }
295
296                 if searching {
297                         // Since searching is done in real time, we need to redraw every time
298                         // there is a new event in the search bar
299                         HandleSearchEvent(event, CurView())
300                 } else {
301                         // Send it to the view
302                         CurView().HandleEvent(event)
303                 }
304         }
305 }