feat(cli): add config open command to edit config in $EDITOR
- commit
- 01be947d1340b39fa7966aa4b5a6a7656a53f973
- author
- bnema · · 2025-12-28 18:13 UTC
- parent
- 89ee588
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
2 files · +63 -0
M CHANGELOG.md +1 -0
| @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. | |||
| 19 | 19 | - `--force` / `-f` flag to skip version check and reinstall the current version. | |
| 20 | 20 | - Reuses existing auto-update infrastructure (check, download, stage, apply on exit). | |
| 21 | 21 | - Theme-aware styling consistent with other CLI commands. | |
| 22 | + | - **Config open command**: New `dumber config open` command to open the config file in your preferred editor. Uses `$EDITOR`, then `$VISUAL`, with fallback to nano/vim/vi. | |
| 22 | 23 | - **Move pane to tab**: New pane-mode actions to move the active pane to another tab. | |
| 23 | 24 | - `Ctrl+P → m`: Opens a tab picker modal. | |
| 24 | 25 | - `Ctrl+P → M`: Moves the active pane to the next tab (creates a new tab if needed). | |
| @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. | |||||
| 19 | - `--force` / `-f` flag to skip version check and reinstall the current version. | 19 | - `--force` / `-f` flag to skip version check and reinstall the current version. | ||
| 20 | - Reuses existing auto-update infrastructure (check, download, stage, apply on exit). | 20 | - Reuses existing auto-update infrastructure (check, download, stage, apply on exit). | ||
| 21 | - Theme-aware styling consistent with other CLI commands. | 21 | - Theme-aware styling consistent with other CLI commands. | ||
| 22 | + | - **Config open command**: New `dumber config open` command to open the config file in your preferred editor. Uses `$EDITOR`, then `$VISUAL`, with fallback to nano/vim/vi. | |||
| 22 | - **Move pane to tab**: New pane-mode actions to move the active pane to another tab. | 23 | - **Move pane to tab**: New pane-mode actions to move the active pane to another tab. | ||
| 23 | - `Ctrl+P → m`: Opens a tab picker modal. | 24 | - `Ctrl+P → m`: Opens a tab picker modal. | ||
| 24 | - `Ctrl+P → M`: Moves the active pane to the next tab (creates a new tab if needed). | 25 | - `Ctrl+P → M`: Moves the active pane to the next tab (creates a new tab if needed). | ||
M internal/cli/cmd/config.go +62 -0
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "os" | |
| 7 | + | "os/exec" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/charmbracelet/bubbles/spinner" | |
| 9 | 10 | tea "github.com/charmbracelet/bubbletea" | |
| @@ -24,6 +25,18 @@ var configCmd = &cobra.Command{ | |||
| 24 | 25 | Long: `View configuration status and migrate to add new default settings.`, | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 28 | + | var configOpenCmd = &cobra.Command{ | |
| 29 | + | Use: "open", | |
| 30 | + | Short: "Open config file in your editor", | |
| 31 | + | Long: `Opens the config file using your preferred editor. | |
| 32 | + | ||
| 33 | + | Editor selection order: | |
| 34 | + | 1. $EDITOR environment variable | |
| 35 | + | 2. $VISUAL environment variable | |
| 36 | + | 3. Fallback to: nano, vim, vi (first found)`, | |
| 37 | + | RunE: runConfigOpen, | |
| 38 | + | } | |
| 39 | + | ||
| 27 | 40 | var configStatusCmd = &cobra.Command{ | |
| 28 | 41 | Use: "status", | |
| 29 | 42 | Short: "Show config file status and migration availability", | |
| @@ -42,11 +55,60 @@ Existing settings are never modified - only missing keys are added with default | |||
| 42 | 55 | ||
| 43 | 56 | func init() { | |
| 44 | 57 | rootCmd.AddCommand(configCmd) | |
| 58 | + | configCmd.AddCommand(configOpenCmd) | |
| 45 | 59 | configCmd.AddCommand(configStatusCmd) | |
| 46 | 60 | configCmd.AddCommand(configMigrateCmd) | |
| 47 | 61 | configMigrateCmd.Flags().BoolVarP(&configYes, "yes", "y", false, "skip confirmation prompt") | |
| 48 | 62 | } | |
| 49 | 63 | ||
| 64 | + | // runConfigOpen opens the config file in the user's preferred editor. | |
| 65 | + | func runConfigOpen(_ *cobra.Command, _ []string) error { | |
| 66 | + | configFile, err := config.GetConfigFile() | |
| 67 | + | if err != nil { | |
| 68 | + | return fmt.Errorf("get config file: %w", err) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // Check if config file exists | |
| 72 | + | if _, statErr := os.Stat(configFile); os.IsNotExist(statErr) { | |
| 73 | + | return fmt.Errorf("config file does not exist: %s", configFile) | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | editor := getEditor() | |
| 77 | + | if editor == "" { | |
| 78 | + | return fmt.Errorf("no editor found: set $EDITOR or $VISUAL environment variable") | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | cmd := exec.Command(editor, configFile) | |
| 82 | + | cmd.Stdin = os.Stdin | |
| 83 | + | cmd.Stdout = os.Stdout | |
| 84 | + | cmd.Stderr = os.Stderr | |
| 85 | + | ||
| 86 | + | return cmd.Run() | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // getEditor returns the user's preferred editor. | |
| 90 | + | func getEditor() string { | |
| 91 | + | // Check $EDITOR first | |
| 92 | + | if editor := os.Getenv("EDITOR"); editor != "" { | |
| 93 | + | return editor | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // Then $VISUAL | |
| 97 | + | if editor := os.Getenv("VISUAL"); editor != "" { | |
| 98 | + | return editor | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // Fallback to common editors | |
| 102 | + | fallbacks := []string{"nano", "vim", "vi"} | |
| 103 | + | for _, editor := range fallbacks { | |
| 104 | + | if path, err := exec.LookPath(editor); err == nil { | |
| 105 | + | return path | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | return "" | |
| 110 | + | } | |
| 111 | + | ||
| 50 | 112 | // runConfigStatus shows config file path and migration status. | |
| 51 | 113 | func runConfigStatus(_ *cobra.Command, _ []string) error { | |
| 52 | 114 | app := GetApp() | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "os" | 6 | "os" | ||
| 7 | + | "os/exec" | |||
| 7 | 8 | ||||
| 8 | "github.com/charmbracelet/bubbles/spinner" | 9 | "github.com/charmbracelet/bubbles/spinner" | ||
| 9 | tea "github.com/charmbracelet/bubbletea" | 10 | tea "github.com/charmbracelet/bubbletea" | ||
| @@ -24,6 +25,18 @@ var configCmd = &cobra.Command{ | |||||
| 24 | Long: `View configuration status and migrate to add new default settings.`, | 25 | Long: `View configuration status and migrate to add new default settings.`, | ||
| 25 | } | 26 | } | ||
| 26 | 27 | ||||
| 28 | + | var configOpenCmd = &cobra.Command{ | |||
| 29 | + | Use: "open", | |||
| 30 | + | Short: "Open config file in your editor", | |||
| 31 | + | Long: `Opens the config file using your preferred editor. | |||
| 32 | + | ||||
| 33 | + | Editor selection order: | |||
| 34 | + | 1. $EDITOR environment variable | |||
| 35 | + | 2. $VISUAL environment variable | |||
| 36 | + | 3. Fallback to: nano, vim, vi (first found)`, | |||
| 37 | + | RunE: runConfigOpen, | |||
| 38 | + | } | |||
| 39 | + | ||||
| 27 | var configStatusCmd = &cobra.Command{ | 40 | var configStatusCmd = &cobra.Command{ | ||
| 28 | Use: "status", | 41 | Use: "status", | ||
| 29 | Short: "Show config file status and migration availability", | 42 | Short: "Show config file status and migration availability", | ||
| @@ -42,11 +55,60 @@ Existing settings are never modified - only missing keys are added with default | |||||
| 42 | 55 | ||||
| 43 | func init() { | 56 | func init() { | ||
| 44 | rootCmd.AddCommand(configCmd) | 57 | rootCmd.AddCommand(configCmd) | ||
| 58 | + | configCmd.AddCommand(configOpenCmd) | |||
| 45 | configCmd.AddCommand(configStatusCmd) | 59 | configCmd.AddCommand(configStatusCmd) | ||
| 46 | configCmd.AddCommand(configMigrateCmd) | 60 | configCmd.AddCommand(configMigrateCmd) | ||
| 47 | configMigrateCmd.Flags().BoolVarP(&configYes, "yes", "y", false, "skip confirmation prompt") | 61 | configMigrateCmd.Flags().BoolVarP(&configYes, "yes", "y", false, "skip confirmation prompt") | ||
| 48 | } | 62 | } | ||
| 49 | 63 | ||||
| 64 | + | // runConfigOpen opens the config file in the user's preferred editor. | |||
| 65 | + | func runConfigOpen(_ *cobra.Command, _ []string) error { | |||
| 66 | + | configFile, err := config.GetConfigFile() | |||
| 67 | + | if err != nil { | |||
| 68 | + | return fmt.Errorf("get config file: %w", err) | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // Check if config file exists | |||
| 72 | + | if _, statErr := os.Stat(configFile); os.IsNotExist(statErr) { | |||
| 73 | + | return fmt.Errorf("config file does not exist: %s", configFile) | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | editor := getEditor() | |||
| 77 | + | if editor == "" { | |||
| 78 | + | return fmt.Errorf("no editor found: set $EDITOR or $VISUAL environment variable") | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | cmd := exec.Command(editor, configFile) | |||
| 82 | + | cmd.Stdin = os.Stdin | |||
| 83 | + | cmd.Stdout = os.Stdout | |||
| 84 | + | cmd.Stderr = os.Stderr | |||
| 85 | + | ||||
| 86 | + | return cmd.Run() | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // getEditor returns the user's preferred editor. | |||
| 90 | + | func getEditor() string { | |||
| 91 | + | // Check $EDITOR first | |||
| 92 | + | if editor := os.Getenv("EDITOR"); editor != "" { | |||
| 93 | + | return editor | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // Then $VISUAL | |||
| 97 | + | if editor := os.Getenv("VISUAL"); editor != "" { | |||
| 98 | + | return editor | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | // Fallback to common editors | |||
| 102 | + | fallbacks := []string{"nano", "vim", "vi"} | |||
| 103 | + | for _, editor := range fallbacks { | |||
| 104 | + | if path, err := exec.LookPath(editor); err == nil { | |||
| 105 | + | return path | |||
| 106 | + | } | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | return "" | |||
| 110 | + | } | |||
| 111 | + | ||||
| 50 | // runConfigStatus shows config file path and migration status. | 112 | // runConfigStatus shows config file path and migration status. | ||
| 51 | func runConfigStatus(_ *cobra.Command, _ []string) error { | 113 | func runConfigStatus(_ *cobra.Command, _ []string) error { | ||
| 52 | app := GetApp() | 114 | app := GetApp() | ||