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/tcell"
19 "github.com/zyedidia/tcell/encoding"
20 "layeh.com/gopher-luar"
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
26 autosaveTime = 8 // Number of seconds to wait before autosaving
33 // Object to send messages and prompts to the user
36 // The default highlighting style
37 // This simply defines the default foreground and background colors
40 // Where the user's configuration is
41 // This should be $XDG_CONFIG_HOME/micro
42 // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
45 // Version is the version number or commit hash
46 // These variables should be set by the linker when compiling
47 Version = "0.0.0-unknown"
48 // CommitHash is the commit this version was built on
49 CommitHash = "Unknown"
50 // CompileDate is the date this binary was compiled on
51 CompileDate = "Unknown"
55 // This is the currently open tab
56 // It's just an index to the tab in the tabs array
59 // Channel of jobs running in the background
62 events chan tcell.Event
68 // LoadInput determines which files should be loaded into buffers
69 // based on the input stored in flag.Args()
70 func LoadInput() []*Buffer {
71 // There are a number of ways micro should start given its input
73 // 1. If it is given a files in flag.Args(), it should open those
75 // 2. If there is no input file and the input is not a terminal, that means
76 // something is being piped in and the stdin should be opened in an
79 // 3. If there is no input file and the input is a terminal, an empty buffer
86 buffers := make([]*Buffer, 0, len(args))
90 // We go through each file and load it
91 for i := 0; i < len(args); i++ {
94 // Check that the file exists
96 if _, e := os.Stat(filename); e == nil {
97 // If it exists we load it into a buffer
98 input, err = os.Open(filename)
99 stat, _ := input.Stat()
106 TermMessage("Cannot read", filename, "because it is a directory")
110 // If the file didn't exist, input will be empty, and we'll open an empty buffer
112 buffers = append(buffers, NewBuffer(input, FSize(input), filename))
114 buffers = append(buffers, NewBufferFromString("", filename))
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 // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
182 // initializing tcell, but after that, we can set the TERM back to whatever it was
183 oldTerm := os.Getenv("TERM")
185 os.Setenv("TERM", "xterm-truecolor")
190 screen, err = tcell.NewScreen()
193 if err == tcell.ErrTermNotFound {
194 fmt.Println("Micro does not recognize your terminal:", oldTerm)
195 fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
199 if err = screen.Init(); err != nil {
204 // Now we can put the TERM back to what it was before
206 os.Setenv("TERM", oldTerm)
209 if GetGlobalOption("mouse").(bool) {
213 // screen.SetStyle(defStyle)
216 // RedrawAll redraws everything -- all the views and the messenger
220 w, h := screen.Size()
221 for x := 0; x < w; x++ {
222 for y := 0; y < h; y++ {
223 screen.SetContent(x, y, ' ', nil, defStyle)
227 for _, v := range tabs[curTab].views {
232 if globalSettings["keymenu"].(bool) {
239 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
242 // Build a list of available Extensions (Syntax, Colorscheme etc.)
245 // Load the user's settings
253 for _, tab := range tabs {
254 for _, v := range tab.views {
260 // Command line flags
261 var flagVersion = flag.Bool("version", false, "Show the version number and information")
262 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
263 var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
264 var flagOptions = flag.Bool("options", false, "Show all option help")
267 flag.Usage = func() {
268 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
269 fmt.Println("-config-dir dir")
270 fmt.Println(" \tSpecify a custom location for the configuration directory")
271 fmt.Println("-startpos LINE,COL")
272 fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
273 fmt.Println("-options")
274 fmt.Println(" \tShow all option help")
275 fmt.Println("-version")
276 fmt.Println(" \tShow the version number and information")
278 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")
279 fmt.Println("-option value")
280 fmt.Println(" \tSet `option` to `value` for this session")
281 fmt.Println(" \tFor example: `micro -syntax off file.c`")
282 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
285 optionFlags := make(map[string]*string)
287 for k, v := range DefaultGlobalSettings() {
288 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
294 // If -version was passed
295 fmt.Println("Version:", Version)
296 fmt.Println("Commit hash:", CommitHash)
297 fmt.Println("Compiled on", CompileDate)
302 // If -options was passed
303 for k, v := range DefaultGlobalSettings() {
304 fmt.Printf("-%s value\n", k)
305 fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v)
310 // Start the Lua VM for running plugins
314 // Some encoding stuff in case the user isn't using UTF-8
316 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
318 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
321 // Build a list of available Extensions (Syntax, Colorscheme etc.)
324 // Load the user's settings
333 // This is just so if we have an error, we can exit cleanly and not completely
334 // mess up the terminal being worked in
335 // In other words we need to shut down tcell before the program crashes
337 if err := recover(); err != nil {
339 fmt.Println("Micro encountered an error:", err)
340 // Print the stack trace too
341 fmt.Print(errors.Wrap(err, 2).ErrorStack())
346 // Create a new messenger
347 // This is used for sending the user messages in the bottom of the editor
348 messenger = new(Messenger)
349 messenger.LoadHistory()
351 // Now we load the input
352 buffers := LoadInput()
353 if len(buffers) == 0 {
358 for _, buf := range buffers {
359 // For each buffer we create a new tab and place the view in that tab
360 tab := NewTabFromView(NewView(buf))
361 tab.SetNum(len(tabs))
362 tabs = append(tabs, tab)
363 for _, t := range tabs {
364 for _, v := range t.views {
372 for k, v := range optionFlags {
378 // Load all the plugin stuff
379 // We give plugins access to a bunch of variables here which could be useful to them
380 L.SetGlobal("OS", luar.New(L, runtime.GOOS))
381 L.SetGlobal("tabs", luar.New(L, tabs))
382 L.SetGlobal("curTab", luar.New(L, curTab))
383 L.SetGlobal("messenger", luar.New(L, messenger))
384 L.SetGlobal("GetOption", luar.New(L, GetOption))
385 L.SetGlobal("AddOption", luar.New(L, AddOption))
386 L.SetGlobal("SetOption", luar.New(L, SetOption))
387 L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
388 L.SetGlobal("BindKey", luar.New(L, BindKey))
389 L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
390 L.SetGlobal("CurView", luar.New(L, CurView))
391 L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
392 L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
393 L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
394 L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
395 L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
396 L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
397 L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
400 L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
403 L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
404 L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
405 L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
406 L.SetGlobal("configDir", luar.New(L, configDir))
407 L.SetGlobal("Reload", luar.New(L, LoadAll))
408 L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
409 L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
411 // Used for asynchronous jobs
412 L.SetGlobal("JobStart", luar.New(L, JobStart))
413 L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
414 L.SetGlobal("JobSend", luar.New(L, JobSend))
415 L.SetGlobal("JobStop", luar.New(L, JobStop))
418 L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
419 L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
420 L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
421 L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
422 L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
424 // Access to Go stdlib
425 L.SetGlobal("import", luar.New(L, Import))
427 jobs = make(chan JobFunction, 100)
428 events = make(chan tcell.Event, 100)
429 autosave = make(chan bool)
430 updateterm = make(chan bool)
431 closeterm = make(chan int)
435 for _, t := range tabs {
436 for _, v := range t.views {
437 for pl := range loadedPlugins {
438 _, err := Call(pl+".onViewOpen", v)
439 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
449 // Here is the event loop which runs in a separate thread
453 events <- screen.PollEvent()
460 time.Sleep(autosaveTime * time.Second)
461 if globalSettings["autosave"].(bool) {
468 // Display everything
471 var event tcell.Event
473 // Check for new events
476 // If a new job has finished while running in the background we should execute the callback
477 f.function(f.output, f.args...)
480 if CurView().Buf.Path != "" {
485 case vnum := <-closeterm:
486 tabs[curTab].views[vnum].CloseTerminal()
487 case event = <-events:
493 switch e := event.(type) {
494 case *tcell.EventResize:
495 for _, t := range tabs {
498 case *tcell.EventMouse:
500 if e.Buttons() == tcell.Button1 {
501 // If the user left clicked we check a couple things
502 _, h := screen.Size()
504 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
505 // If the user clicked in the bottom bar, and there is a message down there
506 // we copy it to the clipboard.
507 // Often error messages are displayed down there so it can be useful to easily
509 clipboard.WriteAll(messenger.message, "primary")
513 if CurView().mouseReleased {
514 // We loop through each view in the current tab and make sure the current view
515 // is the one being clicked in
516 for _, v := range tabs[curTab].views {
517 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
518 tabs[curTab].CurView = v.Num
522 } else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown {
525 for _, v := range tabs[curTab].views {
526 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
527 view = tabs[curTab].views[v.Num]
539 // This function checks the mouse event for the possibility of changing the current tab
540 // If the tab was changed it returns true
541 if TabbarHandleMouseEvent(event) {
546 // Since searching is done in real time, we need to redraw every time
547 // there is a new event in the search bar so we need a special function
548 // to run instead of the standard HandleEvent.
549 HandleSearchEvent(event, CurView())
551 // Send it to the view
552 CurView().HandleEvent(event)
557 case event = <-events: