package project import ( "context" "errors" "os" "os/exec" "path/filepath" "github.com/spf13/cobra" "github.com/spf13/viper" "gitea.libretechconsulting.com/rmcguire/git-project-manager/cmd/util" ) var projectOpenCmd = &cobra.Command{ Use: "open [fuzzy alias search]", Short: "Open project in your IDE", Aliases: []string{"goto", "cd"}, Args: cobra.OnlyValidArgs, ValidArgsFunction: util.ValidAliasesFunc, Long: util.ProjOpenCmdLong, Run: projectOpenCmdRun, } var knownEditors = []string{ "vim", "emacs", "code", "codium", "/Applications/GoLand.app/Contents/MacOS/goland", } var entrypointFiles = []string{ "README.md", "README", "main.go", "main.py", "app.py", "index.php", "server.php", "index.js", "app.js", "server.js", "app.ts", "main.c", "main.cpp", } func projectOpenCmdRun(cmd *cobra.Command, args []string) { // Find an editor editor := findEditor() if editor == "" { utils.Logger().Fatal("No usable editor found") } remotes := viper.GetStringSlice(util.FlagRemote) fzfOpts := &util.FzfProjectOpts{ Ctx: cmd.Context(), Search: utils.SearchStringFromArgs(args), Remotes: remotes, } project := utils.FzfCwdOrSearchProjectAliases(fzfOpts) if project == nil { utils.Logger().Fatal("No project to open, nothing to do") } // Check the project path := utils.Cache().GetProjectPath(project) if _, err := os.Stat(path); err != nil { utils.Logger().Fatal("Unable to open project", utils.Logger().Args("error", err)) } // Open the project with the editor file := getEntrypointFile(path) openEditor(cmd.Context(), editor, path+"/"+file) } func openEditor(ctx context.Context, editor string, path string) { // Compile arguments args := make([]string, 0, 1) if utils.Config().Editor.OpenFlags != "" { args = append(args, utils.Config().Editor.OpenFlags) } args = append(args, path) // Launch editor cmd := exec.CommandContext(ctx, editor, args...) cmd.Dir = filepath.Dir(path) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { utils.Logger().Error("Failed to open project", utils.Logger().Args( "error", err.Error(), "command", cmd.String(), )) } cmd.Wait() } func getEntrypointFile(projectPath string) string { if err := os.Chdir(projectPath); err != nil { return "" } else if utils.Config().Editor.OpenDirectory { return "." } // Search list of well-known main files for _, f := range entrypointFiles { if _, err := os.Stat(f); err == nil { return f } } return "" } func findEditor() string { var editor string var err error // First try configured editor if utils.Config().Editor.Binary != "" { editor, err = getEditor(utils.Config().Editor.Binary) if err != nil || editor == "" { utils.Logger().Error("Configured editor is not usable, finding others", utils.Logger().Args( "error", err, )) } } // Then try to find a known editor if editor == "" || err != nil { utils.Config().Editor.OpenFlags = "" for _, e := range knownEditors { path, err := getEditor(e) if err == nil && path != "" { editor = path break } } } utils.Logger().Debug("Editor found for open", utils.Logger().Args( "editor", editor, "command", editor+" "+utils.Config().Editor.OpenFlags, )) return editor } func getEditor(editor string) (string, error) { path, err := getEditorPath(editor) if path != "" && err == nil { if !isEditorExecutable(path) { err = errors.New("editor is not executable") } } return path, err } func getEditorPath(editor string) (string, error) { // Check path first if editor[0] != '/' { editor, _ = exec.LookPath(editor) } return util.ResolvePath(editor) } func isEditorExecutable(editor string) bool { var canExec bool stat, err := os.Stat(editor) if err == nil && (stat.Mode()&0o444 != 0 && stat.Mode()&0o111 != 0) { canExec = true } return canExec } func init() { projectOpenCmd.PersistentFlags().String("displayName", "", "Set display name of editor (meant for config file)") projectOpenCmd.PersistentFlags().String("binary", "", "Path to editor binary") projectOpenCmd.PersistentFlags().String("openFlags", "", "Optional flags when opening project (e.g. --reuse-window)") viper.BindPFlag("editor.displayName", projectOpenCmd.Flag("displayName")) viper.BindPFlag("editor.binary", projectOpenCmd.Flag("binary")) viper.BindPFlag("editor.openFlags", projectOpenCmd.Flag("openFlags")) }