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 synLinesUp = 75 // How many lines up to look to do syntax highlighting
25 synLinesDown = 75 // How many lines down to look to do syntax highlighting
26 doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
27 undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
28 autosaveTime = 8 // Number of seconds to wait before autosaving
35 // Object to send messages and prompts to the user
38 // The default highlighting style
39 // This simply defines the default foreground and background colors
42 // Where the user's configuration is
43 // This should be $XDG_CONFIG_HOME/micro
44 // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
47 // Version is the version number or commit hash
48 // These variables should be set by the linker when compiling
49 Version = "0.0.0-unknown"
50 CommitHash = "Unknown"
51 CompileDate = "Unknown"
54 // This is the VM that runs the plugins
59 // This is the currently open tab
60 // It's just an index to the tab in the tabs array
63 // Channel of jobs running in the background
66 events chan tcell.Event
70 // LoadInput determines which files should be loaded into buffers
71 // based on the input stored in flag.Args()
72 func LoadInput() []*Buffer {
73 // There are a number of ways micro should start given its input
75 // 1. If it is given a files in flag.Args(), it should open those
77 // 2. If there is no input file and the input is not a terminal, that means
78 // something is being piped in and the stdin should be opened in an
81 // 3. If there is no input file and the input is a terminal, an empty buffer
89 if len(flag.Args()) > 0 {
91 // We go through each file and load it
92 for i := 0; i < len(flag.Args()); i++ {
93 filename = flag.Args()[i]
95 // Check that the file exists
97 if _, e := os.Stat(filename); e == nil {
98 // If it exists we load it into a buffer
99 input, err = os.Open(filename)
100 stat, _ := input.Stat()
107 TermMessage("Cannot read", filename, "because it is a directory")
111 // If the file didn't exist, input will be empty, and we'll open an empty buffer
113 buffers = append(buffers, NewBuffer(input, filename))
115 buffers = append(buffers, NewBuffer(strings.NewReader(""), filename))
118 } else if !isatty.IsTerminal(os.Stdin.Fd()) {
120 // The input is not a terminal, so something is being piped in
121 // and we should read from stdin
122 input, err = ioutil.ReadAll(os.Stdin)
124 TermMessage("Error reading from stdin: ", err)
127 buffers = append(buffers, NewBuffer(strings.NewReader(string(input)), filename))
129 // Option 3, just open an empty buffer
130 buffers = append(buffers, NewBuffer(strings.NewReader(string(input)), filename))
136 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
137 // If no directory is found, it creates one.
138 func InitConfigDir() {
139 xdgHome := os.Getenv("XDG_CONFIG_HOME")
141 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
142 home, err := homedir.Dir()
144 TermMessage("Error finding your home directory\nCan't load config files")
147 xdgHome = home + "/.config"
149 configDir = xdgHome + "/micro"
151 if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
152 // If the xdgHome doesn't exist we should create it
153 err = os.Mkdir(xdgHome, os.ModePerm)
155 TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
159 if _, err := os.Stat(configDir); os.IsNotExist(err) {
160 // If the micro specific config directory doesn't exist we should create that too
161 err = os.Mkdir(configDir, os.ModePerm)
163 TermMessage("Error creating configuration directory: " + err.Error())
168 // InitScreen creates and initializes the tcell screen
170 // Should we enable true color?
171 truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
173 // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
174 // initializing tcell, but after that, we can set the TERM back to whatever it was
175 oldTerm := os.Getenv("TERM")
177 os.Setenv("TERM", "xterm-truecolor")
182 screen, err = tcell.NewScreen()
187 if err = screen.Init(); err != nil {
192 // Now we can put the TERM back to what it was before
194 os.Setenv("TERM", oldTerm)
197 screen.SetStyle(defStyle)
201 // RedrawAll redraws everything -- all the views and the messenger
205 w, h := screen.Size()
206 for x := 0; x < w; x++ {
207 for y := 0; y < h; y++ {
208 screen.SetContent(x, y, ' ', nil, defStyle)
212 for _, v := range tabs[curTab].views {
221 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
224 // Build a list of available Extensions (Syntax, Colorscheme etc.)
227 // Load the user's settings
235 for _, tab := range tabs {
236 for _, v := range tab.views {
242 // Passing -version as a flag will have micro print out the version number
243 var flagVersion = flag.Bool("version", false, "Show the version number and information")
244 var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
247 flag.Usage = func() {
248 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
249 fmt.Print("Micro's options can be set via command line arguments for quick adjustments. For real configuration, please use the bindings.json file (see 'help options').\n\n")
253 optionFlags := make(map[string]*string)
255 for k, v := range DefaultGlobalSettings() {
256 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
262 // If -version was passed
263 fmt.Println("Version:", Version)
264 fmt.Println("Commit hash:", CommitHash)
265 fmt.Println("Compiled on", CompileDate)
269 // Start the Lua VM for running plugins
273 // Some encoding stuff in case the user isn't using UTF-8
275 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
277 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
280 // Build a list of available Extensions (Syntax, Colorscheme etc.)
283 // Load the user's settings
292 // This is just so if we have an error, we can exit cleanly and not completely
293 // mess up the terminal being worked in
294 // In other words we need to shut down tcell before the program crashes
296 if err := recover(); err != nil {
298 fmt.Println("Micro encountered an error:", err)
299 // Print the stack trace too
300 fmt.Print(errors.Wrap(err, 2).ErrorStack())
305 // Create a new messenger
306 // This is used for sending the user messages in the bottom of the editor
307 messenger = new(Messenger)
308 messenger.history = make(map[string][]string)
310 // Now we load the input
311 buffers := LoadInput()
312 if len(buffers) == 0 {
316 for _, buf := range buffers {
317 // For each buffer we create a new tab and place the view in that tab
318 tab := NewTabFromView(NewView(buf))
319 tab.SetNum(len(tabs))
320 tabs = append(tabs, tab)
321 for _, t := range tabs {
322 for _, v := range t.views {
330 for k, v := range optionFlags {
336 // Load all the plugin stuff
337 // We give plugins access to a bunch of variables here which could be useful to them
338 L.SetGlobal("OS", luar.New(L, runtime.GOOS))
339 L.SetGlobal("tabs", luar.New(L, tabs))
340 L.SetGlobal("curTab", luar.New(L, curTab))
341 L.SetGlobal("messenger", luar.New(L, messenger))
342 L.SetGlobal("GetOption", luar.New(L, GetOption))
343 L.SetGlobal("AddOption", luar.New(L, AddOption))
344 L.SetGlobal("SetOption", luar.New(L, SetOption))
345 L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
346 L.SetGlobal("BindKey", luar.New(L, BindKey))
347 L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
348 L.SetGlobal("CurView", luar.New(L, CurView))
349 L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
350 L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
351 L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
352 L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
353 L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
354 L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
355 L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
358 L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
361 L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
362 L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
363 L.SetGlobal("configDir", luar.New(L, configDir))
364 L.SetGlobal("Reload", luar.New(L, LoadAll))
365 L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
366 L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
368 // Used for asynchronous jobs
369 L.SetGlobal("JobStart", luar.New(L, JobStart))
370 L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
371 L.SetGlobal("JobSend", luar.New(L, JobSend))
372 L.SetGlobal("JobStop", luar.New(L, JobStop))
375 L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
376 L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
377 L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
378 L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
379 L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
381 jobs = make(chan JobFunction, 100)
382 events = make(chan tcell.Event, 100)
383 autosave = make(chan bool)
387 // Load the syntax files, including the colorscheme
390 for _, t := range tabs {
391 for _, v := range t.views {
393 for pl := range loadedPlugins {
394 _, err := Call(pl+".onViewOpen", v)
395 if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
403 // Here is the event loop which runs in a separate thread
406 events <- screen.PollEvent()
412 time.Sleep(autosaveTime * time.Second)
413 if globalSettings["autosave"].(bool) {
420 // Display everything
423 var event tcell.Event
425 // Check for new events
428 // If a new job has finished while running in the background we should execute the callback
429 f.function(f.output, f.args...)
433 case event = <-events:
437 switch e := event.(type) {
438 case *tcell.EventResize:
439 for _, t := range tabs {
442 case *tcell.EventMouse:
443 if e.Buttons() == tcell.Button1 {
444 // If the user left clicked we check a couple things
445 _, h := screen.Size()
447 if y == h-1 && messenger.message != "" && globalSettings["infobar"].(bool) {
448 // If the user clicked in the bottom bar, and there is a message down there
449 // we copy it to the clipboard.
450 // Often error messages are displayed down there so it can be useful to easily
452 clipboard.WriteAll(messenger.message, "primary")
456 if CurView().mouseReleased {
457 // We loop through each view in the current tab and make sure the current view
458 // is the one being clicked in
459 for _, v := range tabs[curTab].views {
460 if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
461 tabs[curTab].CurView = v.Num
468 // This function checks the mouse event for the possibility of changing the current tab
469 // If the tab was changed it returns true
470 if TabbarHandleMouseEvent(event) {
475 // Since searching is done in real time, we need to redraw every time
476 // there is a new event in the search bar so we need a special function
477 // to run instead of the standard HandleEvent.
478 HandleSearchEvent(event, CurView())
480 // Send it to the view
481 CurView().HandleEvent(event)
485 case event = <-events: