]> git.lizzy.rs Git - micro.git/blob - cmd/micro/micro.go
Fix reading clipboard internally for OSC52
[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/clipboard"
20         "github.com/zyedidia/micro/v2/internal/config"
21         ulua "github.com/zyedidia/micro/v2/internal/lua"
22         "github.com/zyedidia/micro/v2/internal/screen"
23         "github.com/zyedidia/micro/v2/internal/shell"
24         "github.com/zyedidia/micro/v2/internal/util"
25         "github.com/zyedidia/tcell"
26 )
27
28 var (
29         // Event channel
30         autosave chan bool
31
32         // Command line flags
33         flagVersion   = flag.Bool("version", false, "Show the version number and information")
34         flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
35         flagOptions   = flag.Bool("options", false, "Show all option help")
36         flagDebug     = flag.Bool("debug", false, "Enable debug mode (prints debug info to ./log.txt)")
37         flagPlugin    = flag.String("plugin", "", "Plugin command")
38         flagClean     = flag.Bool("clean", false, "Clean configuration directory")
39         optionFlags   map[string]*string
40 )
41
42 func InitFlags() {
43         flag.Usage = func() {
44                 fmt.Println("Usage: micro [OPTIONS] [FILE]...")
45                 fmt.Println("-clean")
46                 fmt.Println("    \tCleans the configuration directory")
47                 fmt.Println("-config-dir dir")
48                 fmt.Println("    \tSpecify a custom location for the configuration directory")
49                 fmt.Println("[FILE]:LINE:COL (if the `parsecursor` option is enabled)")
50                 fmt.Println("+LINE:COL")
51                 fmt.Println("    \tSpecify a line and column to start the cursor at when opening a buffer")
52                 fmt.Println("-options")
53                 fmt.Println("    \tShow all option help")
54                 fmt.Println("-debug")
55                 fmt.Println("    \tEnable debug mode (enables logging to ./log.txt)")
56                 fmt.Println("-version")
57                 fmt.Println("    \tShow the version number and information")
58
59                 fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n")
60                 fmt.Println("-plugin install [PLUGIN]...")
61                 fmt.Println("    \tInstall plugin(s)")
62                 fmt.Println("-plugin remove [PLUGIN]...")
63                 fmt.Println("    \tRemove plugin(s)")
64                 fmt.Println("-plugin update [PLUGIN]...")
65                 fmt.Println("    \tUpdate plugin(s) (if no argument is given, updates all plugins)")
66                 fmt.Println("-plugin search [PLUGIN]...")
67                 fmt.Println("    \tSearch for a plugin")
68                 fmt.Println("-plugin list")
69                 fmt.Println("    \tList installed plugins")
70                 fmt.Println("-plugin available")
71                 fmt.Println("    \tList available plugins")
72
73                 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")
74                 fmt.Println("-option value")
75                 fmt.Println("    \tSet `option` to `value` for this session")
76                 fmt.Println("    \tFor example: `micro -syntax off file.c`")
77                 fmt.Println("\nUse `micro -options` to see the full list of configuration options")
78         }
79
80         optionFlags = make(map[string]*string)
81
82         for k, v := range config.DefaultAllSettings() {
83                 optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'.", k, v))
84         }
85
86         flag.Parse()
87
88         if *flagVersion {
89                 // If -version was passed
90                 fmt.Println("Version:", util.Version)
91                 fmt.Println("Commit hash:", util.CommitHash)
92                 fmt.Println("Compiled on", util.CompileDate)
93                 os.Exit(0)
94         }
95
96         if *flagOptions {
97                 // If -options was passed
98                 var keys []string
99                 m := config.DefaultAllSettings()
100                 for k := range m {
101                         keys = append(keys, k)
102                 }
103                 sort.Strings(keys)
104                 for _, k := range keys {
105                         v := m[k]
106                         fmt.Printf("-%s value\n", k)
107                         fmt.Printf("    \tDefault value: '%v'\n", v)
108                 }
109                 os.Exit(0)
110         }
111
112         if util.Debug == "OFF" && *flagDebug {
113                 util.Debug = "ON"
114         }
115 }
116
117 // DoPluginFlags parses and executes any flags that require LoadAllPlugins (-plugin and -clean)
118 func DoPluginFlags() {
119         if *flagClean || *flagPlugin != "" {
120                 config.LoadAllPlugins()
121
122                 if *flagPlugin != "" {
123                         args := flag.Args()
124
125                         config.PluginCommand(os.Stdout, *flagPlugin, args)
126                 } else if *flagClean {
127                         CleanConfig()
128                 }
129
130                 os.Exit(0)
131         }
132 }
133
134 // LoadInput determines which files should be loaded into buffers
135 // based on the input stored in flag.Args()
136 func LoadInput(args []string) []*buffer.Buffer {
137         // There are a number of ways micro should start given its input
138
139         // 1. If it is given a files in flag.Args(), it should open those
140
141         // 2. If there is no input file and the input is not a terminal, that means
142         // something is being piped in and the stdin should be opened in an
143         // empty buffer
144
145         // 3. If there is no input file and the input is a terminal, an empty buffer
146         // should be opened
147
148         var filename string
149         var input []byte
150         var err error
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         err = screen.Init()
266         if err != nil {
267                 fmt.Println(err)
268                 fmt.Println("Fatal: Micro could not initialize a Screen.")
269                 os.Exit(1)
270         }
271
272         m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
273         clipErr := clipboard.Initialize(m)
274
275         defer func() {
276                 if err := recover(); err != nil {
277                         screen.Screen.Fini()
278                         fmt.Println("Micro encountered an error:", err)
279                         // backup all open buffers
280                         for _, b := range buffer.OpenBuffers {
281                                 b.Backup()
282                         }
283                         // Print the stack trace too
284                         fmt.Print(errors.Wrap(err, 2).ErrorStack())
285                         os.Exit(1)
286                 }
287         }()
288
289         err = config.LoadAllPlugins()
290         if err != nil {
291                 screen.TermMessage(err)
292         }
293
294         action.InitBindings()
295         action.InitCommands()
296
297         err = config.InitColorscheme()
298         if err != nil {
299                 screen.TermMessage(err)
300         }
301
302         args := flag.Args()
303         b := LoadInput(args)
304
305         if len(b) == 0 {
306                 // No buffers to open
307                 screen.Screen.Fini()
308                 runtime.Goexit()
309         }
310
311         action.InitTabs(b)
312         action.InitGlobals()
313
314         err = config.RunPluginFn("init")
315         if err != nil {
316                 screen.TermMessage(err)
317         }
318
319         if clipErr != nil {
320                 action.InfoBar.Error(clipErr, " or change 'clipboard' option")
321         }
322
323         screen.Events = make(chan tcell.Event)
324
325         // Here is the event loop which runs in a separate thread
326         go func() {
327                 for {
328                         screen.Lock()
329                         e := screen.Screen.PollEvent()
330                         screen.Unlock()
331                         if e != nil {
332                                 screen.Events <- e
333                         }
334                 }
335         }()
336
337         // clear the drawchan so we don't redraw excessively
338         // if someone requested a redraw before we started displaying
339         for len(screen.DrawChan()) > 0 {
340                 <-screen.DrawChan()
341         }
342
343         // wait for initial resize event
344         select {
345         case event := <-screen.Events:
346                 action.Tabs.HandleEvent(event)
347         case <-time.After(10 * time.Millisecond):
348                 // time out after 10ms
349         }
350
351         // Since this loop is very slow (waits for user input every time) it's
352         // okay to be inefficient and run it via a function every time
353         // We do this so we can recover from panics without crashing the editor
354         for {
355                 DoEvent()
356         }
357 }
358
359 // DoEvent runs the main action loop of the editor
360 func DoEvent() {
361         var event tcell.Event
362
363         // recover from errors without crashing the editor
364         defer func() {
365                 if err := recover(); err != nil {
366                         if e, ok := err.(*lua.ApiError); ok {
367                                 screen.TermMessage("Lua API error:", e)
368                         } else {
369                                 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")
370                         }
371                 }
372         }()
373         // Display everything
374         screen.Screen.Fill(' ', config.DefStyle)
375         screen.Screen.HideCursor()
376         action.Tabs.Display()
377         for _, ep := range action.MainTab().Panes {
378                 ep.Display()
379         }
380         action.MainTab().Display()
381         action.InfoBar.Display()
382         screen.Screen.Show()
383
384         // Check for new events
385         select {
386         case f := <-shell.Jobs:
387                 // If a new job has finished while running in the background we should execute the callback
388                 ulua.Lock.Lock()
389                 f.Function(f.Output, f.Args)
390                 ulua.Lock.Unlock()
391         case <-config.Autosave:
392                 ulua.Lock.Lock()
393                 for _, b := range buffer.OpenBuffers {
394                         b.Save()
395                 }
396                 ulua.Lock.Unlock()
397         case <-shell.CloseTerms:
398         case event = <-screen.Events:
399         case <-screen.DrawChan():
400                 for len(screen.DrawChan()) > 0 {
401                         <-screen.DrawChan()
402                 }
403         }
404
405         ulua.Lock.Lock()
406         if action.InfoBar.HasPrompt {
407                 action.InfoBar.HandleEvent(event)
408         } else {
409                 action.Tabs.HandleEvent(event)
410         }
411         ulua.Lock.Unlock()
412 }