diff --git a/AGENTS.md b/AGENTS.md index 88f2c2978..cf41563cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,14 +52,14 @@ build/ # CI build helpers Key paths inside `src/`: -| Path | Purpose | -| ----------------------------- | --------------------------------------------------- | -| `src/segments/` | One `.go` + one `_test.go` per segment | -| `src/config/segment_types.go` | Segment type registry (gob + string constants) | -| `src/cli/` | CLI commands (Cobra); `root.go` is the entry point | -| `src/prompt/engine.go` | Segment rendering loop | -| `src/cache/` | Existing TTL/file/command-path cache infrastructure | -| `src/runtime/` | `Environment` abstraction + mock | +| Path | Purpose | +| ------------------------------ | ----------------------------------------------------- | +| `src/segments/` | One `.go` + one `_test.go` per segment | +| `src/config/segment_types.go` | Segment type registry (gob + string constants) | +| `src/cli/` | CLI commands (cmdtree); `root.go` is the entry point | +| `src/prompt/engine.go` | Segment rendering loop | +| `src/cache/` | Existing TTL/file/command-path cache infrastructure | +| `src/runtime/` | `Environment` abstraction + mock | ## Segment Development @@ -97,10 +97,10 @@ Supported shells: `bash`, `zsh`, `fish`, `powershell`/`pwsh`, `cmd`, `nu`, `elvi ## CLI Commands -CLI commands use [Cobra](https://github.com/spf13/cobra) and live in `src/cli/`. To add a new +CLI commands use the internal `src/cmdtree` command tree and live in `src/cli/`. To add a new command: -1. Create `src/cli/.go` with a `var Cmd = &cobra.Command{...}` +1. Create `src/cli/.go` with a `var Cmd = &cmdtree.Command{...}` 2. Register it in `src/cli/root.go` via `RootCmd.AddCommand(Cmd)` ## Caching diff --git a/src/cli/args.go b/src/cli/args.go index 6fb9aec77..2191ad634 100644 --- a/src/cli/args.go +++ b/src/cli/args.go @@ -1,17 +1,17 @@ package cli import ( - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) -func NoArgsOrOneValidArg(cmd *cobra.Command, args []string) error { +func NoArgsOrOneValidArg(cmd *cmdtree.Command, args []string) error { if len(args) == 0 { return nil } - if err := cobra.ExactArgs(1)(cmd, args); err != nil { + if err := cmdtree.ExactArgs(1)(cmd, args); err != nil { return err } - return cobra.OnlyValidArgs(cmd, args) + return cmdtree.OnlyValidArgs(cmd, args) } diff --git a/src/cli/auth.go b/src/cli/auth.go index 4d42ec2e6..a20863ec6 100644 --- a/src/cli/auth.go +++ b/src/cli/auth.go @@ -8,10 +8,10 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) -var authCmd = &cobra.Command{ +var authCmd = &cmdtree.Command{ Use: "auth [service]", Short: "Authenticate against a service", Long: `Authenticate against a service. @@ -25,7 +25,7 @@ Available services: "ytmda", }, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/cache.go b/src/cli/cache.go index 80e9031f1..8f8331a91 100644 --- a/src/cli/cache.go +++ b/src/cli/cache.go @@ -7,14 +7,14 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( session bool ) -var cacheCmd = &cobra.Command{ +var cacheCmd = &cmdtree.Command{ Use: "cache [path|clear|ttl|show]", Short: "Interact with the oh-my-posh cache", Long: `Interact with the oh-my-posh cache. @@ -31,8 +31,8 @@ You can do the following: cache.TTL, "show", }, - Args: cobra.RangeArgs(1, 2), - Run: func(cmd *cobra.Command, args []string) { + Args: cmdtree.RangeArgs(1, 2), + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/claude.go b/src/cli/claude.go index cb3b307de..63f2d09f4 100644 --- a/src/cli/claude.go +++ b/src/cli/claude.go @@ -6,10 +6,10 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/shell" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) -var claudeCmd = &cobra.Command{ +var claudeCmd = &cmdtree.Command{ Use: "claude", Short: "Render a prompt for Claude Code statusline", Long: `Render a prompt for Claude Code statusline integration. @@ -22,7 +22,7 @@ Example usage in Claude Code settings: "statusLine": { "command": "oh-my-posh claude --config ~/.config/ohmyposh/claude.toml" }`, - Args: cobra.NoArgs, + Args: cmdtree.NoArgs, Run: statuslineRun[segments.ClaudeData]( shell.CLAUDE, cache.CLAUDECACHE, diff --git a/src/cli/config.go b/src/cli/config.go index c4545063c..a21c93a0d 100644 --- a/src/cli/config.go +++ b/src/cli/config.go @@ -6,12 +6,12 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/dsc" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/jandedobbeleer/oh-my-posh/src/config" basedsc "github.com/jandedobbeleer/oh-my-posh/src/dsc" - "github.com/spf13/cobra" ) -var configCmd = &cobra.Command{ +var configCmd = &cmdtree.Command{ Use: "config edit", Short: "Interact with the config", Long: `Interact with the config. @@ -21,7 +21,7 @@ You can export or edit the config (via the editor specified in the environment v "edit", }, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/config_export.go b/src/cli/config_export.go index 74e3baae6..954d0fc35 100644 --- a/src/cli/config_export.go +++ b/src/cli/config_export.go @@ -11,7 +11,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( @@ -19,7 +19,7 @@ var ( output string ) -var exportCmd = &cobra.Command{ +var exportCmd = &cmdtree.Command{ Use: "export", Short: "Export your config", Long: `Export your config. @@ -35,8 +35,8 @@ Exports the config file "~/myconfig.omp.json" in TOML format and prints the resu > oh-my-posh config export --output ~/new_config.omp.json Exports the current config to "~/new_config.omp.json" (in JSON format).`, - Args: cobra.NoArgs, - Run: func(_ *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(_ *cmdtree.Command, _ []string) { if output == "" && format == "" { // usage error fmt.Println("neither output path nor export format is specified") diff --git a/src/cli/config_export_data.go b/src/cli/config_export_data.go index 823265025..1c9ae2fe1 100644 --- a/src/cli/config_export_data.go +++ b/src/cli/config_export_data.go @@ -11,7 +11,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/template" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( @@ -20,7 +20,7 @@ var ( themesDir string ) -var dataCmd = &cobra.Command{ +var dataCmd = &cmdtree.Command{ Use: "data", Short: "Export a template data file for your config", Long: `Export a template data file for your config. @@ -47,8 +47,8 @@ them into a single sanitized fixture (the most common recorded value per segment key wins), and writes it to the given output path. --config is ignored in this mode. This is the single command that regenerates src/prompt/testdata/fixtures - run from src/.`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(cmd *cmdtree.Command, _ []string) { cache.Init(os.Getenv("POSH_SHELL")) if themesDir != "" { diff --git a/src/cli/config_export_image.go b/src/cli/config_export_image.go index ea80ba8d1..b763a4eb4 100644 --- a/src/cli/config_export_image.go +++ b/src/cli/config_export_image.go @@ -11,7 +11,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( @@ -25,7 +25,7 @@ var ( svgBackgroundColor string ) -var imageCmd = &cobra.Command{ +var imageCmd = &cmdtree.Command{ Use: "image", Short: "Export your config to an SVG image", Long: `Export your config to an SVG image. @@ -70,8 +70,8 @@ Exports the config to an image file called myconfig.svg in the current working d > oh-my-posh config export image --config ~/myconfig.omp.json --output ~/mytheme.svg Exports the config to an image file ~/mytheme.svg.`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(cmd *cmdtree.Command, _ []string) { cache.Init(os.Getenv("POSH_SHELL")) setConfigFlag() diff --git a/src/cli/copilot.go b/src/cli/copilot.go index 079ae169a..9541df7a5 100644 --- a/src/cli/copilot.go +++ b/src/cli/copilot.go @@ -6,12 +6,12 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/shell" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) const copilotServiceName = "copilot" -var copilotCmd = &cobra.Command{ +var copilotCmd = &cmdtree.Command{ Use: copilotServiceName, Short: "Render a prompt for GitHub Copilot CLI statusline", Long: `Render a prompt for GitHub Copilot CLI statusline integration. @@ -23,7 +23,7 @@ model name, token usage, costs, and more. Example usage in GitHub Copilot CLI settings (%USERPROFILE%\.copilot\statusline.cmd): @echo off oh-my-posh copilot --config %USERPROFILE%\.config\ohmyposh\copilot.toml`, - Args: cobra.NoArgs, + Args: cmdtree.NoArgs, Run: statuslineRun( shell.COPILOTCLI, cache.COPILOTCLICACHE, diff --git a/src/cli/data_test.go b/src/cli/data_test.go index fda5bed1a..3582a7635 100644 --- a/src/cli/data_test.go +++ b/src/cli/data_test.go @@ -10,7 +10,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -291,7 +291,7 @@ func TestApplyDataFile_DataDeriveIsNoopOnAnUnmarkedFile(t *testing.T) { } func TestPrintAndImageCmd_DataDeriveFlagRegistered(t *testing.T) { - for _, cmd := range []*cobra.Command{printCmd, imageCmd} { + for _, cmd := range []*cmdtree.Command{printCmd, imageCmd} { flag := cmd.Flags().Lookup("data-derive") require.NotNil(t, flag, "%s must register --data-derive", cmd.Use) assert.Equal(t, "false", flag.DefValue) diff --git a/src/cli/debug.go b/src/cli/debug.go index 964a022d5..bd76ebe52 100644 --- a/src/cli/debug.go +++ b/src/cli/debug.go @@ -15,7 +15,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( @@ -27,12 +27,12 @@ func init() { RootCmd.AddCommand(debugCmd) } -func createDebugCmd() *cobra.Command { - debugCmd := &cobra.Command{ +func createDebugCmd() *cmdtree.Command { + debugCmd := &cmdtree.Command{ Use: "debug", Short: "Print the prompt in debug mode", Long: "Print the prompt in debug mode.", - Run: func(_ *cobra.Command, _ []string) { + Run: func(_ *cmdtree.Command, _ []string) { startTime := time.Now() log.Enable(plain) diff --git a/src/cli/disable.go b/src/cli/disable.go index b0194ad13..dc6afa742 100644 --- a/src/cli/disable.go +++ b/src/cli/disable.go @@ -3,16 +3,16 @@ package cli import ( "fmt" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) -var disableCmd = &cobra.Command{ +var disableCmd = &cmdtree.Command{ Use: fmt.Sprintf(toggleUse, "disable"), Short: "Disable a feature", Long: fmt.Sprintf(toggleLong, "Disable"), ValidArgs: toggleArgs, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/enable.go b/src/cli/enable.go index af95ad0b1..bc65aea66 100644 --- a/src/cli/enable.go +++ b/src/cli/enable.go @@ -8,7 +8,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/config" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( @@ -23,13 +23,13 @@ var ( toggleLong = strings.Join(append([]string{toggleHelpText}, toggleArgs...), "\n- ") ) -var enableCmd = &cobra.Command{ +var enableCmd = &cmdtree.Command{ Use: fmt.Sprintf(toggleUse, "enable"), Short: "Enable a feature", Long: fmt.Sprintf(toggleLong, "Enable"), ValidArgs: toggleArgs, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return @@ -42,7 +42,7 @@ func init() { RootCmd.AddCommand(enableCmd) } -func toggleFeature(cmd *cobra.Command, feature string, enable bool) { +func toggleFeature(cmd *cmdtree.Command, feature string, enable bool) { if feature == "" { _ = cmd.Help() return diff --git a/src/cli/font.go b/src/cli/font.go index 366d0849d..b092535b6 100644 --- a/src/cli/font.go +++ b/src/cli/font.go @@ -12,13 +12,13 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/terminal" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( zipFolder string - fontCmd = &cobra.Command{ + fontCmd = &cmdtree.Command{ Use: "font", Short: "Manage fonts", Long: `Manage fonts. @@ -29,7 +29,7 @@ List the available Nerd Fonts and install one: oh-my-posh font install Meslo`, } - fontListCmd = &cobra.Command{ + fontListCmd = &cmdtree.Command{ Use: "list", Short: "List the available Nerd Fonts", Long: `List the available Nerd Fonts. @@ -37,8 +37,8 @@ List the available Nerd Fonts and install one: Prints one font name per line, so it can be searched or piped: oh-my-posh font list | grep -i mono`, - Args: cobra.NoArgs, - Run: func(_ *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(_ *cmdtree.Command, _ []string) { fonts, err := font.List() if err != nil { log.Error(err) @@ -53,7 +53,7 @@ Prints one font name per line, so it can be searched or piped: }, } - fontInstallCmd = &cobra.Command{ + fontInstallCmd = &cmdtree.Command{ Use: "install ", Short: "Install a Nerd Font", Long: `Install a Nerd Font. @@ -63,8 +63,8 @@ Takes a font name from ` + "`oh-my-posh font list`" + `, a URL, or the path to a oh-my-posh font install Meslo oh-my-posh font install https://example.com/font.zip oh-my-posh font install ./CascadiaCode.zip`, - Args: cobra.ExactArgs(1), - Run: func(_ *cobra.Command, args []string) { + Args: cmdtree.ExactArgs(1), + Run: func(_ *cmdtree.Command, args []string) { env := &runtime.Terminal{} env.Init(&runtime.Flags{}) diff --git a/src/cli/font/sfnt.go b/src/cli/font/nametable.go similarity index 100% rename from src/cli/font/sfnt.go rename to src/cli/font/nametable.go diff --git a/src/cli/font/sfnt_test.go b/src/cli/font/nametable_test.go similarity index 100% rename from src/cli/font/sfnt_test.go rename to src/cli/font/nametable_test.go diff --git a/src/cli/get.go b/src/cli/get.go index 42b2c5904..5b250e87d 100644 --- a/src/cli/get.go +++ b/src/cli/get.go @@ -10,10 +10,10 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/runtime" color2 "github.com/gookit/color" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) -var getCmd = &cobra.Command{ +var getCmd = &cmdtree.Command{ Use: "get [shell|millis|accent|toggles|width]", Short: "Get a value from oh-my-posh", Long: `Get a value from oh-my-posh. @@ -34,7 +34,7 @@ This command is used to get the value of the following variables: cache.TTL, }, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/init.go b/src/cli/init.go index f076d1ef0..84b91a773 100644 --- a/src/cli/init.go +++ b/src/cli/init.go @@ -8,14 +8,14 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/dsc" + "github.com/jandedobbeleer/oh-my-posh/src/cmdflag" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/runtime/path" "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" - "github.com/spf13/cobra" - "github.com/spf13/pflag" ) var ( @@ -43,8 +43,8 @@ func init() { RootCmd.AddCommand(initCmd) } -func createInitCmd() *cobra.Command { - initCmd := &cobra.Command{ +func createInitCmd() *cmdtree.Command { + initCmd := &cmdtree.Command{ Use: "init [bash|zsh|fish|powershell|pwsh|cmd|nu|elvish|xonsh|yash]", Short: "Initialize your shell and config", Long: `Initialize your shell and config. @@ -52,7 +52,7 @@ func createInitCmd() *cobra.Command { See the documentation to initialize your shell: https://ohmyposh.dev/docs/installation/prompt.`, ValidArgs: supportedShells, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return @@ -143,7 +143,7 @@ func runInit(sh, command string) { fmt.Print(output) } -func getFullCommand(cmd *cobra.Command, args []string) string { +func getFullCommand(cmd *cmdtree.Command, args []string) string { // Start with the command path cmdPath := cmd.CommandPath() @@ -153,7 +153,7 @@ func getFullCommand(cmd *cobra.Command, args []string) string { } // Add flags that were actually set - cmd.Flags().VisitAll(func(flag *pflag.Flag) { + cmd.Flags().VisitAll(func(flag *cmdflag.Flag) { if !flag.Changed { return } diff --git a/src/cli/notice.go b/src/cli/notice.go index b20fd9f0f..c5f410b7c 100644 --- a/src/cli/notice.go +++ b/src/cli/notice.go @@ -6,17 +6,17 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/runtime" - "github.com/spf13/cobra" ) -var noticeCmd = &cobra.Command{ +var noticeCmd = &cmdtree.Command{ Use: "notice", Short: "Print the upgrade notice when a new version is available.", Long: "Print the upgrade notice when a new version is available.", - Args: cobra.NoArgs, - Run: func(_ *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(_ *cmdtree.Command, _ []string) { env := &runtime.Terminal{} env.Init(&runtime.Flags{}) diff --git a/src/cli/print.go b/src/cli/print.go index b4f31b2f9..368719c44 100644 --- a/src/cli/print.go +++ b/src/cli/print.go @@ -9,7 +9,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( @@ -40,8 +40,8 @@ func init() { RootCmd.AddCommand(printCmd) } -func createPrintCmd() *cobra.Command { - printCmd := &cobra.Command{ +func createPrintCmd() *cmdtree.Command { + printCmd := &cmdtree.Command{ Use: "print [debug|primary|secondary|transient|transient-right|right|tooltip|valid|error|preview]", Short: "Print the prompt/context", Long: "Print one of the prompts based on the location/use-case.", @@ -58,7 +58,7 @@ func createPrintCmd() *cobra.Command { prompt.PREVIEW, }, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/root.go b/src/cli/root.go index ff7bc8dcf..3766ddbd5 100644 --- a/src/cli/root.go +++ b/src/cli/root.go @@ -9,8 +9,8 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/build" "github.com/jandedobbeleer/oh-my-posh/src/cache" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/jandedobbeleer/oh-my-posh/src/log" - "github.com/spf13/cobra" ) var ( @@ -27,14 +27,14 @@ var ( initialize bool ) -var RootCmd = &cobra.Command{ +var RootCmd = &cmdtree.Command{ Use: "oh-my-posh", Short: "oh-my-posh is a tool to render your prompt", Long: `oh-my-posh is a cross platform tool to render your prompt. It can use the same configuration everywhere to offer a consistent experience, regardless of where you are. For a detailed guide on getting started, have a look at the docs at https://ohmyposh.dev`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if initialize { runInit(strings.ToLower(shellName), getFullCommand(cmd, args)) return @@ -47,7 +47,7 @@ on getting started, have a look at the docs at https://ohmyposh.dev`, _ = cmd.Help() }, - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRun: func(cmd *cmdtree.Command, args []string) { configEnv := os.Getenv("POSH_CONFIG") if configEnv != "" && configFlag == "" { configFlag = configEnv @@ -65,7 +65,7 @@ on getting started, have a look at the docs at https://ohmyposh.dev`, log.Debug("version:", build.Version) log.Debug("command:", getFullCommand(cmd, args)) }, - PersistentPostRun: func(cmd *cobra.Command, args []string) { + PersistentPostRun: func(cmd *cmdtree.Command, args []string) { defer func() { if exitcode != 0 { os.Exit(exitcode) @@ -104,7 +104,7 @@ func Execute() { // of milliseconds per prompt. Explorer never passes arguments, so the // check is only needed when there are none. if len(os.Args) > 1 { - cobra.MousetrapHelpText = "" + cmdtree.ExplorerLaunchHelpText = "" } if err := RootCmd.Execute(); err != nil { diff --git a/src/cli/serve.go b/src/cli/serve.go index df75e95ca..647bdbf20 100644 --- a/src/cli/serve.go +++ b/src/cli/serve.go @@ -13,7 +13,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) // Unix only - used by shells that cannot hold a child's stdin open across @@ -66,13 +66,13 @@ const ( // stdout: "\x1f\x00". \x1f is the ASCII unit separator. const serveIDMarker = "\x1f" -func createServeCmd() *cobra.Command { - serveCmd := &cobra.Command{ +func createServeCmd() *cmdtree.Command { + serveCmd := &cmdtree.Command{ Use: "serve", Short: "Start a persistent prompt server that streams prompt updates over stdio", Hidden: true, - Args: cobra.NoArgs, - Run: func(_ *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(_ *cmdtree.Command, _ []string) { if shellName == "" { shellName = shell.GENERIC } diff --git a/src/cli/shell.go b/src/cli/shell.go index 5694fe432..01e9025c9 100644 --- a/src/cli/shell.go +++ b/src/cli/shell.go @@ -5,12 +5,12 @@ import ( "os" "github.com/jandedobbeleer/oh-my-posh/src/cli/dsc" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" basedsc "github.com/jandedobbeleer/oh-my-posh/src/dsc" "github.com/jandedobbeleer/oh-my-posh/src/runtime" - "github.com/spf13/cobra" ) -var shellCmd = &cobra.Command{ +var shellCmd = &cmdtree.Command{ Use: "shell get", Short: "Get the shell name", Long: `Get the shell name. @@ -21,7 +21,7 @@ This command retrieves the name of the current shell being used.`, "get", }, Args: NoArgsOrOneValidArg, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/statusline.go b/src/cli/statusline.go index c26d49609..fb39d48ae 100644 --- a/src/cli/statusline.go +++ b/src/cli/statusline.go @@ -14,7 +14,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/terminal" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) // statuslineRun returns a cobra Run function for statusline commands. @@ -23,8 +23,8 @@ import ( // cacheKey is the session cache key under which data is stored. // sessionID extracts the session ID from parsed data so it can be set as POSH_SESSION_ID. // defaultCfg is called when no --config flag is provided or parsing fails. -func statuslineRun[T any](shellConst, cacheKey string, sessionID func(*T) string, defaultCfg func() *config.Config) func(*cobra.Command, []string) { - return func(cmd *cobra.Command, _ []string) { +func statuslineRun[T any](shellConst, cacheKey string, sessionID func(*T) string, defaultCfg func() *config.Config) func(*cmdtree.Command, []string) { + return func(cmd *cmdtree.Command, _ []string) { log.Debugf("%s command started", shellConst) stdinData, err := io.ReadAll(os.Stdin) diff --git a/src/cli/stream.go b/src/cli/stream.go index aa666ab21..53952e55c 100644 --- a/src/cli/stream.go +++ b/src/cli/stream.go @@ -9,7 +9,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/shell" "github.com/jandedobbeleer/oh-my-posh/src/template" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var streamCmd = createStreamCmd() @@ -18,8 +18,8 @@ func init() { RootCmd.AddCommand(streamCmd) } -func createStreamCmd() *cobra.Command { - streamCmd := &cobra.Command{ +func createStreamCmd() *cmdtree.Command { + streamCmd := &cmdtree.Command{ Use: "stream", Short: "Stream the prompt with incremental updates", Long: `Stream the primary prompt with incremental updates as segments complete. @@ -29,8 +29,8 @@ Records prefixed with a record separator byte (\x1e) carry the transient prompt, which shells cache so no additional CLI call is needed on line acceptance. The shell can read records incrementally and update the display. Command exits when all segments are resolved.`, - Args: cobra.NoArgs, - Run: func(_ *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(_ *cmdtree.Command, _ []string) { if shellName == "" { shellName = shell.GENERIC } diff --git a/src/cli/toggle.go b/src/cli/toggle.go index 91b7f9620..e696a0ee4 100644 --- a/src/cli/toggle.go +++ b/src/cli/toggle.go @@ -5,16 +5,16 @@ import ( "strings" "github.com/jandedobbeleer/oh-my-posh/src/cache" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/jandedobbeleer/oh-my-posh/src/runtime" - "github.com/spf13/cobra" ) -var toggleCmd = &cobra.Command{ +var toggleCmd = &cmdtree.Command{ Use: "toggle segment1 segment2 ...", Short: "Toggle one or more segments on/off", Long: "Toggle one or more segments on/off on the fly. Multiple segments can be specified separated by spaces.", - Args: cobra.MinimumNArgs(1), - Run: func(cmd *cobra.Command, args []string) { + Args: cmdtree.MinimumNArgs(1), + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/cli/upgrade.go b/src/cli/upgrade.go index ca9fac302..2f04c2ef1 100644 --- a/src/cli/upgrade.go +++ b/src/cli/upgrade.go @@ -11,12 +11,12 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/cache" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade" "github.com/jandedobbeleer/oh-my-posh/src/cli/upgrade/tui" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/jandedobbeleer/oh-my-posh/src/config" "github.com/jandedobbeleer/oh-my-posh/src/log" "github.com/jandedobbeleer/oh-my-posh/src/runtime" "github.com/jandedobbeleer/oh-my-posh/src/terminal" "github.com/jandedobbeleer/oh-my-posh/src/text" - "github.com/spf13/cobra" ) var ( @@ -24,12 +24,12 @@ var ( auto bool ) -var upgradeCmd = &cobra.Command{ +var upgradeCmd = &cmdtree.Command{ Use: "upgrade", Short: "Upgrade when a new version is available.", Long: "Upgrade when a new version is available.", - Args: cobra.NoArgs, - Run: func(_ *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(_ *cmdtree.Command, _ []string) { var startTime time.Time if debug { diff --git a/src/cli/version.go b/src/cli/version.go index bad627ee4..bca24188d 100644 --- a/src/cli/version.go +++ b/src/cli/version.go @@ -4,19 +4,19 @@ import ( "fmt" "github.com/jandedobbeleer/oh-my-posh/src/build" - "github.com/spf13/cobra" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" ) var ( verbose bool ) -var versionCmd = &cobra.Command{ +var versionCmd = &cmdtree.Command{ Use: "version", Short: "Print the version", Long: "Print the version number of oh-my-posh.", - Args: cobra.NoArgs, - Run: func(_ *cobra.Command, _ []string) { + Args: cmdtree.NoArgs, + Run: func(_ *cmdtree.Command, _ []string) { if !verbose { fmt.Println(build.Version) return diff --git a/src/cmdflag/cmdflag.go b/src/cmdflag/cmdflag.go new file mode 100644 index 000000000..82f2bac19 --- /dev/null +++ b/src/cmdflag/cmdflag.go @@ -0,0 +1,448 @@ +// Package cmdflag is a minimal POSIX-style command-line flag parser covering +// the API surface oh-my-posh uses: typed Var registration, POSIX-style +// parsing (--flag=value, --flag value, shorthands, the -- terminator, +// interspersed positionals), hidden flags, and usage rendering matching +// common CLI flag conventions byte for byte for the flag types in use. +package cmdflag + +import ( + "fmt" + "strconv" + "strings" +) + +type ErrorHandling int + +const ( + ContinueOnError ErrorHandling = iota + ExitOnError +) + +const ( + boolType = "bool" + trueStr = "true" +) + +// Value is the interface to the dynamic value stored in a flag. +type Value interface { + String() string + Set(string) error + Type() string +} + +type Flag struct { + Name string + Shorthand string + Usage string + Value Value + DefValue string + Hidden bool + Changed bool +} + +type FlagSet struct { + name string + flags map[string]*Flag + shorthands map[byte]*Flag + order []*Flag + args []string + + ParseErrorsAllowlist struct { + UnknownFlags bool + } +} + +func NewFlagSet(name string, _ ErrorHandling) *FlagSet { + return &FlagSet{ + name: name, + flags: make(map[string]*Flag), + shorthands: make(map[byte]*Flag), + } +} + +// value implementations + +type stringValue struct{ p *string } + +func (v stringValue) String() string { return *v.p } +func (v stringValue) Set(s string) error { *v.p = s; return nil } +func (v stringValue) Type() string { return "string" } + +type boolValue struct{ p *bool } + +func (v boolValue) String() string { return strconv.FormatBool(*v.p) } +func (v boolValue) Set(s string) error { + b, err := strconv.ParseBool(s) + *v.p = b + return err +} +func (v boolValue) Type() string { return boolType } + +type intValue struct{ p *int } + +func (v intValue) String() string { return strconv.Itoa(*v.p) } +func (v intValue) Set(s string) error { + i, err := strconv.ParseInt(s, 0, 64) + *v.p = int(i) + return err +} +func (v intValue) Type() string { return "int" } + +type float64Value struct{ p *float64 } + +func (v float64Value) String() string { return strconv.FormatFloat(*v.p, 'g', -1, 64) } +func (v float64Value) Set(s string) error { + f, err := strconv.ParseFloat(s, 64) + *v.p = f + return err +} +func (v float64Value) Type() string { return "float64" } + +// registration + +func (f *FlagSet) Var(value Value, name, shorthand, usage string) *Flag { + flag := &Flag{ + Name: name, + Shorthand: shorthand, + Usage: usage, + Value: value, + DefValue: value.String(), + } + + f.flags[name] = flag + f.order = append(f.order, flag) + + if shorthand != "" { + f.shorthands[shorthand[0]] = flag + } + + return flag +} + +func (f *FlagSet) StringVar(p *string, name, value, usage string) { + f.StringVarP(p, name, "", value, usage) +} + +func (f *FlagSet) StringVarP(p *string, name, shorthand, value, usage string) { + *p = value + f.Var(stringValue{p}, name, shorthand, usage) +} + +func (f *FlagSet) String(name, value, usage string) *string { + p := new(string) + f.StringVar(p, name, value, usage) + return p +} + +func (f *FlagSet) BoolVar(p *bool, name string, value bool, usage string) { + f.BoolVarP(p, name, "", value, usage) +} + +func (f *FlagSet) BoolVarP(p *bool, name, shorthand string, value bool, usage string) { + *p = value + f.Var(boolValue{p}, name, shorthand, usage) +} + +func (f *FlagSet) IntVar(p *int, name string, value int, usage string) { + f.IntVarP(p, name, "", value, usage) +} + +func (f *FlagSet) IntVarP(p *int, name, shorthand string, value int, usage string) { + *p = value + f.Var(intValue{p}, name, shorthand, usage) +} + +func (f *FlagSet) Float64Var(p *float64, name string, value float64, usage string) { + *p = value + f.Var(float64Value{p}, name, "", usage) +} + +// inspection + +func (f *FlagSet) Lookup(name string) *Flag { + return f.flags[name] +} + +func (f *FlagSet) ShorthandLookup(name string) *Flag { + if name == "" { + return nil + } + + return f.shorthands[name[0]] +} + +func (f *FlagSet) Changed(name string) bool { + flag := f.flags[name] + return flag != nil && flag.Changed +} + +func (f *FlagSet) MarkHidden(name string) error { + flag := f.flags[name] + if flag == nil { + return fmt.Errorf("flag %q does not exist", name) + } + + flag.Hidden = true + return nil +} + +// VisitAll visits the flags in registration order rather than +// lexicographically: the sole caller formats a command line and is +// order-insensitive, and usage rendering sorts separately. +func (f *FlagSet) VisitAll(fn func(*Flag)) { + for _, flag := range f.order { + fn(flag) + } +} + +// AddFlagSet adds flags from another set that are not yet present. +func (f *FlagSet) AddFlagSet(other *FlagSet) { + if other == nil { + return + } + + other.VisitAll(func(flag *Flag) { + if f.flags[flag.Name] == nil { + f.flags[flag.Name] = flag + f.order = append(f.order, flag) + + if flag.Shorthand != "" && f.shorthands[flag.Shorthand[0]] == nil { + f.shorthands[flag.Shorthand[0]] = flag + } + } + }) +} + +func (f *FlagSet) HasFlags() bool { + return len(f.order) > 0 +} + +func (f *FlagSet) HasAvailableFlags() bool { + for _, flag := range f.order { + if !flag.Hidden { + return true + } + } + + return false +} + +func (f *FlagSet) Args() []string { + return f.args +} + +// parsing + +func (f *FlagSet) Parse(arguments []string) error { + f.args = make([]string, 0, len(arguments)) + + for len(arguments) > 0 { + arg := arguments[0] + arguments = arguments[1:] + + switch { + case arg == "--": + f.args = append(f.args, arguments...) + return nil + case strings.HasPrefix(arg, "--"): + var err error + arguments, err = f.parseLong(arg[2:], arguments) + if err != nil { + return err + } + case strings.HasPrefix(arg, "-") && len(arg) > 1: + var err error + arguments, err = f.parseShort(arg[1:], arguments) + if err != nil { + return err + } + default: + f.args = append(f.args, arg) + } + } + + return nil +} + +func (f *FlagSet) parseLong(name string, rest []string) ([]string, error) { + value := "" + hasValue := false + + if i := strings.Index(name, "="); i >= 0 { + value = name[i+1:] + name = name[:i] + hasValue = true + } + + flag := f.flags[name] + if flag == nil { + if !f.ParseErrorsAllowlist.UnknownFlags { + return rest, fmt.Errorf("unknown flag: --%s", name) + } + + // an unknown flag given as "--flag value" swallows the + // value token unless the next token is itself a flag + if !hasValue && len(rest) > 0 && !strings.HasPrefix(rest[0], "-") { + return rest[1:], nil + } + + return rest, nil + } + + switch { + case hasValue: + case flag.Value.Type() == boolType: + value = trueStr + case len(rest) > 0: + value = rest[0] + rest = rest[1:] + default: + return rest, fmt.Errorf("flag needs an argument: --%s", name) + } + + if err := flag.Value.Set(value); err != nil { + return rest, fmt.Errorf("invalid argument %q for \"--%s\" flag: %v", value, flag.Name, err) + } + + flag.Changed = true + return rest, nil +} + +func (f *FlagSet) parseShort(shorthands string, rest []string) ([]string, error) { + for len(shorthands) > 0 { + c := shorthands[0] + + flag := f.shorthands[c] + if flag == nil { + if f.ParseErrorsAllowlist.UnknownFlags { + // drop the remainder of the group and a + // separate value token unless it is itself a flag + if len(shorthands) == 1 && len(rest) > 0 && !strings.HasPrefix(rest[0], "-") { + return rest[1:], nil + } + + return rest, nil + } + + return rest, fmt.Errorf("unknown shorthand flag: %q in -%s", c, shorthands) + } + + value := "" + + switch { + case len(shorthands) > 2 && shorthands[1] == '=': + value = shorthands[2:] + shorthands = "" + case flag.Value.Type() == boolType: + value = trueStr + shorthands = shorthands[1:] + case len(shorthands) > 1: + value = shorthands[1:] + shorthands = "" + case len(rest) > 0: + value = rest[0] + rest = rest[1:] + shorthands = "" + default: + return rest, fmt.Errorf("flag needs an argument: %q in -%s", c, shorthands) + } + + if err := flag.Value.Set(value); err != nil { + return rest, fmt.Errorf("invalid argument %q for \"-%s, --%s\" flag: %v", value, flag.Shorthand, flag.Name, err) + } + + flag.Changed = true + } + + return rest, nil +} + +// usage rendering + +// FlagUsages renders the non-hidden flags sorted by name, shorthand column, +// type name after the flag, defaults in parentheses when they differ from +// the zero value, usage aligned in a single column. +func (f *FlagSet) FlagUsages() string { + lines := make([]string, 0, len(f.order)) + maxlen := 0 + + flags := make([]*Flag, len(f.order)) + copy(flags, f.order) + sortFlags(flags) + + for _, flag := range flags { + if flag.Hidden { + continue + } + + var line string + if flag.Shorthand != "" { + line = fmt.Sprintf(" -%s, --%s", flag.Shorthand, flag.Name) + } + + if flag.Shorthand == "" { + line = fmt.Sprintf(" --%s", flag.Name) + } + + if name := typeName(flag); name != "" { + line += " " + name + } + + line += "\x00" + if len(line) > maxlen { + maxlen = len(line) + } + + line += flag.Usage + if defValue, ok := defaultValue(flag); ok { + line += fmt.Sprintf(" (default %s)", defValue) + } + + lines = append(lines, line) + } + + var sb strings.Builder + for _, line := range lines { + sidx := strings.Index(line, "\x00") + sb.WriteString(line[:sidx]) + // the gap is maxlen-sidx+2 spaces wide to match the implicit + // separators of a Fprintln(left, spacing, usage)-style layout + sb.WriteString(strings.Repeat(" ", maxlen-sidx+2)) + sb.WriteString(line[sidx+1:]) + sb.WriteString("\n") + } + + return sb.String() +} + +func sortFlags(flags []*Flag) { + for i := 1; i < len(flags); i++ { + for j := i; j > 0 && flags[j].Name < flags[j-1].Name; j-- { + flags[j], flags[j-1] = flags[j-1], flags[j] + } + } +} + +func typeName(flag *Flag) string { + switch flag.Value.Type() { + case boolType: + return "" + case "float64": + return "float" + case "int64": + return "int" + default: + return flag.Value.Type() + } +} + +func defaultValue(flag *Flag) (string, bool) { + switch flag.Value.Type() { + case boolType: + return flag.DefValue, flag.DefValue == trueStr + case "string": + return fmt.Sprintf("%q", flag.DefValue), flag.DefValue != "" + default: + return flag.DefValue, flag.DefValue != "0" + } +} diff --git a/src/cmdflag/cmdflag_test.go b/src/cmdflag/cmdflag_test.go new file mode 100644 index 000000000..6793d0c24 --- /dev/null +++ b/src/cmdflag/cmdflag_test.go @@ -0,0 +1,107 @@ +package cmdflag + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseStyles(t *testing.T) { + cases := []struct { + Case string + Expected string + Args []string + Rest []string + Number int + Bool bool + }{ + {Case: "long with space", Args: []string{"--name", "x"}, Expected: "x", Rest: []string{}}, + {Case: "long with equals", Args: []string{"--name=x"}, Expected: "x", Rest: []string{}}, + {Case: "shorthand with space", Args: []string{"-n", "x"}, Expected: "x", Rest: []string{}}, + {Case: "shorthand joined", Args: []string{"-nx"}, Expected: "x", Rest: []string{}}, + {Case: "shorthand with equals", Args: []string{"-n=x"}, Expected: "x", Rest: []string{}}, + {Case: "bool does not eat value", Args: []string{"--flag", "positional"}, Bool: true, Rest: []string{"positional"}}, + {Case: "terminator", Args: []string{"--", "--name", "x"}, Rest: []string{"--name", "x"}}, + {Case: "interspersed", Args: []string{"a", "--name", "x", "b"}, Expected: "x", Rest: []string{"a", "b"}}, + {Case: "int value", Args: []string{"--count", "42"}, Number: 42, Rest: []string{}}, + {Case: "combined bool shorthands", Args: []string{"-fv"}, Bool: true, Rest: []string{}}, + } + + for _, tc := range cases { + var name string + var flag, verbose bool + var count int + + fs := NewFlagSet("test", ContinueOnError) + fs.StringVarP(&name, "name", "n", "", "") + fs.BoolVarP(&flag, "flag", "f", false, "") + fs.BoolVarP(&verbose, "verbose", "v", false, "") + fs.IntVar(&count, "count", 0, "") + + err := fs.Parse(tc.Args) + assert.NoError(t, err, tc.Case) + assert.Equal(t, tc.Expected, name, tc.Case) + assert.Equal(t, tc.Bool, flag, tc.Case) + assert.Equal(t, tc.Number, count, tc.Case) + assert.Equal(t, tc.Rest, fs.Args(), tc.Case) + } +} + +func TestParseErrors(t *testing.T) { + fs := NewFlagSet("test", ContinueOnError) + var name string + fs.StringVar(&name, "name", "", "") + + err := fs.Parse([]string{"--unknown"}) + assert.EqualError(t, err, "unknown flag: --unknown") + + err = fs.Parse([]string{"-x"}) + assert.EqualError(t, err, `unknown shorthand flag: 'x' in -x`) + + err = fs.Parse([]string{"--name"}) + assert.EqualError(t, err, "flag needs an argument: --name") +} + +func TestUnknownFlagsAllowlist(t *testing.T) { + // the argocd segment parses ARGOCD_OPTS this way + fs := NewFlagSet("test", ContinueOnError) + fs.ParseErrorsAllowlist.UnknownFlags = true + fs.String("config", "", "") + + err := fs.Parse([]string{"--grpc-web", "--server", "foo.com", "--config", "x", "--insecure"}) + assert.NoError(t, err) + assert.Equal(t, "x", fs.Lookup("config").Value.String()) +} + +func TestChangedAndDefaults(t *testing.T) { + fs := NewFlagSet("test", ContinueOnError) + var escape bool + fs.BoolVar(&escape, "escape", true, "escape the output") + + assert.False(t, fs.Changed("escape")) + assert.NoError(t, fs.Parse([]string{"--escape=false"})) + assert.True(t, fs.Changed("escape")) + assert.False(t, escape) + assert.Equal(t, "true", fs.Lookup("escape").DefValue) +} + +func TestFlagUsages(t *testing.T) { + fs := NewFlagSet("test", ContinueOnError) + var name, pswd string + var force, escape bool + var width int + + fs.StringVarP(&name, "config", "c", "", "config file path") + fs.BoolVarP(&force, "force", "f", false, "force rendering the segments") + fs.BoolVar(&escape, "escape", true, "escape the ANSI sequences for the shell") + fs.IntVarP(&width, "terminal-width", "w", 0, "width of the terminal") + fs.StringVar(&pswd, "pswd", "", "hidden flag") + _ = fs.MarkHidden("pswd") + + expected := ` -c, --config string config file path + --escape escape the ANSI sequences for the shell (default true) + -f, --force force rendering the segments + -w, --terminal-width int width of the terminal +` + assert.Equal(t, expected, fs.FlagUsages()) +} diff --git a/src/cmdtree/cmdtree.go b/src/cmdtree/cmdtree.go new file mode 100644 index 000000000..5a0f13090 --- /dev/null +++ b/src/cmdtree/cmdtree.go @@ -0,0 +1,538 @@ +// Package cmdtree is a minimal command-line command tree implementation covering +// the API surface oh-my-posh uses: a command tree with persistent flags and +// hooks, POSIX flag parsing via the sibling cmdflag package, positional +// argument validators, and help/usage output matching common CLI framework +// conventions byte for byte for the features in use. +package cmdtree + +import ( + "fmt" + "io" + "os" + "slices" + "sort" + "strings" + + "github.com/jandedobbeleer/oh-my-posh/src/cmdflag" +) + +// ExplorerLaunchHelpText is shown on Windows when the binary is launched from +// Explorer instead of a terminal. Setting it to "" disables the check. +var ExplorerLaunchHelpText = `This is a command line tool. + +You need to open cmd.exe and run it from there. +` + +type PositionalArgs func(cmd *Command, args []string) error + +type Command struct { + out io.Writer + flags *cmdflag.FlagSet + parent *Command + PersistentPostRun func(cmd *Command, args []string) + pflags *cmdflag.FlagSet + PersistentPreRun func(cmd *Command, args []string) + Args PositionalArgs + Run func(cmd *Command, args []string) + Example string + Long string + Short string + Use string + ValidArgs []string + commands []*Command + requiredFlags []string + Aliases []string + setArgs []string + Hidden bool + helpRequested bool + CompletionOptions struct{ DisableDefaultCmd bool } +} + +// SetOut redirects help output to a caller-supplied writer instead of stdout. +func (c *Command) SetOut(w io.Writer) { + c.out = w +} + +func (c *Command) outWriter() io.Writer { + for cmd := c; cmd != nil; cmd = cmd.parent { + if cmd.out != nil { + return cmd.out + } + } + + return os.Stdout +} + +func (c *Command) Name() string { + name, _, _ := strings.Cut(c.Use, " ") + return name +} + +func (c *Command) CommandPath() string { + if c.parent == nil { + return c.Name() + } + + return c.parent.CommandPath() + " " + c.Name() +} + +func (c *Command) UseLine() string { + useLine := c.Use + if c.parent != nil { + useLine = c.parent.CommandPath() + " " + c.Use + } + + if c.Flags().HasAvailableFlags() && !strings.Contains(useLine, "[flags]") { + useLine += " [flags]" + } + + return useLine +} + +func (c *Command) Root() *Command { + if c.parent == nil { + return c + } + + return c.parent.Root() +} + +func (c *Command) AddCommand(cmds ...*Command) { + for _, cmd := range cmds { + cmd.parent = c + c.commands = append(c.commands, cmd) + } +} + +func (c *Command) Flags() *cmdflag.FlagSet { + if c.flags == nil { + c.flags = cmdflag.NewFlagSet(c.Name(), cmdflag.ContinueOnError) + } + + return c.flags +} + +func (c *Command) PersistentFlags() *cmdflag.FlagSet { + if c.pflags == nil { + c.pflags = cmdflag.NewFlagSet(c.Name(), cmdflag.ContinueOnError) + } + + return c.pflags +} + +func (c *Command) SetArgs(args []string) { + c.setArgs = args +} + +// MarkPersistentFlagRequired only applies to a flag registered on this +// command's own persistent set and errors otherwise. +func (c *Command) MarkPersistentFlagRequired(name string) error { + flag := c.PersistentFlags().Lookup(name) + if flag == nil { + return fmt.Errorf("no such flag -%v", name) + } + + c.requiredFlags = append(c.requiredFlags, name) + return nil +} + +func (c *Command) hasSubCommands() bool { + for _, cmd := range c.commands { + if !cmd.Hidden && cmd.Name() != "help" { + return true + } + } + + return false +} + +func (c *Command) findChild(name string) *Command { + for _, cmd := range c.commands { + if cmd.Name() == name { + return cmd + } + + if slices.Contains(cmd.Aliases, name) { + return cmd + } + } + + return nil +} + +// mergedFlags returns the command's local flags plus every ancestor's +// persistent flags, with the auto help flag registered. +func (c *Command) mergedFlags() *cmdflag.FlagSet { + merged := cmdflag.NewFlagSet(c.Name(), cmdflag.ContinueOnError) + merged.AddFlagSet(c.Flags()) + + for cmd := c; cmd != nil; cmd = cmd.parent { + merged.AddFlagSet(cmd.pflags) + } + + return merged +} + +// inheritedFlags returns the ancestors' persistent flags, rendered in help +// output as the "Global Flags" section. +func (c *Command) inheritedFlags() *cmdflag.FlagSet { + inherited := cmdflag.NewFlagSet(c.Name(), cmdflag.ContinueOnError) + + for cmd := c.parent; cmd != nil; cmd = cmd.parent { + inherited.AddFlagSet(cmd.pflags) + } + + return inherited +} + +// localFlags returns the command's own flags: declared local flags, its own +// persistent flags, and - only when already initialized - the help flag, +// so the help flag is only listed once a caller has actually requested help. +// The unknown-help-topic path renders root usage without a help flag. +func (c *Command) localFlags() *cmdflag.FlagSet { + local := cmdflag.NewFlagSet(c.Name(), cmdflag.ContinueOnError) + local.AddFlagSet(c.Flags()) + local.AddFlagSet(c.pflags) + + return local +} + +func (c *Command) registerHelpFlag() { + if c.Flags().Lookup("help") != nil { + return + } + + c.Flags().BoolVarP(&c.helpRequested, "help", "h", false, "help for "+c.Name()) +} + +// Execute parses os.Args (or SetArgs), routes to the addressed subcommand, +// validates positionals, and runs the hooks and the command. +func (c *Command) Execute() error { + if c.parent == nil { + checkExplorerLaunch() + c.ensureHelpCommand() + } + + args := c.setArgs + if args == nil { + args = os.Args[1:] + } + + cmd, remaining, err := c.route(args) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + fmt.Fprintf(os.Stderr, "Run '%s --help' for usage.\n", c.CommandPath()) + return err + } + + return cmd.execute(remaining) +} + +// route walks the command tree: at each level the first token that is not a +// flag (accounting for flags that consume a value) addresses a child. +func (c *Command) route(args []string) (*Command, []string, error) { + current := c + + for { + name, ok := firstPositional(current, args) + if !ok { + return current, args, nil + } + + child := current.findChild(name) + if child == nil { + if current == c && len(current.commands) > 0 { + return nil, nil, fmt.Errorf("unknown command %q for %q", name, c.CommandPath()) + } + + return current, args, nil + } + + args = removeFirst(args, name) + current = child + } +} + +// firstPositional finds the first argument that cannot be a flag or a flag +// value at this command level. +func firstPositional(c *Command, args []string) (string, bool) { + flags := c.mergedFlags() + + for i := 0; i < len(args); i++ { + arg := args[i] + + switch { + case arg == "--": + return "", false + case strings.HasPrefix(arg, "--"): + name, _, hasValue := strings.Cut(arg[2:], "=") + if hasValue { + continue + } + + if flag := flags.Lookup(name); flag != nil && flag.Value.Type() != "bool" { + i++ // skip the flag's value + } + case strings.HasPrefix(arg, "-") && len(arg) > 1: + if strings.Contains(arg, "=") { + continue + } + + shorthand := arg[len(arg)-1:] + if flag := flags.ShorthandLookup(shorthand); flag != nil && flag.Value.Type() != "bool" { + i++ // skip the flag's value + } + default: + return arg, true + } + } + + return "", false +} + +func removeFirst(args []string, value string) []string { + result := make([]string, 0, len(args)-1) + removed := false + + for _, arg := range args { + if !removed && arg == value { + removed = true + continue + } + + result = append(result, arg) + } + + return result +} + +func (c *Command) execute(args []string) error { + c.registerHelpFlag() + flags := c.mergedFlags() + + if err := flags.Parse(args); err != nil { + return c.flagError(err) + } + + if c.helpRequested { + return c.Help() + } + + for _, name := range c.requiredFlags { + if !flags.Changed(name) { + return c.flagError(fmt.Errorf("required flag(s) \"%s\" not set", name)) + } + } + + positionals := flags.Args() + + if c.Args != nil { + if err := c.Args(c, positionals); err != nil { + return c.flagError(err) + } + } + + if c.Run == nil { + return c.Help() + } + + if hook := c.findHook(func(cmd *Command) func(*Command, []string) { return cmd.PersistentPreRun }); hook != nil { + hook(c, positionals) + } + + c.Run(c, positionals) + + if hook := c.findHook(func(cmd *Command) func(*Command, []string) { return cmd.PersistentPostRun }); hook != nil { + hook(c, positionals) + } + + return nil +} + +// findHook returns the nearest defined hook walking up the tree, so only the +// closest one runs. +func (c *Command) findHook(get func(*Command) func(*Command, []string)) func(*Command, []string) { + for cmd := c; cmd != nil; cmd = cmd.parent { + if hook := get(cmd); hook != nil { + return hook + } + } + + return nil +} + +func (c *Command) flagError(err error) error { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + // print the usage string via Println, leaving a trailing blank line + fmt.Fprintln(os.Stderr, c.usageString()) + return err +} + +// ensureHelpCommand adds the implicit help command if none is registered. +func (c *Command) ensureHelpCommand() { + if c.findChild("help") != nil { + return + } + + c.AddCommand(&Command{ + Use: "help [command]", + Short: "Help about any command", + Long: `Help provides help for any command in the application. +Simply type ` + c.Name() + ` help [path to command] for full details.`, + Run: func(_ *Command, args []string) { + target := c + for _, name := range args { + child := target.findChild(name) + if child == nil { + fmt.Printf("Unknown help topic %#q\n", args) + fmt.Fprint(os.Stderr, c.usageString()) + return + } + + target = child + } + + _ = target.Help() + }, + }) +} + +// help output + +func (c *Command) Help() error { + c.registerHelpFlag() + + long := c.Long + if long == "" { + long = c.Short + } + + if long != "" { + fmt.Fprintln(c.outWriter(), strings.TrimRight(long, "\n")) + fmt.Fprintln(c.outWriter()) + } + + fmt.Fprint(c.outWriter(), c.usageString()) + return nil +} + +func (c *Command) usageString() string { + var sb strings.Builder + + sb.WriteString("Usage:\n") + if c.Run != nil || !c.hasSubCommands() { + sb.WriteString(" " + c.UseLine() + "\n") + } + + if c.hasSubCommands() { + sb.WriteString(" " + c.CommandPath() + " [command]\n") + } + + if len(c.Aliases) > 0 { + sb.WriteString("\nAliases:\n") + sb.WriteString(" " + c.Name() + ", " + strings.Join(c.Aliases, ", ") + "\n") + } + + if c.Example != "" { + sb.WriteString("\nExamples:\n") + sb.WriteString(c.Example + "\n") + } + + if c.hasSubCommands() { + sb.WriteString("\nAvailable Commands:\n") + + commands := make([]*Command, 0, len(c.commands)) + for _, cmd := range c.commands { + if !cmd.Hidden { + commands = append(commands, cmd) + } + } + + sort.Slice(commands, func(i, j int) bool { return commands[i].Name() < commands[j].Name() }) + + padding := 11 + for _, cmd := range commands { + if len(cmd.Name())+2 > padding { + padding = len(cmd.Name()) + 2 + } + } + + for _, cmd := range commands { + fmt.Fprintf(&sb, " %-*s %s\n", padding, cmd.Name(), cmd.Short) + } + } + + if local := c.localFlags(); local.HasAvailableFlags() { + sb.WriteString("\nFlags:\n") + sb.WriteString(local.FlagUsages()) + } + + if inherited := c.inheritedFlags(); inherited.HasAvailableFlags() { + sb.WriteString("\nGlobal Flags:\n") + sb.WriteString(inherited.FlagUsages()) + } + + if c.hasSubCommands() { + sb.WriteString("\nUse \"" + c.CommandPath() + " [command] --help\" for more information about a command.\n") + } + + return sb.String() +} + +// positional argument validators + +func NoArgs(cmd *Command, args []string) error { + if len(args) > 0 { + return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) + } + + return nil +} + +func ExactArgs(n int) PositionalArgs { + return func(_ *Command, args []string) error { + if len(args) != n { + return fmt.Errorf("accepts %d arg(s), received %d", n, len(args)) + } + + return nil + } +} + +func MinimumNArgs(n int) PositionalArgs { + return func(_ *Command, args []string) error { + if len(args) < n { + return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args)) + } + + return nil + } +} + +func RangeArgs(minimum, maximum int) PositionalArgs { + return func(_ *Command, args []string) error { + if len(args) < minimum || len(args) > maximum { + return fmt.Errorf("accepts between %d and %d arg(s), received %d", minimum, maximum, len(args)) + } + + return nil + } +} + +func OnlyValidArgs(cmd *Command, args []string) error { + if len(cmd.ValidArgs) == 0 { + return nil + } + + for _, arg := range args { + if !contains(cmd.ValidArgs, arg) { + return fmt.Errorf("invalid argument %q for %q", arg, cmd.CommandPath()) + } + } + + return nil +} + +func contains(values []string, value string) bool { + return slices.Contains(values, value) +} diff --git a/src/cmdtree/cmdtree_test.go b/src/cmdtree/cmdtree_test.go new file mode 100644 index 000000000..898109be0 --- /dev/null +++ b/src/cmdtree/cmdtree_test.go @@ -0,0 +1,133 @@ +package cmdtree + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func testTree() (root *Command, ran *string) { + ran = new(string) + + root = &Command{ + Use: "root", + Run: func(_ *Command, _ []string) { *ran = "root" }, + } + + child := &Command{ + Use: "child", + Run: func(_ *Command, args []string) { *ran = "child:" + join(args) }, + } + + grandchild := &Command{ + Use: "grandchild", + Run: func(_ *Command, args []string) { *ran = "grandchild:" + join(args) }, + } + + root.AddCommand(child) + child.AddCommand(grandchild) + + return root, ran +} + +func join(args []string) string { + result := "" + for i, arg := range args { + if i > 0 { + result += " " + } + result += arg + } + return result +} + +func TestRouting(t *testing.T) { + root, ran := testTree() + root.SetArgs([]string{"child", "grandchild", "pos"}) + assert.NoError(t, root.Execute()) + assert.Equal(t, "grandchild:pos", *ran) +} + +func TestRoutingWithFlagBeforeCommand(t *testing.T) { + root, ran := testTree() + var config string + root.PersistentFlags().StringVarP(&config, "config", "c", "", "") + + root.SetArgs([]string{"--config", "x", "child", "positional"}) + assert.NoError(t, root.Execute()) + assert.Equal(t, "child:positional", *ran) + assert.Equal(t, "x", config) +} + +func TestUnknownCommand(t *testing.T) { + root, _ := testTree() + root.SetArgs([]string{"bogus"}) + assert.EqualError(t, root.Execute(), `unknown command "bogus" for "root"`) +} + +func TestPersistentFlagInheritance(t *testing.T) { + root, _ := testTree() + var trace bool + root.PersistentFlags().BoolVar(&trace, "trace", false, "") + + root.SetArgs([]string{"child", "--trace"}) + assert.NoError(t, root.Execute()) + assert.True(t, trace) +} + +func TestPersistentHooksRunOnce(t *testing.T) { + var order []string + + root := &Command{ + Use: "root", + PersistentPreRun: func(_ *Command, _ []string) { order = append(order, "pre") }, + PersistentPostRun: func(_ *Command, _ []string) { + order = append(order, "post") + }, + } + + child := &Command{ + Use: "child", + Run: func(_ *Command, _ []string) { order = append(order, "run") }, + } + + root.AddCommand(child) + root.SetArgs([]string{"child"}) + assert.NoError(t, root.Execute()) + assert.Equal(t, []string{"pre", "run", "post"}, order) +} + +func TestArgsValidators(t *testing.T) { + cmd := &Command{Use: "cmd"} + + assert.NoError(t, NoArgs(cmd, nil)) + assert.EqualError(t, NoArgs(cmd, []string{"x"}), `unknown command "x" for "cmd"`) + + assert.NoError(t, ExactArgs(1)(cmd, []string{"a"})) + assert.EqualError(t, ExactArgs(1)(cmd, nil), "accepts 1 arg(s), received 0") + + assert.EqualError(t, MinimumNArgs(1)(cmd, nil), "requires at least 1 arg(s), only received 0") + assert.EqualError(t, RangeArgs(1, 2)(cmd, nil), "accepts between 1 and 2 arg(s), received 0") + + cmd.ValidArgs = []string{"get", "set"} + assert.NoError(t, OnlyValidArgs(cmd, []string{"get"})) + assert.EqualError(t, OnlyValidArgs(cmd, []string{"bogus"}), `invalid argument "bogus" for "cmd"`) +} + +func TestShorthandShadowing(t *testing.T) { + // print defines -s stack-count while root defines -s shell; the + // subcommand's local flag must win + root := &Command{Use: "root"} + var shell string + root.Flags().StringVarP(&shell, "shell", "s", "", "") + + var stack int + child := &Command{Use: "child", Run: func(_ *Command, _ []string) {}} + child.Flags().IntVarP(&stack, "stack-count", "s", 0, "") + root.AddCommand(child) + + root.SetArgs([]string{"child", "-s", "3"}) + assert.NoError(t, root.Execute()) + assert.Equal(t, 3, stack) + assert.Equal(t, "", shell) +} diff --git a/src/cmdtree/explorerguard_other.go b/src/cmdtree/explorerguard_other.go new file mode 100644 index 000000000..fa15e2acd --- /dev/null +++ b/src/cmdtree/explorerguard_other.go @@ -0,0 +1,5 @@ +//go:build !windows + +package cmdtree + +func checkExplorerLaunch() {} diff --git a/src/cmdtree/explorerguard_windows.go b/src/cmdtree/explorerguard_windows.go new file mode 100644 index 000000000..a6a1c1b47 --- /dev/null +++ b/src/cmdtree/explorerguard_windows.go @@ -0,0 +1,63 @@ +//go:build windows + +package cmdtree + +import ( + "fmt" + "os" + "strings" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +// checkExplorerLaunch detects a double-click launch from Explorer: there is +// no console to read the output, so print an explanation, linger, and exit. +func checkExplorerLaunch() { + if ExplorerLaunchHelpText == "" || !startedByExplorer() { + return + } + + fmt.Println(ExplorerLaunchHelpText) + time.Sleep(5 * time.Second) + os.Exit(1) +} + +func startedByExplorer() bool { + pid := windows.GetCurrentProcessId() + + snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if err != nil { + return false + } + + defer func() { _ = windows.CloseHandle(snapshot) }() + + var entry windows.ProcessEntry32 + entry.Size = uint32(unsafe.Sizeof(entry)) + + var parentID uint32 + + for err = windows.Process32First(snapshot, &entry); err == nil; err = windows.Process32Next(snapshot, &entry) { + if entry.ProcessID == pid { + parentID = entry.ParentProcessID + break + } + } + + if parentID == 0 { + return false + } + + for err = windows.Process32First(snapshot, &entry); err == nil; err = windows.Process32Next(snapshot, &entry) { + if entry.ProcessID != parentID { + continue + } + + name := windows.UTF16ToString(entry.ExeFile[:]) + return strings.EqualFold(name, "explorer.exe") + } + + return false +} diff --git a/src/dsc/cli.go b/src/dsc/cli.go index 52faaead8..ae1536a5e 100644 --- a/src/dsc/cli.go +++ b/src/dsc/cli.go @@ -5,8 +5,8 @@ import ( "os" "github.com/jandedobbeleer/oh-my-posh/src/cache" + "github.com/jandedobbeleer/oh-my-posh/src/cmdtree" "github.com/jandedobbeleer/oh-my-posh/src/runtime" - "github.com/spf13/cobra" ) var ( @@ -23,13 +23,13 @@ type resource interface { Test(input string) error } -func Command(r resource) *cobra.Command { - cmd := &cobra.Command{ +func Command(r resource) *cmdtree.Command { + cmd := &cmdtree.Command{ Use: "dsc", Short: "Manage Oh My Posh DSC (Desired State Configuration)", Long: "Manage Oh My Posh DSC (Desired State Configuration).", ValidArgs: []string{"get", "set", "test", "schema", "export"}, - Run: func(cmd *cobra.Command, args []string) { + Run: func(cmd *cmdtree.Command, args []string) { if len(args) == 0 { _ = cmd.Help() return diff --git a/src/go.mod b/src/go.mod index b456c4f8e..817f4ac85 100644 --- a/src/go.mod +++ b/src/go.mod @@ -26,8 +26,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.4.0 github.com/pelletier/go-toml/v2 v2.4.3 github.com/shirou/gopsutil/v4 v4.26.6 - github.com/spf13/cobra v1.10.2 - github.com/spf13/pflag v1.0.10 go.yaml.in/yaml/v3 v3.0.5 ) @@ -51,7 +49,6 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/ebitengine/purego v0.10.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pb33f/ordered-map/v2 v2.3.1 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/shopspring/decimal v1.4.0 // indirect diff --git a/src/go.sum b/src/go.sum index 98783f3fe..e6d8ddf9e 100644 --- a/src/go.sum +++ b/src/go.sum @@ -18,7 +18,6 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -39,8 +38,6 @@ github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU= github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -70,7 +67,6 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= @@ -79,11 +75,6 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= @@ -100,7 +91,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= diff --git a/src/segments/argocd.go b/src/segments/argocd.go index 9479f197d..549925597 100644 --- a/src/segments/argocd.go +++ b/src/segments/argocd.go @@ -6,8 +6,8 @@ import ( "path" "strings" + "github.com/jandedobbeleer/oh-my-posh/src/cmdflag" "github.com/jandedobbeleer/oh-my-posh/src/log" - "github.com/spf13/pflag" yaml "go.yaml.in/yaml/v3" ) @@ -62,7 +62,7 @@ func (a *Argocd) getConfigPath() string { func (a *Argocd) getConfigFromOpts() string { // don't exit/panic when encountering invalid flags - flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError) + flags := cmdflag.NewFlagSet(os.Args[0], cmdflag.ContinueOnError) // ignore other valid and invalid flags flags.ParseErrorsAllowlist.UnknownFlags = true // only care about config diff --git a/src/segments/terraform_hcl.go b/src/segments/terraform_required_version.go similarity index 100% rename from src/segments/terraform_hcl.go rename to src/segments/terraform_required_version.go diff --git a/src/segments/terraform_hcl_test.go b/src/segments/terraform_required_version_test.go similarity index 100% rename from src/segments/terraform_hcl_test.go rename to src/segments/terraform_required_version_test.go