]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
Tab bar and support for opening multiple files
[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() []*Buffer {
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         var buffers []*Buffer
76
77         if len(os.Args) > 1 {
78                 // Option 1
79                 for i := 1; i < len(os.Args); i++ {
80                         filename = os.Args[i]
81                         // Check that the file exists
82                         if _, e := os.Stat(filename); e == nil {
83                                 input, err = ioutil.ReadFile(filename)
84                                 if err != nil {
85                                         TermMessage(err)
86                                         continue
87                                 }
88                         }
89                         buffers = append(buffers, NewBuffer(input, filename))
90                 }
91         } else if !isatty.IsTerminal(os.Stdin.Fd()) {
92                 // Option 2
93                 // The input is not a terminal, so something is being piped in
94                 // and we should read from stdin
95                 input, err = ioutil.ReadAll(os.Stdin)
96                 buffers = append(buffers, NewBuffer(input, filename))
97         } else {
98                 // Option 3, just open an empty buffer
99                 buffers = append(buffers, NewBuffer(input, filename))
100         }
101
102         return buffers
103 }
104
105 // InitConfigDir finds the configuration directory for micro according to the XDG spec.
106 // If no directory is found, it creates one.
107 func InitConfigDir() {
108         xdgHome := os.Getenv("XDG_CONFIG_HOME")
109         if xdgHome == "" {
110                 // The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
111                 home, err := homedir.Dir()
112                 if err != nil {
113                         TermMessage("Error finding your home directory\nCan't load config files")
114                         return
115                 }
116                 xdgHome = home + "/.config"
117         }
118         configDir = xdgHome + "/micro"
119
120         if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
121                 // If the xdgHome doesn't exist we should create it
122                 err = os.Mkdir(xdgHome, os.ModePerm)
123                 if err != nil {
124                         TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
125                 }
126         }
127
128         if _, err := os.Stat(configDir); os.IsNotExist(err) {
129                 // If the micro specific config directory doesn't exist we should create that too
130                 err = os.Mkdir(configDir, os.ModePerm)
131                 if err != nil {
132                         TermMessage("Error creating configuration directory: " + err.Error())
133                 }
134         }
135 }
136
137 // InitScreen creates and initializes the tcell screen
138 func InitScreen() {
139         // Should we enable true color?
140         truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
141
142         // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
143         // initializing tcell, but after that, we can set the TERM back to whatever it was
144         oldTerm := os.Getenv("TERM")
145         if truecolor {
146                 os.Setenv("TERM", "xterm-truecolor")
147         }
148
149         // Initilize tcell
150         var err error
151         screen, err = tcell.NewScreen()
152         if err != nil {
153                 fmt.Println(err)
154                 os.Exit(1)
155         }
156         if err = screen.Init(); err != nil {
157                 fmt.Println(err)
158                 os.Exit(1)
159         }
160
161         // Now we can put the TERM back to what it was before
162         if truecolor {
163                 os.Setenv("TERM", oldTerm)
164         }
165
166         // Default style
167         defStyle = tcell.StyleDefault.
168                 Foreground(tcell.ColorDefault).
169                 Background(tcell.ColorDefault)
170
171         // There may be another default style defined in the colorscheme
172         // In that case we should use that one
173         if style, ok := colorscheme["default"]; ok {
174                 defStyle = style
175         }
176
177         screen.SetStyle(defStyle)
178         screen.EnableMouse()
179 }
180
181 // RedrawAll redraws everything -- all the views and the messenger
182 func RedrawAll() {
183         messenger.Clear()
184         // for _, v := range views {
185         views[mainView].Display()
186         // }
187         DisplayTabBar()
188         messenger.Display()
189         screen.Show()
190 }
191
192 var flagVersion = flag.Bool("version", false, "Show version number")
193
194 func main() {
195         flag.Parse()
196         if *flagVersion {
197                 fmt.Println("Micro version:", Version)
198                 os.Exit(0)
199         }
200
201         L = lua.NewState()
202         defer L.Close()
203
204         // Some encoding stuff in case the user isn't using UTF-8
205         encoding.Register()
206         tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
207
208         // Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
209         InitConfigDir()
210         // Load the user's settings
211         InitSettings()
212         InitCommands()
213         InitBindings()
214         // Load the syntax files, including the colorscheme
215         LoadSyntaxFiles()
216         // Load the help files
217         LoadHelp()
218
219         InitScreen()
220
221         // This is just so if we have an error, we can exit cleanly and not completely
222         // mess up the terminal being worked in
223         // In other words we need to shut down tcell before the program crashes
224         defer func() {
225                 if err := recover(); err != nil {
226                         screen.Fini()
227                         fmt.Println("Micro encountered an error:", err)
228                         // Print the stack trace too
229                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
230                         os.Exit(1)
231                 }
232         }()
233
234         messenger = new(Messenger)
235         messenger.history = make(map[string][]string)
236         // views = make([]*View, 1)
237         buffers := LoadInput()
238         for i, buf := range buffers {
239                 views = append(views, NewView(buf))
240                 views[i].Num = i
241         }
242
243         L.SetGlobal("OS", luar.New(L, runtime.GOOS))
244         L.SetGlobal("views", luar.New(L, views))
245         L.SetGlobal("mainView", luar.New(L, mainView))
246         L.SetGlobal("messenger", luar.New(L, messenger))
247         L.SetGlobal("GetOption", luar.New(L, GetOption))
248         L.SetGlobal("AddOption", luar.New(L, AddOption))
249         L.SetGlobal("BindKey", luar.New(L, BindKey))
250         L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
251
252         LoadPlugins()
253
254         for {
255                 // Display everything
256                 RedrawAll()
257
258                 // Wait for the user's action
259                 event := screen.PollEvent()
260
261                 if searching {
262                         // Since searching is done in real time, we need to redraw every time
263                         // there is a new event in the search bar
264                         HandleSearchEvent(event, views[mainView])
265                 } else {
266                         // Send it to the view
267                         views[mainView].HandleEvent(event)
268                 }
269         }
270 }