10 "github.com/atotto/clipboard"
11 "github.com/go-errors/errors"
12 "github.com/layeh/gopher-luar"
13 "github.com/mattn/go-isatty"
14 "github.com/mitchellh/go-homedir"
15 "github.com/yuin/gopher-lua"
16 "github.com/zyedidia/tcell"
17 "github.com/zyedidia/tcell/encoding"
24 // Object to send messages and prompts to the user
27 // The default highlighting style
28 // This simply defines the default foreground and background colors
31 // Where the user's configuration is
32 // This should be $XDG_CONFIG_HOME/micro
33 // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
36 // Version is the version number or commit hash
37 // This should be set by the linker when compiling
41 // This is the VM that runs the plugins
46 // This is the currently open tab
47 // It's just an index to the tab in the tabs array
50 // Channel of jobs running in the background
53 events chan tcell.Event
56 // LoadInput determines which files should be loaded into buffers
57 // based on the input stored in os.Args
58 func LoadInput() []*Buffer {
59 // There are a number of ways micro should start given its input
61 // 1. If it is given a files in os.Args, it should open those
63 // 2. If there is no input file and the input is not a terminal, that means
64 // something is being piped in and the stdin should be opened in an
67 // 3. If there is no input file and the input is a terminal, an empty buffer
77 // We go through each file and load it
78 for i := 1; i < len(os.Args); i++ {
80 // Check that the file exists
81 if _, e := os.Stat(filename); e == nil {
82 // If it exists we load it into a buffer
83 input, err = ioutil.ReadFile(filename)
89 // If the file didn't exist, input will be empty, and we'll open an empty buffer
90 buffers = append(buffers, NewBuffer(input, filename))
92 } else if !isatty.IsTerminal(os.Stdin.Fd()) {
94 // The input is not a terminal, so something is being piped in
95 // and we should read from stdin
96 input, err = ioutil.ReadAll(os.Stdin)
97 buffers = append(buffers, NewBuffer(input, filename))
99 // Option 3, just open an empty buffer
100 buffers = append(buffers, NewBuffer(input, filename))
106 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
107 // If no directory is found, it creates one.
108 func InitConfigDir() {
109 xdgHome := os.Getenv("XDG_CONFIG_HOME")
111 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
112 home, err := homedir.Dir()
114 TermMessage("Error finding your home directory\nCan't load config files")
117 xdgHome = home + "/.config"
119 configDir = xdgHome + "/micro"
121 if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
122 // If the xdgHome doesn't exist we should create it
123 err = os.Mkdir(xdgHome, os.ModePerm)
125 TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
129 if _, err := os.Stat(configDir); os.IsNotExist(err) {
130 // If the micro specific config directory doesn't exist we should create that too
131 err = os.Mkdir(configDir, os.ModePerm)
133 TermMessage("Error creating configuration directory: " + err.Error())
138 // InitScreen creates and initializes the tcell screen
140 // Should we enable true color?
141 truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
143 // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
144 // initializing tcell, but after that, we can set the TERM back to whatever it was
145 oldTerm := os.Getenv("TERM")
147 os.Setenv("TERM", "xterm-truecolor")
152 screen, err = tcell.NewScreen()
157 if err = screen.Init(); err != nil {
162 // Now we can put the TERM back to what it was before
164 os.Setenv("TERM", oldTerm)
167 screen.SetStyle(defStyle)
171 // RedrawAll redraws everything -- all the views and the messenger
176 for _, v := range tabs[curTab].views {
182 // Passing -version as a flag will have micro print out the version number
183 var flagVersion = flag.Bool("version", false, "Show the version number")
188 // If -version was passed
189 fmt.Println("Micro version:", Version)
193 // Start the Lua VM for running plugins
197 // Some encoding stuff in case the user isn't using UTF-8
199 tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
201 // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
204 // Load the user's settings
209 // Load the syntax files, including the colorscheme
212 // Load the help files
218 // This is just so if we have an error, we can exit cleanly and not completely
219 // mess up the terminal being worked in
220 // In other words we need to shut down tcell before the program crashes
222 if err := recover(); err != nil {
224 fmt.Println("Micro encountered an error:", err)
225 // Print the stack trace too
226 fmt.Print(errors.Wrap(err, 2).ErrorStack())
231 // Create a new messenger
232 // This is used for sending the user messages in the bottom of the editor
233 messenger = new(Messenger)
234 messenger.history = make(map[string][]string)
236 // Now we load the input
237 buffers := LoadInput()
238 for _, buf := range buffers {
239 // For each buffer we create a new tab and place the view in that tab
240 tab := NewTabFromView(NewView(buf))
241 tab.SetNum(len(tabs))
242 tabs = append(tabs, tab)
243 for _, t := range tabs {
244 for _, v := range t.views {
245 v.Resize(screen.Size())
250 // Load all the plugin stuff
251 // We give plugins access to a bunch of variables here which could be useful to them
252 L.SetGlobal("OS", luar.New(L, runtime.GOOS))
253 L.SetGlobal("tabs", luar.New(L, tabs))
254 L.SetGlobal("curTab", luar.New(L, curTab))
255 L.SetGlobal("messenger", luar.New(L, messenger))
256 L.SetGlobal("GetOption", luar.New(L, GetOption))
257 L.SetGlobal("AddOption", luar.New(L, AddOption))
258 L.SetGlobal("BindKey", luar.New(L, BindKey))
259 L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
260 L.SetGlobal("CurView", luar.New(L, CurView))
261 L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
263 // Used for asynchronous jobs
264 L.SetGlobal("JobStart", luar.New(L, JobStart))
265 L.SetGlobal("JobSend", luar.New(L, JobSend))
266 L.SetGlobal("JobStop", luar.New(L, JobStop))
270 jobs = make(chan JobFunction, 100)
271 events = make(chan tcell.Event)
273 // Here is the event loop which runs in a separate thread
276 events <- screen.PollEvent()
281 // Display everything
284 var event tcell.Event
286 // Check for new events
289 // If a new job has finished while running in the background we should execute the callback
290 f.function(f.output, f.args...)
292 case event = <-events:
295 switch e := event.(type) {
296 case *tcell.EventMouse:
297 if e.Buttons() == tcell.Button1 {
298 // If the user left clicked we check a couple things
299 _, h := screen.Size()
301 if y == h-1 && messenger.message != "" {
302 // If the user clicked in the bottom bar, and there is a message down there
303 // we copy it to the clipboard.
304 // Often error messages are displayed down there so it can be useful to easily
306 clipboard.WriteAll(messenger.message)
310 // We loop through each view in the current tab and make sure the current view
311 // it the one being clicked in
312 for _, v := range tabs[curTab].views {
313 if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
314 tabs[curTab].curView = v.Num
320 // This function checks the mouse event for the possibility of changing the current tab
321 // If the tab was changed it returns true
322 if TabbarHandleMouseEvent(event) {
327 // Since searching is done in real time, we need to redraw every time
328 // there is a new event in the search bar so we need a special function
329 // to run instead of the standard HandleEvent.
330 HandleSearchEvent(event, CurView())
332 // Send it to the view
333 CurView().HandleEvent(event)