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 = "Unknown"
49 CompileDate = "Unknown"
53 // This is the currently open tab
54 // It's just an index to the tab in the tabs array
57 // Channel of jobs running in the background
60 events chan tcell.Event
64 // LoadInput determines which files should be loaded into buffers
65 // based on the input stored in flag.Args()
66 func LoadInput() []*Buffer {
67 // There are a number of ways micro should start given its input
69 // 1. If it is given a files in flag.Args(), it should open those
71 // 2. If there is no input file and the input is not a terminal, that means
72 // something is being piped in and the stdin should be opened in an
75 // 3. If there is no input file and the input is a terminal, an empty buffer
82 buffers := make([]*Buffer, 0, len(args))
86 // We go through each file and load it
87 for i := 0; i < len(args); i++ {
90 // 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 = os.Open(filename)
95 stat, _ := input.Stat()
102 TermMessage("Cannot read", filename, "because it is a directory")
106 // If the file didn't exist, input will be empty, and we'll open an empty buffer
108 buffers = append(buffers, NewBuffer(input, FSize(input), filename))
110 buffers = append(buffers, NewBufferFromString("", filename))
113 } else if !isatty.IsTerminal(os.Stdin.Fd()) {
115 // The input is not a terminal, so something is being piped in
116 // and we should read from stdin
117 input, err = ioutil.ReadAll(os.Stdin)
119 TermMessage("Error reading from stdin: ", err)
122 buffers = append(buffers, NewBufferFromString(string(input), filename))
124 // Option 3, just open an empty buffer
125 buffers = append(buffers, NewBufferFromString(string(input), filename))
131 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
132 // If no directory is found, it creates one.
133 func InitConfigDir() {
134 xdgHome := os.Getenv("XDG_CONFIG_HOME")
136 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
137 home, err := homedir.Dir()
139 TermMessage("Error finding your home directory\nCan't load config files")
142 xdgHome = home + "/.config"
144 configDir = xdgHome + "/micro"
146 if len(*flagConfigDir) > 0 {
147 if _, err := os.Stat(*flagConfigDir); os.IsNotExist(err) {
148 TermMessage("Error: " + *flagConfigDir + " does not exist. Defaulting to " + configDir + ".")
150 configDir = *flagConfigDir
155 if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
156 // If the xdgHome doesn't exist we should create it
157 err = os.Mkdir(xdgHome, os.ModePerm)
159 TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
163 if _, err := os.Stat(configDir); os.IsNotExist(err) {
164 // If the micro specific config directory doesn't exist we should create that too
165 err = os.Mkdir(configDir, os.ModePerm)
167 TermMessage("Error creating configuration directory: " + err.Error())
172 // InitScreen creates and initializes the tcell screen
174 // Should we enable true color?
175 truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
177 // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
178 // initializing tcell, but after that, we can set the TERM back to whatever it was
179 oldTerm := os.Getenv("TERM")
181 os.Setenv("TERM", "xterm-truecolor")
186 screen, err = tcell.NewScreen()
189 if err == tcell.ErrTermNotFound {
190 fmt.Println("Micro does not recognize your terminal:", oldTerm)
191 fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
195 if err = screen.Init(); err != nil {
200 // Now we can put the TERM back to what it was before
202 os.Setenv("TERM", oldTerm)
205 if GetGlobalOption("mouse").(bool) {
209 screen.SetStyle(defStyle)
212 // RedrawAll redraws everything -- all the views and the messenger
216 w, h := screen.Size()
217 for x := 0; x < w; x++ {
218 for y := 0; y < h; y++ {
219 screen.SetContent(x, y, ' ', nil, defStyle)
223 for _, v := range tabs[curTab].views {
228 if globalSettings["keymenu"].(bool) {
235 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
238 // Build a list of available Extensions (Syntax, Colorscheme etc.)
241 // Load the user's settings
249 for _, tab := range tabs {
250 for _, v := range tab.views {
256 // Passing -version as a flag will have micro print out the version number
257 var flagVersion = flag.Bool("version", false, "Show the version number and information")
258 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
259 var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
260 var flagOptions = flag.Bool("options", false, "Show all option help")
263 flag.Usage = func() {
264 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
265 fmt.Println("-config-dir dir")
266 fmt.Println(" \tSpecify a custom location for the configuration directory")
267 fmt.Println("-startpos LINE,COL")
268 fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
269 fmt.Println("-options")
270 fmt.Println(" \tShow all option help")
271 fmt.Println("-version")
272 fmt.Println(" \tShow the version number and information")
274 fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the bindings.json\nfile (see 'help options').\n\n")
275 fmt.Println("-option value")
276 fmt.Println(" \tSet `option` to `value` for this session")
277 fmt.Println(" \tFor example: `micro -syntax off file.c`")
278 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
281 optionFlags := make(map[string]*string)
283 for k, v := range DefaultGlobalSettings() {
284 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
290 // If -version was passed
291 fmt.Println("Version:", Version)
292 fmt.Println("Commit hash:", CommitHash)
293 fmt.Println("Compiled on", CompileDate)
298 // If -options was passed
299 for k, v := range DefaultGlobalSettings() {
300 fmt.Printf("-%s value\n", k)
301 fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v)
306 // Start the Lua VM for running plugins
310 // Some encoding stuff in case the user isn't using UTF-8
312 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
314 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
317 // Build a list of available Extensions (Syntax, Colorscheme etc.)
320 // Load the user's settings
329 // This is just so if we have an error, we can exit cleanly and not completely
330 // mess up the terminal being worked in
331 // In other words we need to shut down tcell before the program crashes
333 if err := recover(); err != nil {
335 fmt.Println("Micro encountered an error:", err)
336 // Print the stack trace too
337 fmt.Print(errors.Wrap(err, 2).ErrorStack())
342 // Create a new messenger
343 // This is used for sending the user messages in the bottom of the editor
344 messenger = new(Messenger)
345 messenger.LoadHistory()
347 // Now we load the input
348 buffers := LoadInput()
349 if len(buffers) == 0 {
354 for _, buf := range buffers {
355 // For each buffer we create a new tab and place the view in that tab
356 tab := NewTabFromView(NewView(buf))
357 tab.SetNum(len(tabs))
358 tabs = append(tabs, tab)
359 for _, t := range tabs {
360 for _, v := range t.views {
368 for k, v := range optionFlags {
374 // Load all the plugin stuff
375 // We give plugins access to a bunch of variables here which could be useful to them
376 L.SetGlobal("OS", luar.New(L, runtime.GOOS))
377 L.SetGlobal("tabs", luar.New(L, tabs))
378 L.SetGlobal("curTab", luar.New(L, curTab))
379 L.SetGlobal("messenger", luar.New(L, messenger))
380 L.SetGlobal("GetOption", luar.New(L, GetOption))
381 L.SetGlobal("AddOption", luar.New(L, AddOption))
382 L.SetGlobal("SetOption", luar.New(L, SetOption))
383 L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
384 L.SetGlobal("BindKey", luar.New(L, BindKey))
385 L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
386 L.SetGlobal("CurView", luar.New(L, CurView))
387 L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
388 L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
389 L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
390 L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
391 L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
392 L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
393 L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
396 L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
399 L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
400 L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
401 L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
402 L.SetGlobal("configDir", luar.New(L, configDir))
403 L.SetGlobal("Reload", luar.New(L, LoadAll))
404 L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
405 L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
407 // Used for asynchronous jobs
408 L.SetGlobal("JobStart", luar.New(L, JobStart))
409 L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
410 L.SetGlobal("JobSend", luar.New(L, JobSend))
411 L.SetGlobal("JobStop", luar.New(L, JobStop))
414 L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
415 L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
416 L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
417 L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
418 L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
420 // Access to Go stdlib
421 L.SetGlobal("import", luar.New(L, Import))
423 jobs = make(chan JobFunction, 100)
424 events = make(chan tcell.Event, 100)
425 autosave = make(chan bool)
429 for _, t := range tabs {
430 for _, v := range t.views {
431 for pl := range loadedPlugins {
432 _, err := Call(pl+".onViewOpen", v)
433 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
443 // Here is the event loop which runs in a separate thread
447 events <- screen.PollEvent()
454 time.Sleep(autosaveTime * time.Second)
455 if globalSettings["autosave"].(bool) {
462 // Display everything
465 var event tcell.Event
467 // Check for new events
470 // If a new job has finished while running in the background we should execute the callback
471 f.function(f.output, f.args...)
475 case event = <-events:
479 switch e := event.(type) {
480 case *tcell.EventResize:
481 for _, t := range tabs {
484 case *tcell.EventMouse:
486 if e.Buttons() == tcell.Button1 {
487 // If the user left clicked we check a couple things
488 _, h := screen.Size()
490 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
491 // If the user clicked in the bottom bar, and there is a message down there
492 // we copy it to the clipboard.
493 // Often error messages are displayed down there so it can be useful to easily
495 clipboard.WriteAll(messenger.message, "primary")
499 if CurView().mouseReleased {
500 // We loop through each view in the current tab and make sure the current view
501 // is the one being clicked in
502 for _, v := range tabs[curTab].views {
503 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
504 tabs[curTab].CurView = v.Num
512 // This function checks the mouse event for the possibility of changing the current tab
513 // If the tab was changed it returns true
514 if TabbarHandleMouseEvent(event) {
519 // Since searching is done in real time, we need to redraw every time
520 // there is a new event in the search bar so we need a special function
521 // to run instead of the standard HandleEvent.
522 HandleSearchEvent(event, CurView())
524 // Send it to the view
525 CurView().HandleEvent(event)
529 case event = <-events: