]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
Rename to tabstospaces for consistency
[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 style
34         defStyle tcell.Style
35
36         // Where the user's configuration is
37         // This should be $XDG_CONFIG_HOME/micro
38         // If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
39         configDir string
40
41         // Version is the version number.
42         // This should be set by the linker
43         Version = "Unknown"
44
45         // Is the help screen open
46         helpOpen = false
47
48         // L is the lua state
49         L *lua.LState
50 )
51
52 // LoadInput loads the file input for the editor
53 func LoadInput() (string, []byte, error) {
54         // There are a number of ways micro should start given its input
55         // 1. If it is given a file in os.Args, it should open that
56
57         // 2. If there is no input file and the input is not a terminal, that means
58         // something is being piped in and the stdin should be opened in an
59         // empty buffer
60
61         // 3. If there is no input file and the input is a terminal, an empty buffer
62         // should be opened
63
64         // These are empty by default so if we get to option 3, we can just returns the
65         // default values
66         var filename string
67         var input []byte
68         var err error
69
70         if len(os.Args) > 1 {
71                 // Option 1
72                 filename = os.Args[1]
73                 // Check that the file exists
74                 if _, e := os.Stat(filename); e == nil {
75                         input, err = ioutil.ReadFile(filename)
76                 }
77         } else if !isatty.IsTerminal(os.Stdin.Fd()) {
78                 // Option 2
79                 // The input is not a terminal, so something is being piped in
80                 // and we should read from stdin
81                 input, err = ioutil.ReadAll(os.Stdin)
82         }
83
84         // Option 3, or just return whatever we got
85         return filename, input, err
86 }
87
88 // InitConfigDir finds the configuration directory for micro according to the
89 // XDG spec.
90 // If no directory is found, it creates one.
91 func InitConfigDir() {
92         xdgHome := os.Getenv("XDG_CONFIG_HOME")
93         if xdgHome == "" {
94                 home, err := homedir.Dir()
95                 if err != nil {
96                         TermMessage("Error finding your home directory\nCan't load syntax files")
97                         return
98                 }
99                 xdgHome = home + "/.config"
100         }
101         configDir = xdgHome + "/micro"
102
103         if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
104                 err = os.Mkdir(xdgHome, os.ModePerm)
105                 if err != nil {
106                         TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
107                 }
108         }
109
110         if _, err := os.Stat(configDir); os.IsNotExist(err) {
111                 err = os.Mkdir(configDir, os.ModePerm)
112                 if err != nil {
113                         TermMessage("Error creating configuration directory: " + err.Error())
114                 }
115         }
116 }
117
118 // InitScreen creates and initializes the tcell screen
119 func InitScreen() {
120         // Should we enable true color?
121         truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
122
123         // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
124         // initializing tcell, but after that, we can set the TERM back to whatever it was
125         oldTerm := os.Getenv("TERM")
126         if truecolor {
127                 os.Setenv("TERM", "xterm-truecolor")
128         }
129
130         // Initilize tcell
131         var err error
132         screen, err = tcell.NewScreen()
133         if err != nil {
134                 fmt.Println(err)
135                 os.Exit(1)
136         }
137         if err = screen.Init(); err != nil {
138                 fmt.Println(err)
139                 os.Exit(1)
140         }
141
142         // Now we can put the TERM back to what it was before
143         if truecolor {
144                 os.Setenv("TERM", oldTerm)
145         }
146
147         // Default style
148         defStyle = tcell.StyleDefault.
149                 Foreground(tcell.ColorDefault).
150                 Background(tcell.ColorDefault)
151
152         // There may be another default style defined in the colorscheme
153         if style, ok := colorscheme["default"]; ok {
154                 defStyle = style
155         }
156
157         screen.SetStyle(defStyle)
158         screen.EnableMouse()
159 }
160
161 // Redraw redraws the screen and the given view
162 func Redraw(view *View) {
163         screen.Clear()
164         view.Display()
165         messenger.Display()
166         screen.Show()
167 }
168
169 var flagVersion = flag.Bool("version", false, "Show version number")
170
171 func main() {
172         flag.Parse()
173         if *flagVersion {
174                 fmt.Println("Micro version:", Version)
175                 os.Exit(0)
176         }
177
178         filename, input, err := LoadInput()
179         if err != nil {
180                 fmt.Println(err)
181                 os.Exit(1)
182         }
183
184         L = lua.NewState()
185         defer L.Close()
186
187         encoding.Register()
188         tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
189
190         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
191         InitConfigDir()
192         // Load the user's settings
193         InitSettings()
194         InitBindings()
195         // Load the syntax files, including the colorscheme
196         LoadSyntaxFiles()
197         // Load the help files
198         LoadHelp()
199
200         buf := NewBuffer(string(input), filename)
201
202         InitScreen()
203
204         // This is just so if we have an error, we can exit cleanly and not completely
205         // mess up the terminal being worked in
206         defer func() {
207                 if err := recover(); err != nil {
208                         screen.Fini()
209                         fmt.Println("Micro encountered an error:", err)
210                         // Print the stack trace too
211                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
212                         os.Exit(1)
213                 }
214         }()
215
216         messenger = new(Messenger)
217         view := NewView(buf)
218
219         L.SetGlobal("OS", luar.New(L, runtime.GOOS))
220         L.SetGlobal("view", luar.New(L, view))
221         L.SetGlobal("messenger", luar.New(L, messenger))
222         L.SetGlobal("GetOption", luar.New(L, GetOption))
223         L.SetGlobal("AddOption", luar.New(L, AddOption))
224
225         LoadPlugins()
226
227         for {
228                 // Display everything
229                 Redraw(view)
230
231                 // Wait for the user's action
232                 event := screen.PollEvent()
233
234                 if searching {
235                         HandleSearchEvent(event, view)
236                 } else {
237                         // Check if we should quit
238                         switch e := event.(type) {
239                         case *tcell.EventKey:
240                                 switch e.Key() {
241                                 case tcell.KeyCtrlQ:
242                                         // Make sure not to quit if there are unsaved changes
243                                         if helpOpen {
244                                                 view.OpenBuffer(buf)
245                                                 helpOpen = false
246                                         } else {
247                                                 if view.CanClose("Quit anyway? (yes, no, save) ") {
248                                                         screen.Fini()
249                                                         os.Exit(0)
250                                                 }
251                                         }
252                                 case tcell.KeyCtrlE:
253                                         input, canceled := messenger.Prompt("> ")
254                                         if !canceled {
255                                                 HandleCommand(input, view)
256                                         }
257                                 case tcell.KeyCtrlB:
258                                         input, canceled := messenger.Prompt("$ ")
259                                         if !canceled {
260                                                 HandleShellCommand(input, view, true)
261                                         }
262                                 case tcell.KeyCtrlG:
263                                         if !helpOpen {
264                                                 helpBuffer := NewBuffer(helpTxt, "help.md")
265                                                 helpBuffer.Name = "Help"
266                                                 helpOpen = true
267                                                 view.OpenBuffer(helpBuffer)
268                                         } else {
269                                                 view.OpenBuffer(buf)
270                                                 helpOpen = false
271                                         }
272                                 }
273                         }
274
275                         // Send it to the view
276                         view.HandleEvent(event)
277                 }
278         }
279 }