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