From 7f38141bf6e7b6b4b4149f40d48e511fd41ef50c Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Wed, 11 Feb 2026 11:10:58 -0800 Subject: [PATCH 1/4] Shorten top-level help to show only command names with sub-command counts Top-level help no longer shows descriptions for each command, keeping the output concise. Commands and namespaces with children now display a "(N sub-commands)" indicator so users know to drill deeper. --- dispatcher.go | 32 +++++++++++++++++++------------- dispatcher_test.go | 9 ++++++--- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/dispatcher.go b/dispatcher.go index 6c18e33..19cd415 100644 --- a/dispatcher.go +++ b/dispatcher.go @@ -435,18 +435,10 @@ func (d *Dispatcher) showHelp() error { children := d.getDirectChildren("") - // Find max length for alignment - maxLen := 0 for _, child := range children { - if len(child.Name) > maxLen { - maxLen = len(child.Name) - } - } - - // Print commands with usage - for _, child := range children { - if child.Usage != "" { - fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, child.Usage) + grandchildren := d.getDirectChildren(child.Path) + if len(grandchildren) > 0 { + fmt.Printf(" %s (%d sub-commands)\n", child.Name, len(grandchildren)) } else { fmt.Printf(" %s\n", child.Name) } @@ -568,8 +560,15 @@ func (d *Dispatcher) showCommandHelp(entry *CommandEntry) error { // Print sub-commands with usage for _, child := range children { + grandchildren := d.getDirectChildren(child.Path) + suffix := "" + if len(grandchildren) > 0 { + suffix = fmt.Sprintf(" (%d sub-commands)", len(grandchildren)) + } if child.Usage != "" { - fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, child.Usage) + fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) + } else if suffix != "" { + fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, suffix[1:]) } else { fmt.Printf(" %s\n", child.Name) } @@ -704,8 +703,15 @@ func (d *Dispatcher) showNamespaceHelp(namespacePath string) error { } for _, child := range children { + grandchildren := d.getDirectChildren(child.Path) + suffix := "" + if len(grandchildren) > 0 { + suffix = fmt.Sprintf(" (%d sub-commands)", len(grandchildren)) + } if child.Usage != "" { - fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, child.Usage) + fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) + } else if suffix != "" { + fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, suffix[1:]) } else { fmt.Printf(" %s\n", child.Name) } diff --git a/dispatcher_test.go b/dispatcher_test.go index 43f7a92..3f62446 100644 --- a/dispatcher_test.go +++ b/dispatcher_test.go @@ -167,10 +167,11 @@ func TestDispatcherHelp(t *testing.T) { assert.NoError(t, err) assert.Contains(t, output, "Available commands:") assert.Contains(t, output, "build") - assert.Contains(t, output, "Build the project") assert.Contains(t, output, "test") - assert.Contains(t, output, "Run tests") assert.Contains(t, output, "clean") + // Top-level help should NOT show descriptions (short format) + assert.NotContains(t, output, "Build the project") + assert.NotContains(t, output, "Run tests") } func TestDispatcherCommandHelp(t *testing.T) { @@ -963,6 +964,8 @@ func TestDispatcherSubCommandHelp(t *testing.T) { assert.Contains(t, output, "Update remote refs") // Should not show nested sub-command "remote add", only direct children assert.NotContains(t, output, "remote add") + // "remote" is a namespace with 1 sub-command, should show the count + assert.Contains(t, output, "(1 sub-commands)") } func TestDispatcherNamespaceDiscovery(t *testing.T) { @@ -999,7 +1002,7 @@ func TestDispatcherNamespaceDiscovery(t *testing.T) { assert.NoError(t, err) assert.Contains(t, output, "Available commands:") assert.Contains(t, output, "build") - assert.Contains(t, output, "config") + assert.Contains(t, output, "config (2 sub-commands)") // Should NOT show the full subcommand paths at the top level assert.NotContains(t, output, "config get") assert.NotContains(t, output, "config set") From e6502a3ec684c49d4437b1ad6a0c1cc73ef08bfb Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Wed, 11 Feb 2026 11:20:09 -0800 Subject: [PATCH 2/4] Render sub-command counts with faint ANSI styling on TTYs Uses golang.org/x/term to detect if stdout is a terminal and applies faint ANSI styling to "(N sub-commands)" hints. Respects NO_COLOR. The IsTerminal check is cached with sync.OnceValue. --- dispatcher.go | 29 ++++++++++++++++++++++++----- go.mod | 2 ++ go.sum | 4 ++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/dispatcher.go b/dispatcher.go index 19cd415..242ad2f 100644 --- a/dispatcher.go +++ b/dispatcher.go @@ -6,8 +6,27 @@ import ( "os" "sort" "strings" + "sync" + + "golang.org/x/term" ) +var stdoutIsTerminal = sync.OnceValue(func() bool { + return term.IsTerminal(int(os.Stdout.Fd())) +}) + +// faint returns the string wrapped in ANSI faint styling if stdout is a TTY +// and the NO_COLOR environment variable is not set. +func faint(s string) string { + if os.Getenv("NO_COLOR") != "" { + return s + } + if !stdoutIsTerminal() { + return s + } + return "\033[2m" + s + "\033[0m" +} + // Command is an interface for executable commands type Command interface { // FlagSet returns the flagset for this command @@ -438,7 +457,7 @@ func (d *Dispatcher) showHelp() error { for _, child := range children { grandchildren := d.getDirectChildren(child.Path) if len(grandchildren) > 0 { - fmt.Printf(" %s (%d sub-commands)\n", child.Name, len(grandchildren)) + fmt.Printf(" %s %s\n", child.Name, faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren)))) } else { fmt.Printf(" %s\n", child.Name) } @@ -563,12 +582,12 @@ func (d *Dispatcher) showCommandHelp(entry *CommandEntry) error { grandchildren := d.getDirectChildren(child.Path) suffix := "" if len(grandchildren) > 0 { - suffix = fmt.Sprintf(" (%d sub-commands)", len(grandchildren)) + suffix = " " + faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren))) } if child.Usage != "" { fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) } else if suffix != "" { - fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, suffix[1:]) + fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, suffix) } else { fmt.Printf(" %s\n", child.Name) } @@ -706,12 +725,12 @@ func (d *Dispatcher) showNamespaceHelp(namespacePath string) error { grandchildren := d.getDirectChildren(child.Path) suffix := "" if len(grandchildren) > 0 { - suffix = fmt.Sprintf(" (%d sub-commands)", len(grandchildren)) + suffix = " " + faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren))) } if child.Usage != "" { fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) } else if suffix != "" { - fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, suffix[1:]) + fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, suffix) } else { fmt.Printf(" %s\n", child.Name) } diff --git a/go.mod b/go.mod index 79ce28b..6835cea 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cc8b3f4..a19b998 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From d3db9380b2a34baa10ac20f0bf0e374862270856 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Wed, 11 Feb 2026 11:24:33 -0800 Subject: [PATCH 3/4] Show both usage descriptions and sub-command counts in top-level help --- dispatcher.go | 16 +++++++++++++++- dispatcher_test.go | 8 ++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/dispatcher.go b/dispatcher.go index 242ad2f..5b8d6d4 100644 --- a/dispatcher.go +++ b/dispatcher.go @@ -454,10 +454,24 @@ func (d *Dispatcher) showHelp() error { children := d.getDirectChildren("") + // Find max length for alignment + maxLen := 0 + for _, child := range children { + if len(child.Name) > maxLen { + maxLen = len(child.Name) + } + } + for _, child := range children { grandchildren := d.getDirectChildren(child.Path) + suffix := "" if len(grandchildren) > 0 { - fmt.Printf(" %s %s\n", child.Name, faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren)))) + suffix = " " + faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren))) + } + if child.Usage != "" { + fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) + } else if suffix != "" { + fmt.Printf(" %-*s %s\n", maxLen+2, child.Name, suffix) } else { fmt.Printf(" %s\n", child.Name) } diff --git a/dispatcher_test.go b/dispatcher_test.go index 3f62446..8ec1ec5 100644 --- a/dispatcher_test.go +++ b/dispatcher_test.go @@ -167,11 +167,10 @@ func TestDispatcherHelp(t *testing.T) { assert.NoError(t, err) assert.Contains(t, output, "Available commands:") assert.Contains(t, output, "build") + assert.Contains(t, output, "Build the project") assert.Contains(t, output, "test") + assert.Contains(t, output, "Run tests") assert.Contains(t, output, "clean") - // Top-level help should NOT show descriptions (short format) - assert.NotContains(t, output, "Build the project") - assert.NotContains(t, output, "Run tests") } func TestDispatcherCommandHelp(t *testing.T) { @@ -1002,7 +1001,8 @@ func TestDispatcherNamespaceDiscovery(t *testing.T) { assert.NoError(t, err) assert.Contains(t, output, "Available commands:") assert.Contains(t, output, "build") - assert.Contains(t, output, "config (2 sub-commands)") + assert.Contains(t, output, "config") + assert.Contains(t, output, "(2 sub-commands)") // Should NOT show the full subcommand paths at the top level assert.NotContains(t, output, "config get") assert.NotContains(t, output, "config set") From d8a537f18611c7061bed67972a9ab3447398b780 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Wed, 11 Feb 2026 13:18:37 -0800 Subject: [PATCH 4/4] Use singular "sub-command" when count is 1 --- dispatcher.go | 13 ++++++++++--- dispatcher_test.go | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dispatcher.go b/dispatcher.go index 5b8d6d4..121fd8a 100644 --- a/dispatcher.go +++ b/dispatcher.go @@ -27,6 +27,13 @@ func faint(s string) string { return "\033[2m" + s + "\033[0m" } +func subCommandsLabel(n int) string { + if n == 1 { + return "(1 sub-command)" + } + return fmt.Sprintf("(%d sub-commands)", n) +} + // Command is an interface for executable commands type Command interface { // FlagSet returns the flagset for this command @@ -466,7 +473,7 @@ func (d *Dispatcher) showHelp() error { grandchildren := d.getDirectChildren(child.Path) suffix := "" if len(grandchildren) > 0 { - suffix = " " + faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren))) + suffix = " " + faint(subCommandsLabel(len(grandchildren))) } if child.Usage != "" { fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) @@ -596,7 +603,7 @@ func (d *Dispatcher) showCommandHelp(entry *CommandEntry) error { grandchildren := d.getDirectChildren(child.Path) suffix := "" if len(grandchildren) > 0 { - suffix = " " + faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren))) + suffix = " " + faint(subCommandsLabel(len(grandchildren))) } if child.Usage != "" { fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) @@ -739,7 +746,7 @@ func (d *Dispatcher) showNamespaceHelp(namespacePath string) error { grandchildren := d.getDirectChildren(child.Path) suffix := "" if len(grandchildren) > 0 { - suffix = " " + faint(fmt.Sprintf("(%d sub-commands)", len(grandchildren))) + suffix = " " + faint(subCommandsLabel(len(grandchildren))) } if child.Usage != "" { fmt.Printf(" %-*s %s%s\n", maxLen+2, child.Name, child.Usage, suffix) diff --git a/dispatcher_test.go b/dispatcher_test.go index 8ec1ec5..807a18c 100644 --- a/dispatcher_test.go +++ b/dispatcher_test.go @@ -964,7 +964,7 @@ func TestDispatcherSubCommandHelp(t *testing.T) { // Should not show nested sub-command "remote add", only direct children assert.NotContains(t, output, "remote add") // "remote" is a namespace with 1 sub-command, should show the count - assert.Contains(t, output, "(1 sub-commands)") + assert.Contains(t, output, "(1 sub-command)") } func TestDispatcherNamespaceDiscovery(t *testing.T) {