12 "github.com/go-errors/errors"
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 "layeh.com/gopher-luar"
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 autosaveTime = 8 // Number of seconds to wait before autosaving
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
46 Version = "0.0.0-unknown"
47 // CommitHash is the commit this version was built on
48 CommitHash = "Unknown"
49 // CompileDate is the date this binary was compiled on
50 CompileDate = "Unknown"
54 // This is the currently open tab
55 // It's just an index to the tab in the tabs array
58 // Channel of jobs running in the background
61 events chan tcell.Event
67 // LoadInput determines which files should be loaded into buffers
68 // based on the input stored in flag.Args()
69 func LoadInput() []*Buffer {
70 // There are a number of ways micro should start given its input
72 // 1. If it is given a files in flag.Args(), it should open those
74 // 2. If there is no input file and the input is not a terminal, that means
75 // something is being piped in and the stdin should be opened in an
78 // 3. If there is no input file and the input is a terminal, an empty buffer
85 buffers := make([]*Buffer, 0, len(args))
89 // We go through each file and load it
90 for i := 0; i < len(args); i++ {
93 // Check that the file exists
95 if _, e := os.Stat(filename); e == nil {
96 // If it exists we load it into a buffer
97 input, err = os.Open(filename)
98 stat, _ := input.Stat()
105 TermMessage("Cannot read", filename, "because it is a directory")
109 // If the file didn't exist, input will be empty, and we'll open an empty buffer
111 buffers = append(buffers, NewBuffer(input, FSize(input), filename))
113 buffers = append(buffers, NewBufferFromString("", filename))
116 } else if !isatty.IsTerminal(os.Stdin.Fd()) {
118 // The input is not a terminal, so something is being piped in
119 // and we should read from stdin
120 input, err = ioutil.ReadAll(os.Stdin)
122 TermMessage("Error reading from stdin: ", err)
125 buffers = append(buffers, NewBufferFromString(string(input), filename))
127 // Option 3, just open an empty buffer
128 buffers = append(buffers, NewBufferFromString(string(input), filename))
134 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
135 // If no directory is found, it creates one.
136 func InitConfigDir() {
137 xdgHome := os.Getenv("XDG_CONFIG_HOME")
139 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
140 home, err := homedir.Dir()
142 TermMessage("Error finding your home directory\nCan't load config files")
145 xdgHome = home + "/.config"
147 configDir = xdgHome + "/micro"
149 if len(*flagConfigDir) > 0 {
150 if _, err := os.Stat(*flagConfigDir); os.IsNotExist(err) {
151 TermMessage("Error: " + *flagConfigDir + " does not exist. Defaulting to " + configDir + ".")
153 configDir = *flagConfigDir
158 if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
159 // If the xdgHome doesn't exist we should create it
160 err = os.Mkdir(xdgHome, os.ModePerm)
162 TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
166 if _, err := os.Stat(configDir); os.IsNotExist(err) {
167 // If the micro specific config directory doesn't exist we should create that too
168 err = os.Mkdir(configDir, os.ModePerm)
170 TermMessage("Error creating configuration directory: " + err.Error())
175 // InitScreen creates and initializes the tcell screen
177 // Should we enable true color?
178 truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
180 // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
181 // initializing tcell, but after that, we can set the TERM back to whatever it was
182 oldTerm := os.Getenv("TERM")
184 os.Setenv("TERM", "xterm-truecolor")
189 screen, err = tcell.NewScreen()
192 if err == tcell.ErrTermNotFound {
193 fmt.Println("Micro does not recognize your terminal:", oldTerm)
194 fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
198 if err = screen.Init(); err != nil {
203 // Now we can put the TERM back to what it was before
205 os.Setenv("TERM", oldTerm)
208 if GetGlobalOption("mouse").(bool) {
212 // screen.SetStyle(defStyle)
215 // RedrawAll redraws everything -- all the views and the messenger
219 w, h := screen.Size()
220 for x := 0; x < w; x++ {
221 for y := 0; y < h; y++ {
222 screen.SetContent(x, y, ' ', nil, defStyle)
226 for _, v := range tabs[curTab].views {
231 if globalSettings["keymenu"].(bool) {
238 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
241 // Build a list of available Extensions (Syntax, Colorscheme etc.)
244 // Load the user's settings
252 for _, tab := range tabs {
253 for _, v := range tab.views {
259 // Command line flags
260 var flagVersion = flag.Bool("version", false, "Show the version number and information")
261 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
262 var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
263 var flagOptions = flag.Bool("options", false, "Show all option help")
266 flag.Usage = func() {
267 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
268 fmt.Println("-config-dir dir")
269 fmt.Println(" \tSpecify a custom location for the configuration directory")
270 fmt.Println("-startpos LINE,COL")
271 fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
272 fmt.Println("-options")
273 fmt.Println(" \tShow all option help")
274 fmt.Println("-version")
275 fmt.Println(" \tShow the version number and information")
277 fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
278 fmt.Println("-option value")
279 fmt.Println(" \tSet `option` to `value` for this session")
280 fmt.Println(" \tFor example: `micro -syntax off file.c`")
281 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
284 optionFlags := make(map[string]*string)
286 for k, v := range DefaultGlobalSettings() {
287 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
293 // If -version was passed
294 fmt.Println("Version:", Version)
295 fmt.Println("Commit hash:", CommitHash)
296 fmt.Println("Compiled on", CompileDate)
301 // If -options was passed
302 for k, v := range DefaultGlobalSettings() {
303 fmt.Printf("-%s value\n", k)
304 fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v)
309 // Start the Lua VM for running plugins
313 // Some encoding stuff in case the user isn't using UTF-8
315 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
317 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
320 // Build a list of available Extensions (Syntax, Colorscheme etc.)
323 // Load the user's settings
332 // This is just so if we have an error, we can exit cleanly and not completely
333 // mess up the terminal being worked in
334 // In other words we need to shut down tcell before the program crashes
336 if err := recover(); err != nil {
338 fmt.Println("Micro encountered an error:", err)
339 // Print the stack trace too
340 fmt.Print(errors.Wrap(err, 2).ErrorStack())
345 // Create a new messenger
346 // This is used for sending the user messages in the bottom of the editor
347 messenger = new(Messenger)
348 messenger.LoadHistory()
350 // Now we load the input
351 buffers := LoadInput()
352 if len(buffers) == 0 {
357 for _, buf := range buffers {
358 // For each buffer we create a new tab and place the view in that tab
359 tab := NewTabFromView(NewView(buf))
360 tab.SetNum(len(tabs))
361 tabs = append(tabs, tab)
362 for _, t := range tabs {
363 for _, v := range t.views {
371 for k, v := range optionFlags {
377 // Load all the plugin stuff
378 // We give plugins access to a bunch of variables here which could be useful to them
379 L.SetGlobal("OS", luar.New(L, runtime.GOOS))
380 L.SetGlobal("tabs", luar.New(L, tabs))
381 L.SetGlobal("curTab", luar.New(L, curTab))
382 L.SetGlobal("messenger", luar.New(L, messenger))
383 L.SetGlobal("GetOption", luar.New(L, GetOption))
384 L.SetGlobal("AddOption", luar.New(L, AddOption))
385 L.SetGlobal("SetOption", luar.New(L, SetOption))
386 L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
387 L.SetGlobal("BindKey", luar.New(L, BindKey))
388 L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
389 L.SetGlobal("CurView", luar.New(L, CurView))
390 L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
391 L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
392 L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
393 L.SetGlobal("ExecCommand", luar.New(L, ExecCommand))
394 L.SetGlobal("RunShellCommand", luar.New(L, RunShellCommand))
395 L.SetGlobal("RunBackgroundShell", luar.New(L, RunBackgroundShell))
396 L.SetGlobal("RunInteractiveShell", luar.New(L, RunInteractiveShell))
397 L.SetGlobal("TermEmuSupported", luar.New(L, TermEmuSupported))
398 L.SetGlobal("RunTermEmulator", luar.New(L, RunTermEmulator))
399 L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
400 L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
401 L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
402 L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
405 L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
408 L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
409 L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
410 L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
411 L.SetGlobal("configDir", luar.New(L, configDir))
412 L.SetGlobal("Reload", luar.New(L, LoadAll))
413 L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
414 L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
416 // Used for asynchronous jobs
417 L.SetGlobal("JobStart", luar.New(L, JobStart))
418 L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
419 L.SetGlobal("JobSend", luar.New(L, JobSend))
420 L.SetGlobal("JobStop", luar.New(L, JobStop))
423 L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
424 L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
425 L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
426 L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
427 L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
429 // Access to Go stdlib
430 L.SetGlobal("import", luar.New(L, Import))
432 jobs = make(chan JobFunction, 100)
433 events = make(chan tcell.Event, 100)
434 autosave = make(chan bool)
435 updateterm = make(chan bool)
436 closeterm = make(chan int)
440 for _, t := range tabs {
441 for _, v := range t.views {
442 GlobalPluginCall("onViewOpen", v)
443 GlobalPluginCall("onBufferOpen", v.Buf)
448 messenger.style = defStyle
450 // Here is the event loop which runs in a separate thread
454 events <- screen.PollEvent()
461 time.Sleep(autosaveTime * time.Second)
462 if globalSettings["autosave"].(bool) {
469 // Display everything
472 var event tcell.Event
474 // Check for new events
477 // If a new job has finished while running in the background we should execute the callback
478 f.function(f.output, f.args...)
481 if CurView().Buf.Path != "" {
486 case vnum := <-closeterm:
487 tabs[curTab].views[vnum].CloseTerminal()
488 case event = <-events:
494 switch e := event.(type) {
495 case *tcell.EventResize:
496 for _, t := range tabs {
499 case *tcell.EventMouse:
501 if e.Buttons() == tcell.Button1 {
502 // If the user left clicked we check a couple things
503 _, h := screen.Size()
505 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
506 // If the user clicked in the bottom bar, and there is a message down there
507 // we copy it to the clipboard.
508 // Often error messages are displayed down there so it can be useful to easily
510 clipboard.WriteAll(messenger.message, "primary")
514 if CurView().mouseReleased {
515 // We loop through each view in the current tab and make sure the current view
516 // is the one being clicked in
517 for _, v := range tabs[curTab].views {
518 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
519 tabs[curTab].CurView = v.Num
523 } else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown {
526 for _, v := range tabs[curTab].views {
527 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
528 view = tabs[curTab].views[v.Num]
540 // This function checks the mouse event for the possibility of changing the current tab
541 // If the tab was changed it returns true
542 if TabbarHandleMouseEvent(event) {
547 // Since searching is done in real time, we need to redraw every time
548 // there is a new event in the search bar so we need a special function
549 // to run instead of the standard HandleEvent.
550 HandleSearchEvent(event, CurView())
552 // Send it to the view
553 CurView().HandleEvent(event)
558 case event = <-events: