]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
Add support for user-created commands
[micro.git] / cmd / micro / micro.go
1 package main
2
3 import (
4         "flag"
5         "fmt"
6         "io/ioutil"
7         "os"
8         "runtime"
9
10         "github.com/go-errors/errors"
11         "github.com/layeh/gopher-luar"
12         "github.com/mattn/go-isatty"
13         "github.com/mitchellh/go-homedir"
14         "github.com/yuin/gopher-lua"
15         "github.com/zyedidia/tcell"
16         "github.com/zyedidia/tcell/encoding"
17 )
18
19 const (
20         synLinesUp           = 75  // How many lines up to look to do syntax highlighting
21         synLinesDown         = 75  // How many lines down to look to do syntax highlighting
22         doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
23         undoThreshold        = 500 // If two events are less than n milliseconds apart, undo both of them
24 )
25
26 var (
27         // The main screen
28         screen tcell.Screen
29
30         // Object to send messages and prompts to the user
31         messenger *Messenger
32
33         // The default highlighting style
34         // This simply defines the default foreground and background colors
35         defStyle tcell.Style
36
37         // Where the user's configuration is
38         // This should be $XDG_CONFIG_HOME/micro
39         // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
40         configDir string
41
42         // Version is the version number or commit hash
43         // This should be set by the linker
44         Version = "Unknown"
45
46         // L is the lua state
47         // This is the VM that runs the plugins
48         L *lua.LState
49
50         // The list of views
51         views []*View
52         // This is the currently open view
53         // It's just an index to the view in the views array
54         mainView int
55 )
56
57 // LoadInput loads the file input for the editor
58 func LoadInput() (string, []byte, error) {
59         // There are a number of ways micro should start given its input
60
61         // 1. If it is given a file in os.Args, it should open that
62
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
65         // empty buffer
66
67         // 3. If there is no input file and the input is a terminal, an empty buffer
68         // should be opened
69
70         // These are empty by default so if we get to option 3, we can just returns the
71         // default values
72         var filename string
73         var input []byte
74         var err error
75
76         if len(os.Args) > 1 {
77                 // Option 1
78                 filename = os.Args[1]
79                 // Check that the file exists
80                 if _, e := os.Stat(filename); e == nil {
81                         input, err = ioutil.ReadFile(filename)
82                 }
83         } else if !isatty.IsTerminal(os.Stdin.Fd()) {
84                 // Option 2
85                 // The input is not a terminal, so something is being piped in
86                 // and we should read from stdin
87                 input, err = ioutil.ReadAll(os.Stdin)
88         }
89
90         // Option 3, or just return whatever we got
91         return filename, input, err
92 }
93
94 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
95 // If no directory is found, it creates one.
96 func InitConfigDir() {
97         xdgHome := os.Getenv("XDG_CONFIG_HOME")
98         if xdgHome == "" {
99                 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
100                 home, err := homedir.Dir()
101                 if err != nil {
102                         TermMessage("Error finding your home directory\nCan't load config files")
103                         return
104                 }
105                 xdgHome = home + "/.config"
106         }
107         configDir = xdgHome + "/micro"
108
109         if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
110                 // If the xdgHome doesn't exist we should create it
111                 err = os.Mkdir(xdgHome, os.ModePerm)
112                 if err != nil {
113                         TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
114                 }
115         }
116
117         if _, err := os.Stat(configDir); os.IsNotExist(err) {
118                 // If the micro specific config directory doesn't exist we should create that too
119                 err = os.Mkdir(configDir, os.ModePerm)
120                 if err != nil {
121                         TermMessage("Error creating configuration directory: " + err.Error())
122                 }
123         }
124 }
125
126 // InitScreen creates and initializes the tcell screen
127 func InitScreen() {
128         // Should we enable true color?
129         truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
130
131         // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
132         // initializing tcell, but after that, we can set the TERM back to whatever it was
133         oldTerm := os.Getenv("TERM")
134         if truecolor {
135                 os.Setenv("TERM", "xterm-truecolor")
136         }
137
138         // Initilize tcell
139         var err error
140         screen, err = tcell.NewScreen()
141         if err != nil {
142                 fmt.Println(err)
143                 os.Exit(1)
144         }
145         if err = screen.Init(); err != nil {
146                 fmt.Println(err)
147                 os.Exit(1)
148         }
149
150         // Now we can put the TERM back to what it was before
151         if truecolor {
152                 os.Setenv("TERM", oldTerm)
153         }
154
155         // Default style
156         defStyle = tcell.StyleDefault.
157                 Foreground(tcell.ColorDefault).
158                 Background(tcell.ColorDefault)
159
160         // There may be another default style defined in the colorscheme
161         // In that case we should use that one
162         if style, ok := colorscheme["default"]; ok {
163                 defStyle = style
164         }
165
166         screen.SetStyle(defStyle)
167         screen.EnableMouse()
168 }
169
170 // RedrawAll redraws everything -- all the views and the messenger
171 func RedrawAll() {
172         screen.Clear()
173         for _, v := range views {
174                 v.Display()
175         }
176         messenger.Display()
177         screen.Show()
178 }
179
180 var flagVersion = flag.Bool("version", false, "Show version number")
181
182 func main() {
183         flag.Parse()
184         if *flagVersion {
185                 fmt.Println("Micro version:", Version)
186                 os.Exit(0)
187         }
188
189         filename, input, err := LoadInput()
190         if err != nil {
191                 fmt.Println(err)
192                 os.Exit(1)
193         }
194
195         L = lua.NewState()
196         defer L.Close()
197
198         // Some encoding stuff in case the user isn't using UTF-8
199         encoding.Register()
200         tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
201
202         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
203         InitConfigDir()
204         // Load the user's settings
205         InitSettings()
206         InitCommands()
207         InitBindings()
208         // Load the syntax files, including the colorscheme
209         LoadSyntaxFiles()
210         // Load the help files
211         LoadHelp()
212
213         buf := NewBuffer(string(input), filename)
214
215         InitScreen()
216
217         // This is just so if we have an error, we can exit cleanly and not completely
218         // mess up the terminal being worked in
219         // In other words we need to shut down tcell before the program crashes
220         defer func() {
221                 if err := recover(); err != nil {
222                         screen.Fini()
223                         fmt.Println("Micro encountered an error:", err)
224                         // Print the stack trace too
225                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
226                         os.Exit(1)
227                 }
228         }()
229
230         messenger = new(Messenger)
231         messenger.history = make(map[string][]string)
232         views = make([]*View, 1)
233         views[0] = NewView(buf)
234
235         L.SetGlobal("OS", luar.New(L, runtime.GOOS))
236         L.SetGlobal("views", luar.New(L, views))
237         L.SetGlobal("mainView", luar.New(L, mainView))
238         L.SetGlobal("messenger", luar.New(L, messenger))
239         L.SetGlobal("GetOption", luar.New(L, GetOption))
240         L.SetGlobal("AddOption", luar.New(L, AddOption))
241         L.SetGlobal("BindKey", luar.New(L, BindKey))
242         L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
243
244         LoadPlugins()
245
246         for {
247                 // Display everything
248                 RedrawAll()
249
250                 // Wait for the user's action
251                 event := screen.PollEvent()
252
253                 if searching {
254                         // Since searching is done in real time, we need to redraw every time
255                         // there is a new event in the search bar
256                         HandleSearchEvent(event, views[mainView])
257                 } else {
258                         // Send it to the view
259                         views[mainView].HandleEvent(event)
260                 }
261         }
262 }