]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
Fix dropped redraw events (#1675)
[micro.git] / cmd / micro / micro.go
1 package main
2
3 import (
4         "flag"
5         "fmt"
6         "io/ioutil"
7         "os"
8         "regexp"
9         "runtime"
10         "sort"
11         "time"
12
13         "github.com/go-errors/errors"
14         isatty "github.com/mattn/go-isatty"
15         lua "github.com/yuin/gopher-lua"
16         "github.com/zyedidia/micro/v2/internal/action"
17         "github.com/zyedidia/micro/v2/internal/buffer"
18         "github.com/zyedidia/micro/v2/internal/config"
19         "github.com/zyedidia/micro/v2/internal/screen"
20         "github.com/zyedidia/micro/v2/internal/shell"
21         "github.com/zyedidia/micro/v2/internal/util"
22         "github.com/zyedidia/tcell"
23 )
24
25 var (
26         // Event channel
27         events   chan tcell.Event
28         autosave chan bool
29
30         // Command line flags
31         flagVersion   = flag.Bool("version", false, "Show the version number and information")
32         flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
33         flagOptions   = flag.Bool("options", false, "Show all option help")
34         flagDebug     = flag.Bool("debug", false, "Enable debug mode (prints debug info to ./log.txt)")
35         flagPlugin    = flag.String("plugin", "", "Plugin command")
36         flagClean     = flag.Bool("clean", false, "Clean configuration directory")
37         optionFlags   map[string]*string
38 )
39
40 func InitFlags() {
41         flag.Usage = func() {
42                 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
43                 fmt.Println("-clean")
44                 fmt.Println("    \tCleans the configuration directory")
45                 fmt.Println("-config-dir dir")
46                 fmt.Println("    \tSpecify a custom location for the configuration directory")
47                 fmt.Println("[FILE]:LINE:COL")
48                 fmt.Println("+LINE:COL")
49                 fmt.Println("    \tSpecify a line and column to start the cursor at when opening a buffer")
50                 fmt.Println("-options")
51                 fmt.Println("    \tShow all option help")
52                 fmt.Println("-debug")
53                 fmt.Println("    \tEnable debug mode (enables logging to ./log.txt)")
54                 fmt.Println("-version")
55                 fmt.Println("    \tShow the version number and information")
56
57                 fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n")
58                 fmt.Println("-plugin install [PLUGIN]...")
59                 fmt.Println("    \tInstall plugin(s)")
60                 fmt.Println("-plugin remove [PLUGIN]...")
61                 fmt.Println("    \tRemove plugin(s)")
62                 fmt.Println("-plugin update [PLUGIN]...")
63                 fmt.Println("    \tUpdate plugin(s) (if no argument is given, updates all plugins)")
64                 fmt.Println("-plugin search [PLUGIN]...")
65                 fmt.Println("    \tSearch for a plugin")
66                 fmt.Println("-plugin list")
67                 fmt.Println("    \tList installed plugins")
68                 fmt.Println("-plugin available")
69                 fmt.Println("    \tList available plugins")
70
71                 fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
72                 fmt.Println("-option value")
73                 fmt.Println("    \tSet `option` to `value` for this session")
74                 fmt.Println("    \tFor example: `micro -syntax off file.c`")
75                 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
76         }
77
78         optionFlags = make(map[string]*string)
79
80         for k, v := range config.DefaultAllSettings() {
81                 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'.", k, v))
82         }
83
84         flag.Parse()
85
86         if *flagVersion {
87                 // If -version was passed
88                 fmt.Println("Version:", util.Version)
89                 fmt.Println("Commit hash:", util.CommitHash)
90                 fmt.Println("Compiled on", util.CompileDate)
91                 os.Exit(0)
92         }
93
94         if *flagOptions {
95                 // If -options was passed
96                 var keys []string
97                 m := config.DefaultAllSettings()
98                 for k := range m {
99                         keys = append(keys, k)
100                 }
101                 sort.Strings(keys)
102                 for _, k := range keys {
103                         v := m[k]
104                         fmt.Printf("-%s value\n", k)
105                         fmt.Printf("    \tDefault value: '%v'\n", v)
106                 }
107                 os.Exit(0)
108         }
109
110         if util.Debug == "OFF" && *flagDebug {
111                 util.Debug = "ON"
112         }
113 }
114
115 // DoPluginFlags parses and executes any flags that require LoadAllPlugins (-plugin and -clean)
116 func DoPluginFlags() {
117         if *flagClean || *flagPlugin != "" {
118                 config.LoadAllPlugins()
119
120                 if *flagPlugin != "" {
121                         args := flag.Args()
122
123                         config.PluginCommand(os.Stdout, *flagPlugin, args)
124                 } else if *flagClean {
125                         CleanConfig()
126                 }
127
128                 os.Exit(0)
129         }
130 }
131
132 // LoadInput determines which files should be loaded into buffers
133 // based on the input stored in flag.Args()
134 func LoadInput() []*buffer.Buffer {
135         // There are a number of ways micro should start given its input
136
137         // 1. If it is given a files in flag.Args(), it should open those
138
139         // 2. If there is no input file and the input is not a terminal, that means
140         // something is being piped in and the stdin should be opened in an
141         // empty buffer
142
143         // 3. If there is no input file and the input is a terminal, an empty buffer
144         // should be opened
145
146         var filename string
147         var input []byte
148         var err error
149         args := flag.Args()
150         buffers := make([]*buffer.Buffer, 0, len(args))
151
152         btype := buffer.BTDefault
153         if !isatty.IsTerminal(os.Stdout.Fd()) {
154                 btype = buffer.BTStdout
155         }
156
157         files := make([]string, 0, len(args))
158         flagStartPos := ""
159         flagr := regexp.MustCompile(`^\+\d+(:\d+)?$`)
160         for _, a := range args {
161                 if flagr.MatchString(a) {
162                         flagStartPos = a[1:]
163                 } else {
164                         if flagStartPos != "" {
165                                 files = append(files, a+":"+flagStartPos)
166                                 flagStartPos = ""
167                         } else {
168                                 files = append(files, a)
169                         }
170                 }
171         }
172
173         if len(files) > 0 {
174                 // Option 1
175                 // We go through each file and load it
176                 for i := 0; i < len(files); i++ {
177                         buf, err := buffer.NewBufferFromFile(files[i], btype)
178                         if err != nil {
179                                 screen.TermMessage(err)
180                                 continue
181                         }
182                         // If the file didn't exist, input will be empty, and we'll open an empty buffer
183                         buffers = append(buffers, buf)
184                 }
185         } else if !isatty.IsTerminal(os.Stdin.Fd()) {
186                 // Option 2
187                 // The input is not a terminal, so something is being piped in
188                 // and we should read from stdin
189                 input, err = ioutil.ReadAll(os.Stdin)
190                 if err != nil {
191                         screen.TermMessage("Error reading from stdin: ", err)
192                         input = []byte{}
193                 }
194                 buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, btype))
195         } else {
196                 // Option 3, just open an empty buffer
197                 buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, btype))
198         }
199
200         return buffers
201 }
202
203 func main() {
204         defer func() {
205                 if util.Stdout.Len() > 0 {
206                         fmt.Fprint(os.Stdout, util.Stdout.String())
207                 }
208                 os.Exit(0)
209         }()
210
211         // runtime.SetCPUProfileRate(400)
212         // f, _ := os.Create("micro.prof")
213         // pprof.StartCPUProfile(f)
214         // defer pprof.StopCPUProfile()
215
216         var err error
217
218         InitFlags()
219
220         InitLog()
221
222         err = config.InitConfigDir(*flagConfigDir)
223         if err != nil {
224                 screen.TermMessage(err)
225         }
226
227         config.InitRuntimeFiles()
228         err = config.ReadSettings()
229         if err != nil {
230                 screen.TermMessage(err)
231         }
232         config.InitGlobalSettings()
233
234         // flag options
235         for k, v := range optionFlags {
236                 if *v != "" {
237                         nativeValue, err := config.GetNativeValue(k, config.DefaultAllSettings()[k], *v)
238                         if err != nil {
239                                 screen.TermMessage(err)
240                                 continue
241                         }
242                         config.GlobalSettings[k] = nativeValue
243                 }
244         }
245
246         DoPluginFlags()
247
248         screen.Init()
249
250         defer func() {
251                 if err := recover(); err != nil {
252                         screen.Screen.Fini()
253                         fmt.Println("Micro encountered an error:", err)
254                         // backup all open buffers
255                         for _, b := range buffer.OpenBuffers {
256                                 b.Backup(false)
257                         }
258                         // Print the stack trace too
259                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
260                         os.Exit(1)
261                 }
262         }()
263
264         err = config.LoadAllPlugins()
265         if err != nil {
266                 screen.TermMessage(err)
267         }
268
269         action.InitBindings()
270         action.InitCommands()
271
272         err = config.InitColorscheme()
273         if err != nil {
274                 screen.TermMessage(err)
275         }
276
277         b := LoadInput()
278
279         if len(b) == 0 {
280                 // No buffers to open
281                 screen.Screen.Fini()
282                 runtime.Goexit()
283         }
284
285         action.InitTabs(b)
286         action.InitGlobals()
287
288         err = config.RunPluginFn("init")
289         if err != nil {
290                 screen.TermMessage(err)
291         }
292
293         events = make(chan tcell.Event)
294
295         // Here is the event loop which runs in a separate thread
296         go func() {
297                 for {
298                         screen.Lock()
299                         e := screen.Screen.PollEvent()
300                         screen.Unlock()
301                         if e != nil {
302                                 events <- e
303                         }
304                 }
305         }()
306
307         // clear the drawchan so we don't redraw excessively
308         // if someone requested a redraw before we started displaying
309         for len(screen.DrawChan()) > 0 {
310                 <-screen.DrawChan()
311         }
312
313         // wait for initial resize event
314         select {
315         case event := <-events:
316                 action.Tabs.HandleEvent(event)
317         case <-time.After(10 * time.Millisecond):
318                 // time out after 10ms
319         }
320
321         // Since this loop is very slow (waits for user input every time) it's
322         // okay to be inefficient and run it via a function every time
323         // We do this so we can recover from panics without crashing the editor
324         for {
325                 DoEvent()
326         }
327 }
328
329 // DoEvent runs the main action loop of the editor
330 func DoEvent() {
331         var event tcell.Event
332
333         // recover from errors without crashing the editor
334         defer func() {
335                 if err := recover(); err != nil {
336                         if e, ok := err.(*lua.ApiError); ok {
337                                 screen.TermMessage("Lua API error:", e)
338                         } else {
339                                 screen.TermMessage("Micro encountered an error:", errors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/zyedidia/micro/issues")
340                         }
341                 }
342         }()
343         // Display everything
344         screen.Screen.Fill(' ', config.DefStyle)
345         screen.Screen.HideCursor()
346         action.Tabs.Display()
347         for _, ep := range action.MainTab().Panes {
348                 ep.Display()
349         }
350         action.MainTab().Display()
351         action.InfoBar.Display()
352         screen.Screen.Show()
353
354         // Check for new events
355         select {
356         case f := <-shell.Jobs:
357                 // If a new job has finished while running in the background we should execute the callback
358                 f.Function(f.Output, f.Args)
359         case <-config.Autosave:
360                 for _, b := range buffer.OpenBuffers {
361                         b.Save()
362                 }
363         case <-shell.CloseTerms:
364         case event = <-events:
365         case <-screen.DrawChan():
366                 for len(screen.DrawChan()) > 0 {
367                         <-screen.DrawChan()
368                 }
369         }
370
371         if action.InfoBar.HasPrompt {
372                 action.InfoBar.HandleEvent(event)
373         } else {
374                 action.Tabs.HandleEvent(event)
375         }
376 }