X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fmicro.go;h=a4d22b70e5db8f95e194c532e0499c33957b49cd;hb=b8fbbf5c83862cc47fac7edc2c26a6d79a9c9df0;hp=37c8218da838028ea549c19cb3f8dde6ef9f3855;hpb=c01995c1b61e2705d14ffa8e0b0679dd4c9f4912;p=micro.git diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 37c8218d..a4d22b70 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -5,60 +5,71 @@ import ( "fmt" "io/ioutil" "os" - "strings" + "regexp" + "runtime" + "sort" + "strconv" + "time" "github.com/go-errors/errors" isatty "github.com/mattn/go-isatty" - "github.com/zyedidia/micro/cmd/micro/action" - "github.com/zyedidia/micro/cmd/micro/buffer" - "github.com/zyedidia/micro/cmd/micro/config" - "github.com/zyedidia/micro/cmd/micro/screen" - "github.com/zyedidia/micro/cmd/micro/util" + lua "github.com/yuin/gopher-lua" + "github.com/zyedidia/micro/v2/internal/action" + "github.com/zyedidia/micro/v2/internal/buffer" + "github.com/zyedidia/micro/v2/internal/config" + ulua "github.com/zyedidia/micro/v2/internal/lua" + "github.com/zyedidia/micro/v2/internal/screen" + "github.com/zyedidia/micro/v2/internal/shell" + "github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/tcell" ) -const ( - doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click - autosaveTime = 8 // Number of seconds to wait before autosaving -) - var ( - // These variables should be set by the linker when compiling - - // Version is the version number or commit hash - Version = "0.0.0-unknown" - // CommitHash is the commit this version was built on - CommitHash = "Unknown" - // CompileDate is the date this binary was compiled on - CompileDate = "Unknown" - // Debug logging - Debug = "ON" - // Event channel events chan tcell.Event autosave chan bool // Command line flags flagVersion = flag.Bool("version", false, "Show the version number and information") - flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.") flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory") flagOptions = flag.Bool("options", false, "Show all option help") + flagDebug = flag.Bool("debug", false, "Enable debug mode (prints debug info to ./log.txt)") + flagPlugin = flag.String("plugin", "", "Plugin command") + flagClean = flag.Bool("clean", false, "Clean configuration directory") + optionFlags map[string]*string ) func InitFlags() { flag.Usage = func() { fmt.Println("Usage: micro [OPTIONS] [FILE]...") + fmt.Println("-clean") + fmt.Println(" \tCleans the configuration directory") fmt.Println("-config-dir dir") fmt.Println(" \tSpecify a custom location for the configuration directory") - fmt.Println("-startpos LINE,COL") + fmt.Println("[FILE]:LINE:COL (if the `parsecursor` option is enabled)") fmt.Println("+LINE:COL") fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer") - fmt.Println(" \tThis can also be done by opening file:LINE:COL") fmt.Println("-options") fmt.Println(" \tShow all option help") + fmt.Println("-debug") + fmt.Println(" \tEnable debug mode (enables logging to ./log.txt)") fmt.Println("-version") fmt.Println(" \tShow the version number and information") + fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n") + fmt.Println("-plugin install [PLUGIN]...") + fmt.Println(" \tInstall plugin(s)") + fmt.Println("-plugin remove [PLUGIN]...") + fmt.Println(" \tRemove plugin(s)") + fmt.Println("-plugin update [PLUGIN]...") + fmt.Println(" \tUpdate plugin(s) (if no argument is given, updates all plugins)") + fmt.Println("-plugin search [PLUGIN]...") + fmt.Println(" \tSearch for a plugin") + fmt.Println("-plugin list") + fmt.Println(" \tList installed plugins") + fmt.Println("-plugin available") + fmt.Println(" \tList available plugins") + 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") fmt.Println("-option value") fmt.Println(" \tSet `option` to `value` for this session") @@ -66,35 +77,63 @@ func InitFlags() { fmt.Println("\nUse `micro -options` to see the full list of configuration options") } - optionFlags := make(map[string]*string) + optionFlags = make(map[string]*string) - for k, v := range config.DefaultGlobalSettings() { - optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v)) + for k, v := range config.DefaultAllSettings() { + optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'.", k, v)) } flag.Parse() if *flagVersion { // If -version was passed - fmt.Println("Version:", Version) - fmt.Println("Commit hash:", CommitHash) - fmt.Println("Compiled on", CompileDate) + fmt.Println("Version:", util.Version) + fmt.Println("Commit hash:", util.CommitHash) + fmt.Println("Compiled on", util.CompileDate) os.Exit(0) } if *flagOptions { // If -options was passed - for k, v := range config.DefaultGlobalSettings() { + var keys []string + m := config.DefaultAllSettings() + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := m[k] fmt.Printf("-%s value\n", k) fmt.Printf(" \tDefault value: '%v'\n", v) } os.Exit(0) } + + if util.Debug == "OFF" && *flagDebug { + util.Debug = "ON" + } +} + +// DoPluginFlags parses and executes any flags that require LoadAllPlugins (-plugin and -clean) +func DoPluginFlags() { + if *flagClean || *flagPlugin != "" { + config.LoadAllPlugins() + + if *flagPlugin != "" { + args := flag.Args() + + config.PluginCommand(os.Stdout, *flagPlugin, args) + } else if *flagClean { + CleanConfig() + } + + os.Exit(0) + } } // LoadInput determines which files should be loaded into buffers // based on the input stored in flag.Args() -func LoadInput() []*buffer.Buffer { +func LoadInput(args []string) []*buffer.Buffer { // There are a number of ways micro should start given its input // 1. If it is given a files in flag.Args(), it should open those @@ -109,26 +148,49 @@ func LoadInput() []*buffer.Buffer { var filename string var input []byte var err error - args := flag.Args() buffers := make([]*buffer.Buffer, 0, len(args)) - if len(args) > 0 { - // Option 1 - // We go through each file and load it - for i := 0; i < len(args); i++ { - if strings.HasPrefix(args[i], "+") { - if strings.Contains(args[i], ":") { - split := strings.Split(args[i], ":") - *flagStartPos = split[0][1:] + "," + split[1] - } else { - *flagStartPos = args[i][1:] + ",0" - } + btype := buffer.BTDefault + if !isatty.IsTerminal(os.Stdout.Fd()) { + btype = buffer.BTStdout + } + + files := make([]string, 0, len(args)) + flagStartPos := buffer.Loc{-1, -1} + flagr := regexp.MustCompile(`^\+(\d+)(?::(\d+))?$`) + for _, a := range args { + match := flagr.FindStringSubmatch(a) + if len(match) == 3 && match[2] != "" { + line, err := strconv.Atoi(match[1]) + if err != nil { + screen.TermMessage(err) + continue + } + col, err := strconv.Atoi(match[2]) + if err != nil { + screen.TermMessage(err) + continue + } + flagStartPos = buffer.Loc{col - 1, line - 1} + } else if len(match) == 3 && match[2] == "" { + line, err := strconv.Atoi(match[1]) + if err != nil { + screen.TermMessage(err) continue } + flagStartPos = buffer.Loc{0, line - 1} + } else { + files = append(files, a) + } + } - buf, err := buffer.NewBufferFromFile(args[i], buffer.BTDefault) + if len(files) > 0 { + // Option 1 + // We go through each file and load it + for i := 0; i < len(files); i++ { + buf, err := buffer.NewBufferFromFileAtLoc(files[i], btype, flagStartPos) if err != nil { - util.TermMessage(err) + screen.TermMessage(err) continue } // If the file didn't exist, input will be empty, and we'll open an empty buffer @@ -140,94 +202,204 @@ func LoadInput() []*buffer.Buffer { // and we should read from stdin input, err = ioutil.ReadAll(os.Stdin) if err != nil { - util.TermMessage("Error reading from stdin: ", err) + screen.TermMessage("Error reading from stdin: ", err) input = []byte{} } - buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, buffer.BTDefault)) + buffers = append(buffers, buffer.NewBufferFromStringAtLoc(string(input), filename, btype, flagStartPos)) } else { // Option 3, just open an empty buffer - buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, buffer.BTDefault)) + buffers = append(buffers, buffer.NewBufferFromStringAtLoc(string(input), filename, btype, flagStartPos)) } return buffers } func main() { + defer func() { + if util.Stdout.Len() > 0 { + fmt.Fprint(os.Stdout, util.Stdout.String()) + } + os.Exit(0) + }() + + // runtime.SetCPUProfileRate(400) + // f, _ := os.Create("micro.prof") + // pprof.StartCPUProfile(f) + // defer pprof.StopCPUProfile() + var err error - InitLog() InitFlags() + + InitLog() + err = config.InitConfigDir(*flagConfigDir) if err != nil { - util.TermMessage(err) + screen.TermMessage(err) } + config.InitRuntimeFiles() err = config.ReadSettings() if err != nil { - util.TermMessage(err) + screen.TermMessage(err) } - config.InitGlobalSettings() - action.InitBindings() - - err = config.InitColorscheme() + err = config.InitGlobalSettings() if err != nil { - util.TermMessage(err) + screen.TermMessage(err) } - screen.Init() + // flag options + for k, v := range optionFlags { + if *v != "" { + nativeValue, err := config.GetNativeValue(k, config.DefaultAllSettings()[k], *v) + if err != nil { + screen.TermMessage(err) + continue + } + config.GlobalSettings[k] = nativeValue + } + } + + DoPluginFlags() + + err = screen.Init() + if err != nil { + fmt.Println(err) + fmt.Println("Fatal: Micro could not initialize a Screen.") + os.Exit(1) + } - // If we have an error, we can exit cleanly and not completely - // mess up the terminal being worked in - // In other words we need to shut down tcell before the program crashes defer func() { if err := recover(); err != nil { screen.Screen.Fini() fmt.Println("Micro encountered an error:", err) + // backup all open buffers + for _, b := range buffer.OpenBuffers { + b.Backup() + } // Print the stack trace too fmt.Print(errors.Wrap(err, 2).ErrorStack()) os.Exit(1) } }() - b := LoadInput()[0] - width, height := screen.Screen.Size() - ep := action.NewBufEditPane(0, 0, width, height-1, b) + err = config.LoadAllPlugins() + if err != nil { + screen.TermMessage(err) + } + action.InitBindings() + action.InitCommands() + + err = config.InitColorscheme() + if err != nil { + screen.TermMessage(err) + } + + args := flag.Args() + b := LoadInput(args) + + if len(b) == 0 { + // No buffers to open + screen.Screen.Fini() + runtime.Goexit() + } + + action.InitTabs(b) action.InitGlobals() + err = config.RunPluginFn("init") + if err != nil { + screen.TermMessage(err) + } + + events = make(chan tcell.Event) + // Here is the event loop which runs in a separate thread go func() { - events = make(chan tcell.Event) for { screen.Lock() - events <- screen.Screen.PollEvent() + e := screen.Screen.PollEvent() screen.Unlock() + if e != nil { + events <- e + } } }() - for { - // Display everything - screen.Screen.Fill(' ', config.DefStyle) - screen.Screen.HideCursor() - ep.Display() - action.InfoBar.Display() - screen.Screen.Show() + // clear the drawchan so we don't redraw excessively + // if someone requested a redraw before we started displaying + for len(screen.DrawChan()) > 0 { + <-screen.DrawChan() + } - var event tcell.Event + // wait for initial resize event + select { + case event := <-events: + action.Tabs.HandleEvent(event) + case <-time.After(10 * time.Millisecond): + // time out after 10ms + } - // Check for new events - select { - case event = <-events: - } + // Since this loop is very slow (waits for user input every time) it's + // okay to be inefficient and run it via a function every time + // We do this so we can recover from panics without crashing the editor + for { + DoEvent() + } +} + +// DoEvent runs the main action loop of the editor +func DoEvent() { + var event tcell.Event - if event != nil { - if action.InfoBar.HasPrompt { - action.InfoBar.HandleEvent(event) + // recover from errors without crashing the editor + defer func() { + if err := recover(); err != nil { + if e, ok := err.(*lua.ApiError); ok { + screen.TermMessage("Lua API error:", e) } else { - ep.HandleEvent(event) + 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") } } + }() + // Display everything + screen.Screen.Fill(' ', config.DefStyle) + screen.Screen.HideCursor() + action.Tabs.Display() + for _, ep := range action.MainTab().Panes { + ep.Display() + } + action.MainTab().Display() + action.InfoBar.Display() + screen.Screen.Show() + + // Check for new events + select { + case f := <-shell.Jobs: + // If a new job has finished while running in the background we should execute the callback + ulua.Lock.Lock() + f.Function(f.Output, f.Args) + ulua.Lock.Unlock() + case <-config.Autosave: + ulua.Lock.Lock() + for _, b := range buffer.OpenBuffers { + b.Save() + } + ulua.Lock.Unlock() + case <-shell.CloseTerms: + case event = <-events: + case <-screen.DrawChan(): + for len(screen.DrawChan()) > 0 { + <-screen.DrawChan() + } } - screen.Screen.Fini() + ulua.Lock.Lock() + if action.InfoBar.HasPrompt { + action.InfoBar.HandleEvent(event) + } else { + action.Tabs.HandleEvent(event) + } + ulua.Lock.Unlock() }