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
62 events chan tcell.Event
65 // Channels for the terminal emulator
69 // How many redraws have happened
73 // LoadInput determines which files should be loaded into buffers
74 // based on the input stored in flag.Args()
75 func LoadInput() []*Buffer {
76 // There are a number of ways micro should start given its input
78 // 1. If it is given a files in flag.Args(), it should open those
80 // 2. If there is no input file and the input is not a terminal, that means
81 // something is being piped in and the stdin should be opened in an
84 // 3. If there is no input file and the input is a terminal, an empty buffer
91 buffers := make([]*Buffer, 0, len(args))
95 // We go through each file and load it
96 for i := 0; i < len(args); i++ {
99 // Check that the file exists
101 if _, e := os.Stat(filename); e == nil {
102 // If it exists we load it into a buffer
103 input, err = os.Open(filename)
104 stat, _ := input.Stat()
111 TermMessage("Cannot read", filename, "because it is a directory")
115 // If the file didn't exist, input will be empty, and we'll open an empty buffer
117 buffers = append(buffers, NewBuffer(input, FSize(input), filename))
119 buffers = append(buffers, NewBufferFromString("", filename))
122 } else if !isatty.IsTerminal(os.Stdin.Fd()) {
124 // The input is not a terminal, so something is being piped in
125 // and we should read from stdin
126 input, err = ioutil.ReadAll(os.Stdin)
128 TermMessage("Error reading from stdin: ", err)
131 buffers = append(buffers, NewBufferFromString(string(input), filename))
133 // Option 3, just open an empty buffer
134 buffers = append(buffers, NewBufferFromString(string(input), filename))
140 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
141 // If no directory is found, it creates one.
142 func InitConfigDir() {
143 xdgHome := os.Getenv("XDG_CONFIG_HOME")
145 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
146 home, err := homedir.Dir()
148 TermMessage("Error finding your home directory\nCan't load config files")
151 xdgHome = home + "/.config"
153 configDir = xdgHome + "/micro"
155 if len(*flagConfigDir) > 0 {
156 if _, err := os.Stat(*flagConfigDir); os.IsNotExist(err) {
157 TermMessage("Error: " + *flagConfigDir + " does not exist. Defaulting to " + configDir + ".")
159 configDir = *flagConfigDir
164 if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
165 // If the xdgHome doesn't exist we should create it
166 err = os.Mkdir(xdgHome, os.ModePerm)
168 TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
172 if _, err := os.Stat(configDir); os.IsNotExist(err) {
173 // If the micro specific config directory doesn't exist we should create that too
174 err = os.Mkdir(configDir, os.ModePerm)
176 TermMessage("Error creating configuration directory: " + err.Error())
181 // InitScreen creates and initializes the tcell screen
183 // Should we enable true color?
184 truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
186 // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
187 // initializing tcell, but after that, we can set the TERM back to whatever it was
188 oldTerm := os.Getenv("TERM")
190 os.Setenv("TERM", "xterm-truecolor")
195 screen, err = tcell.NewScreen()
198 if err == tcell.ErrTermNotFound {
199 fmt.Println("Micro does not recognize your terminal:", oldTerm)
200 fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
204 if err = screen.Init(); err != nil {
209 // Now we can put the TERM back to what it was before
211 os.Setenv("TERM", oldTerm)
214 if GetGlobalOption("mouse").(bool) {
218 // screen.SetStyle(defStyle)
221 // RedrawAll redraws everything -- all the views and the messenger
225 w, h := screen.Size()
226 for x := 0; x < w; x++ {
227 for y := 0; y < h; y++ {
228 screen.SetContent(x, y, ' ', nil, defStyle)
232 for _, v := range tabs[curTab].views {
237 if globalSettings["keymenu"].(bool) {
242 if numRedraw%50 == 0 {
249 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
252 // Build a list of available Extensions (Syntax, Colorscheme etc.)
255 // Load the user's settings
263 for _, tab := range tabs {
264 for _, v := range tab.views {
270 // Command line flags
271 var flagVersion = flag.Bool("version", false, "Show the version number and information")
272 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
273 var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
274 var flagOptions = flag.Bool("options", false, "Show all option help")
277 flag.Usage = func() {
278 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
279 fmt.Println("-config-dir dir")
280 fmt.Println(" \tSpecify a custom location for the configuration directory")
281 fmt.Println("-startpos LINE,COL")
282 fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
283 fmt.Println("-options")
284 fmt.Println(" \tShow all option help")
285 fmt.Println("-version")
286 fmt.Println(" \tShow the version number and information")
288 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")
289 fmt.Println("-option value")
290 fmt.Println(" \tSet `option` to `value` for this session")
291 fmt.Println(" \tFor example: `micro -syntax off file.c`")
292 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
295 optionFlags := make(map[string]*string)
297 for k, v := range DefaultGlobalSettings() {
298 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
304 // If -version was passed
305 fmt.Println("Version:", Version)
306 fmt.Println("Commit hash:", CommitHash)
307 fmt.Println("Compiled on", CompileDate)
312 // If -options was passed
313 for k, v := range DefaultGlobalSettings() {
314 fmt.Printf("-%s value\n", k)
315 fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v)
320 // Start the Lua VM for running plugins
324 // Some encoding stuff in case the user isn't using UTF-8
326 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
328 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
331 // Build a list of available Extensions (Syntax, Colorscheme etc.)
334 // Load the user's settings
343 // This is just so if we have an error, we can exit cleanly and not completely
344 // mess up the terminal being worked in
345 // In other words we need to shut down tcell before the program crashes
347 if err := recover(); err != nil {
349 fmt.Println("Micro encountered an error:", err)
350 // Print the stack trace too
351 fmt.Print(errors.Wrap(err, 2).ErrorStack())
356 // Create a new messenger
357 // This is used for sending the user messages in the bottom of the editor
358 messenger = new(Messenger)
359 messenger.LoadHistory()
361 // Now we load the input
362 buffers := LoadInput()
363 if len(buffers) == 0 {
368 for _, buf := range buffers {
369 // For each buffer we create a new tab and place the view in that tab
370 tab := NewTabFromView(NewView(buf))
371 tab.SetNum(len(tabs))
372 tabs = append(tabs, tab)
373 for _, t := range tabs {
374 for _, v := range t.views {
382 for k, v := range optionFlags {
388 // Load all the plugin stuff
389 // We give plugins access to a bunch of variables here which could be useful to them
390 L.SetGlobal("OS", luar.New(L, runtime.GOOS))
391 L.SetGlobal("tabs", luar.New(L, tabs))
392 L.SetGlobal("curTab", luar.New(L, curTab))
393 L.SetGlobal("messenger", luar.New(L, messenger))
394 L.SetGlobal("GetOption", luar.New(L, GetOption))
395 L.SetGlobal("AddOption", luar.New(L, AddOption))
396 L.SetGlobal("SetOption", luar.New(L, SetOption))
397 L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
398 L.SetGlobal("BindKey", luar.New(L, BindKey))
399 L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
400 L.SetGlobal("CurView", luar.New(L, CurView))
401 L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
402 L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
403 L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
404 L.SetGlobal("ExecCommand", luar.New(L, ExecCommand))
405 L.SetGlobal("RunShellCommand", luar.New(L, RunShellCommand))
406 L.SetGlobal("RunBackgroundShell", luar.New(L, RunBackgroundShell))
407 L.SetGlobal("RunInteractiveShell", luar.New(L, RunInteractiveShell))
408 L.SetGlobal("TermEmuSupported", luar.New(L, TermEmuSupported))
409 L.SetGlobal("RunTermEmulator", luar.New(L, RunTermEmulator))
410 L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
411 L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
412 L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
413 L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
416 L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
419 L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
420 L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
421 L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
422 L.SetGlobal("configDir", luar.New(L, configDir))
423 L.SetGlobal("Reload", luar.New(L, LoadAll))
424 L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
425 L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
427 // Used for asynchronous jobs
428 L.SetGlobal("JobStart", luar.New(L, JobStart))
429 L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
430 L.SetGlobal("JobSend", luar.New(L, JobSend))
431 L.SetGlobal("JobStop", luar.New(L, JobStop))
434 L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
435 L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
436 L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
437 L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
438 L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
440 // Access to Go stdlib
441 L.SetGlobal("import", luar.New(L, Import))
443 jobs = make(chan JobFunction, 100)
444 events = make(chan tcell.Event, 100)
445 autosave = make(chan bool)
446 updateterm = make(chan bool)
447 closeterm = make(chan int)
451 for _, t := range tabs {
452 for _, v := range t.views {
453 GlobalPluginCall("onViewOpen", v)
454 GlobalPluginCall("onBufferOpen", v.Buf)
459 messenger.style = defStyle
461 // Here is the event loop which runs in a separate thread
465 events <- screen.PollEvent()
472 time.Sleep(autosaveTime * time.Second)
473 if globalSettings["autosave"].(bool) {
480 // Display everything
483 var event tcell.Event
485 // Check for new events
488 // If a new job has finished while running in the background we should execute the callback
489 f.function(f.output, f.args...)
492 if CurView().Buf.Path != "" {
497 case vnum := <-closeterm:
498 tabs[curTab].views[vnum].CloseTerminal()
499 case event = <-events:
505 switch e := event.(type) {
506 case *tcell.EventResize:
507 for _, t := range tabs {
510 case *tcell.EventMouse:
512 if e.Buttons() == tcell.Button1 {
513 // If the user left clicked we check a couple things
514 _, h := screen.Size()
516 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
517 // If the user clicked in the bottom bar, and there is a message down there
518 // we copy it to the clipboard.
519 // Often error messages are displayed down there so it can be useful to easily
521 clipboard.WriteAll(messenger.message, "primary")
525 if CurView().mouseReleased {
526 // We loop through each view in the current tab and make sure the current view
527 // is the one being clicked in
528 for _, v := range tabs[curTab].views {
529 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
530 tabs[curTab].CurView = v.Num
534 } else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown {
537 for _, v := range tabs[curTab].views {
538 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
539 view = tabs[curTab].views[v.Num]
551 // This function checks the mouse event for the possibility of changing the current tab
552 // If the tab was changed it returns true
553 if TabbarHandleMouseEvent(event) {
558 // Since searching is done in real time, we need to redraw every time
559 // there is a new event in the search bar so we need a special function
560 // to run instead of the standard HandleEvent.
561 HandleSearchEvent(event, CurView())
563 // Send it to the view
564 CurView().HandleEvent(event)
569 case event = <-events: