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"
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
32 // Object to send messages and prompts to the user
35 // The default highlighting style
36 // This simply defines the default foreground and background colors
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
44 // Version is the version number or commit hash
45 // These variables should be set by the linker when compiling
47 CommitHash = "Unknown"
48 CompileDate = "Unknown"
51 // This is the VM that runs the plugins
56 // This is the currently open tab
57 // It's just an index to the tab in the tabs array
60 // Channel of jobs running in the background
63 events chan tcell.Event
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
71 // 1. If it is given a files in flag.Args(), it should open those
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
77 // 3. If there is no input file and the input is a terminal, an empty buffer
85 if len(flag.Args()) > 0 {
87 // We go through each file and load it
88 for i := 0; i < len(flag.Args()); i++ {
89 filename = flag.Args()[i]
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)
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))
104 } else if !isatty.IsTerminal(os.Stdin.Fd()) {
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)
110 TermMessage("Error reading from stdin: ", err)
113 buffers = append(buffers, NewBuffer(input, filename))
115 // Option 3, just open an empty buffer
116 buffers = append(buffers, NewBuffer(input, filename))
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")
127 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
128 home, err := homedir.Dir()
130 TermMessage("Error finding your home directory\nCan't load config files")
133 xdgHome = home + "/.config"
135 configDir = xdgHome + "/micro"
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)
141 TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
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)
149 TermMessage("Error creating configuration directory: " + err.Error())
154 // InitScreen creates and initializes the tcell screen
156 // Should we enable true color?
157 truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
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")
163 os.Setenv("TERM", "xterm-truecolor")
168 screen, err = tcell.NewScreen()
173 if err = screen.Init(); err != nil {
178 // Now we can put the TERM back to what it was before
180 os.Setenv("TERM", oldTerm)
183 screen.SetStyle(defStyle)
187 // RedrawAll redraws everything -- all the views and the messenger
190 for _, v := range tabs[curTab].views {
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.")
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")
209 optionFlags := make(map[string]*string)
211 for k, v := range DefaultGlobalSettings() {
212 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
218 // If -version was passed
219 fmt.Println("Version:", Version)
220 fmt.Println("Commit hash:", CommitHash)
221 fmt.Println("Compiled on", CompileDate)
225 // Start the Lua VM for running plugins
229 // Some encoding stuff in case the user isn't using UTF-8
231 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
233 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
236 // Load the user's settings
242 // Load the syntax files, including the colorscheme
245 // Load the help files
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
255 if err := recover(); err != nil {
257 fmt.Println("Micro encountered an error:", err)
258 // Print the stack trace too
259 fmt.Print(errors.Wrap(err, 2).ErrorStack())
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)
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 {
279 if globalSettings["syntax"].(bool) {
286 for k, v := range optionFlags {
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))
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))
319 jobs = make(chan JobFunction, 100)
320 events = make(chan tcell.Event)
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") {
331 if v.Buf.Settings["syntax"].(bool) {
337 // Here is the event loop which runs in a separate thread
340 events <- screen.PollEvent()
345 // Display everything
348 var event tcell.Event
350 // Check for new events
353 // If a new job has finished while running in the background we should execute the callback
354 f.function(f.output, f.args...)
356 case event = <-events:
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()
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
370 clipboard.WriteAll(messenger.message, "primary")
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
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) {
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())
398 // Send it to the view
399 CurView().HandleEvent(event)