13 "github.com/go-errors/errors"
14 "github.com/mattn/go-isatty"
15 "github.com/mitchellh/go-homedir"
16 "github.com/yuin/gopher-lua"
17 "github.com/zyedidia/clipboard"
18 "github.com/zyedidia/micro/cmd/micro/terminfo"
19 "github.com/zyedidia/tcell"
20 "github.com/zyedidia/tcell/encoding"
21 "layeh.com/gopher-luar"
25 doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
26 undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
27 autosaveTime = 8 // Number of seconds to wait before autosaving
34 // Object to send messages and prompts to the user
37 // The default highlighting style
38 // This simply defines the default foreground and background colors
41 // Where the user's configuration is
42 // This should be $XDG_CONFIG_HOME/micro
43 // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
46 // Version is the version number or commit hash
47 // These variables should be set by the linker when compiling
48 Version = "0.0.0-unknown"
49 // CommitHash is the commit this version was built on
50 CommitHash = "Unknown"
51 // CompileDate is the date this binary was compiled on
52 CompileDate = "Unknown"
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
64 events chan tcell.Event
67 // Channels for the terminal emulator
71 // How many redraws have happened
75 // LoadInput determines which files should be loaded into buffers
76 // based on the input stored in flag.Args()
77 func LoadInput() []*Buffer {
78 // There are a number of ways micro should start given its input
80 // 1. If it is given a files in flag.Args(), it should open those
82 // 2. If there is no input file and the input is not a terminal, that means
83 // something is being piped in and the stdin should be opened in an
86 // 3. If there is no input file and the input is a terminal, an empty buffer
93 buffers := make([]*Buffer, 0, len(args))
97 // We go through each file and load it
98 for i := 0; i < len(args); i++ {
99 if strings.HasPrefix(args[i], "+") {
100 if strings.Contains(args[i], ":") {
101 split := strings.Split(args[i], ":")
102 *flagStartPos = split[0][1:] + "," + split[1]
104 *flagStartPos = args[i][1:] + ",0"
109 buf, err := NewBufferFromFile(args[i])
114 // If the file didn't exist, input will be empty, and we'll open an empty buffer
115 buffers = append(buffers, buf)
117 } else if !isatty.IsTerminal(os.Stdin.Fd()) {
119 // The input is not a terminal, so something is being piped in
120 // and we should read from stdin
121 input, err = ioutil.ReadAll(os.Stdin)
123 TermMessage("Error reading from stdin: ", err)
126 buffers = append(buffers, NewBufferFromString(string(input), filename))
128 // Option 3, just open an empty buffer
129 buffers = append(buffers, NewBufferFromString(string(input), filename))
135 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
136 // If no directory is found, it creates one.
137 func InitConfigDir() {
138 xdgHome := os.Getenv("XDG_CONFIG_HOME")
140 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
141 home, err := homedir.Dir()
143 TermMessage("Error finding your home directory\nCan't load config files")
146 xdgHome = home + "/.config"
148 configDir = xdgHome + "/micro"
150 if len(*flagConfigDir) > 0 {
151 if _, err := os.Stat(*flagConfigDir); os.IsNotExist(err) {
152 TermMessage("Error: " + *flagConfigDir + " does not exist. Defaulting to " + configDir + ".")
154 configDir = *flagConfigDir
159 if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
160 // If the xdgHome doesn't exist we should create it
161 err = os.Mkdir(xdgHome, os.ModePerm)
163 TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
167 if _, err := os.Stat(configDir); os.IsNotExist(err) {
168 // If the micro specific config directory doesn't exist we should create that too
169 err = os.Mkdir(configDir, os.ModePerm)
171 TermMessage("Error creating configuration directory: " + err.Error())
176 // InitScreen creates and initializes the tcell screen
178 // Should we enable true color?
179 truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
181 tcelldb := os.Getenv("TCELLDB")
182 os.Setenv("TCELLDB", configDir+"/.tcelldb")
184 // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
185 // initializing tcell, but after that, we can set the TERM back to whatever it was
186 oldTerm := os.Getenv("TERM")
188 os.Setenv("TERM", "xterm-truecolor")
193 screen, err = tcell.NewScreen()
195 if err == tcell.ErrTermNotFound {
196 terminfo.WriteDB(configDir + "/.tcelldb")
197 screen, err = tcell.NewScreen()
200 fmt.Println("Fatal: Micro could not initialize a screen.")
205 fmt.Println("Fatal: Micro could not initialize a screen.")
209 if err = screen.Init(); err != nil {
214 // Now we can put the TERM back to what it was before
216 os.Setenv("TERM", oldTerm)
219 if GetGlobalOption("mouse").(bool) {
223 os.Setenv("TCELLDB", tcelldb)
225 // screen.SetStyle(defStyle)
228 // RedrawAll redraws everything -- all the views and the messenger
232 w, h := screen.Size()
233 for x := 0; x < w; x++ {
234 for y := 0; y < h; y++ {
235 screen.SetContent(x, y, ' ', nil, defStyle)
239 for _, v := range tabs[curTab].Views {
244 if globalSettings["keymenu"].(bool) {
249 if numRedraw%50 == 0 {
256 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
259 // Build a list of available Extensions (Syntax, Colorscheme etc.)
262 // Load the user's settings
270 for _, tab := range tabs {
271 for _, v := range tab.Views {
277 // Command line flags
278 var flagVersion = flag.Bool("version", false, "Show the version number and information")
279 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
280 var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
281 var flagOptions = flag.Bool("options", false, "Show all option help")
284 flag.Usage = func() {
285 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
286 fmt.Println("-config-dir dir")
287 fmt.Println(" \tSpecify a custom location for the configuration directory")
288 fmt.Println("-startpos LINE,COL")
289 fmt.Println("+LINE:COL")
290 fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
291 fmt.Println(" \tThis can also be done by opening file:LINE:COL")
292 fmt.Println("-options")
293 fmt.Println(" \tShow all option help")
294 fmt.Println("-version")
295 fmt.Println(" \tShow the version number and information")
297 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")
298 fmt.Println("-option value")
299 fmt.Println(" \tSet `option` to `value` for this session")
300 fmt.Println(" \tFor example: `micro -syntax off file.c`")
301 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
304 optionFlags := make(map[string]*string)
306 for k, v := range DefaultGlobalSettings() {
307 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
313 // If -version was passed
314 fmt.Println("Version:", Version)
315 fmt.Println("Commit hash:", CommitHash)
316 fmt.Println("Compiled on", CompileDate)
321 // If -options was passed
322 for k, v := range DefaultGlobalSettings() {
323 fmt.Printf("-%s value\n", k)
324 fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v)
329 // Start the Lua VM for running plugins
333 // Some encoding stuff in case the user isn't using UTF-8
335 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
337 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
340 // Build a list of available Extensions (Syntax, Colorscheme etc.)
343 // Load the user's settings
352 // This is just so if we have an error, we can exit cleanly and not completely
353 // mess up the terminal being worked in
354 // In other words we need to shut down tcell before the program crashes
356 if err := recover(); err != nil {
358 fmt.Println("Micro encountered an error:", err)
359 // Print the stack trace too
360 fmt.Print(errors.Wrap(err, 2).ErrorStack())
365 // Create a new messenger
366 // This is used for sending the user messages in the bottom of the editor
367 messenger = new(Messenger)
368 messenger.LoadHistory()
370 // Now we load the input
371 buffers := LoadInput()
372 if len(buffers) == 0 {
377 for _, buf := range buffers {
378 // For each buffer we create a new tab and place the view in that tab
379 tab := NewTabFromView(NewView(buf))
380 tab.SetNum(len(tabs))
381 tabs = append(tabs, tab)
382 for _, t := range tabs {
383 for _, v := range t.Views {
391 for k, v := range optionFlags {
397 // Load all the plugin stuff
398 // We give plugins access to a bunch of variables here which could be useful to them
399 L.SetGlobal("OS", luar.New(L, runtime.GOOS))
400 L.SetGlobal("tabs", luar.New(L, tabs))
401 L.SetGlobal("GetTabs", luar.New(L, func() []*Tab {
404 L.SetGlobal("curTab", luar.New(L, curTab))
405 L.SetGlobal("messenger", luar.New(L, messenger))
406 L.SetGlobal("GetOption", luar.New(L, GetOption))
407 L.SetGlobal("AddOption", luar.New(L, AddOption))
408 L.SetGlobal("SetOption", luar.New(L, SetOption))
409 L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
410 L.SetGlobal("BindKey", luar.New(L, BindKey))
411 L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
412 L.SetGlobal("CurView", luar.New(L, CurView))
413 L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
414 L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
415 L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
416 L.SetGlobal("ExecCommand", luar.New(L, ExecCommand))
417 L.SetGlobal("RunShellCommand", luar.New(L, RunShellCommand))
418 L.SetGlobal("RunBackgroundShell", luar.New(L, RunBackgroundShell))
419 L.SetGlobal("RunInteractiveShell", luar.New(L, RunInteractiveShell))
420 L.SetGlobal("TermEmuSupported", luar.New(L, TermEmuSupported))
421 L.SetGlobal("RunTermEmulator", luar.New(L, RunTermEmulator))
422 L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
423 L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
424 L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
425 L.SetGlobal("NewBufferFromFile", luar.New(L, NewBufferFromFile))
426 L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
429 L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
432 L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
433 L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
434 L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
435 L.SetGlobal("configDir", luar.New(L, configDir))
436 L.SetGlobal("Reload", luar.New(L, LoadAll))
437 L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
438 L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
440 // Used for asynchronous jobs
441 L.SetGlobal("JobStart", luar.New(L, JobStart))
442 L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
443 L.SetGlobal("JobSend", luar.New(L, JobSend))
444 L.SetGlobal("JobStop", luar.New(L, JobStop))
447 L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
448 L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
449 L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
450 L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
451 L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
453 // Access to Go stdlib
454 L.SetGlobal("import", luar.New(L, Import))
456 jobs = make(chan JobFunction, 100)
457 events = make(chan tcell.Event, 100)
458 autosave = make(chan bool)
459 updateterm = make(chan bool)
460 closeterm = make(chan int)
464 for _, t := range tabs {
465 for _, v := range t.Views {
466 GlobalPluginCall("onViewOpen", v)
467 GlobalPluginCall("onBufferOpen", v.Buf)
472 messenger.style = defStyle
474 // Here is the event loop which runs in a separate thread
478 events <- screen.PollEvent()
485 time.Sleep(autosaveTime * time.Second)
486 if globalSettings["autosave"].(bool) {
493 // Display everything
496 var event tcell.Event
498 // Check for new events
501 // If a new job has finished while running in the background we should execute the callback
502 f.function(f.output, f.args...)
505 if CurView().Buf.Path != "" {
510 case vnum := <-closeterm:
511 tabs[curTab].Views[vnum].CloseTerminal()
512 case event = <-events:
518 switch e := event.(type) {
519 case *tcell.EventResize:
520 for _, t := range tabs {
523 case *tcell.EventMouse:
525 if e.Buttons() == tcell.Button1 {
526 // If the user left clicked we check a couple things
527 _, h := screen.Size()
529 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
530 // If the user clicked in the bottom bar, and there is a message down there
531 // we copy it to the clipboard.
532 // Often error messages are displayed down there so it can be useful to easily
534 clipboard.WriteAll(messenger.message, "primary")
538 if CurView().mouseReleased {
539 // We loop through each view in the current tab and make sure the current view
540 // is the one being clicked in
541 for _, v := range tabs[curTab].Views {
542 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
543 tabs[curTab].CurView = v.Num
547 } else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown {
550 for _, v := range tabs[curTab].Views {
551 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
552 view = tabs[curTab].Views[v.Num]
564 // This function checks the mouse event for the possibility of changing the current tab
565 // If the tab was changed it returns true
566 if TabbarHandleMouseEvent(event) {
571 // Since searching is done in real time, we need to redraw every time
572 // there is a new event in the search bar so we need a special function
573 // to run instead of the standard HandleEvent.
574 HandleSearchEvent(event, CurView())
576 // Send it to the view
577 CurView().HandleEvent(event)
582 case event = <-events: