From d59e6acd7286457209b8db10792ea135967c4e98 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 11:01:41 +0000 Subject: [PATCH] fix(cli): route prompt selection hints to stderr Co-Authored-By: Virgil --- pkg/cli/prompt.go | 2 ++ pkg/cli/prompt_test.go | 47 +++++++++++++++++++++++++++++++++++++++++- pkg/cli/utils.go | 2 +- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/pkg/cli/prompt.go b/pkg/cli/prompt.go index af01afd..13287ce 100644 --- a/pkg/cli/prompt.go +++ b/pkg/cli/prompt.go @@ -75,12 +75,14 @@ func Select(label string, options []string) (string, error) { r := newReader() input, err := r.ReadString('\n') if err != nil && strings.TrimSpace(input) == "" { + promptHint("No input received. Selection cancelled.") return "", fmt.Errorf("selection cancelled: %w", err) } trimmed := strings.TrimSpace(input) n, err := strconv.Atoi(trimmed) if err != nil || n < 1 || n > len(options) { + promptHint(fmt.Sprintf("Please enter a number between 1 and %d.", len(options))) return "", fmt.Errorf("invalid selection %q: choose a number between 1 and %d", trimmed, len(options)) } return options[n-1], nil diff --git a/pkg/cli/prompt_test.go b/pkg/cli/prompt_test.go index 07bdd78..8c1c45d 100644 --- a/pkg/cli/prompt_test.go +++ b/pkg/cli/prompt_test.go @@ -10,6 +10,34 @@ import ( "github.com/stretchr/testify/assert" ) +func captureStderr(t *testing.T, fn func()) string { + t.Helper() + + oldErr := os.Stderr + r, w, err := os.Pipe() + if !assert.NoError(t, err) { + return "" + } + os.Stderr = w + + defer func() { + os.Stderr = oldErr + }() + + fn() + + if !assert.NoError(t, w.Close()) { + return "" + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + if !assert.NoError(t, err) { + return "" + } + return buf.String() +} + func TestPrompt_Good(t *testing.T) { SetStdin(strings.NewReader("hello\n")) defer SetStdin(nil) // reset @@ -59,9 +87,13 @@ func TestSelect_Bad_Invalid(t *testing.T) { SetStdin(strings.NewReader("5\n")) defer SetStdin(nil) - _, err := Select("Pick", []string{"a", "b"}) + var err error + stderr := captureStderr(t, func() { + _, err = Select("Pick", []string{"a", "b"}) + }) assert.Error(t, err) assert.Contains(t, err.Error(), "choose a number between 1 and 2") + assert.Contains(t, stderr, "Please enter a number between 1 and 2.") } func TestSelect_Bad_EOF(t *testing.T) { @@ -223,6 +255,19 @@ func TestChoose_Bad_FilteredDefaultDoesNotFallBackToFirstVisible(t *testing.T) { assert.Equal(t, "apricot", val) } +func TestChoose_Bad_InvalidNumberUsesStderrHint(t *testing.T) { + SetStdin(strings.NewReader("5\n2\n")) + defer SetStdin(nil) + + var val string + stderr := captureStderr(t, func() { + val = Choose("Pick", []string{"a", "b"}) + }) + + assert.Equal(t, "b", val) + assert.Contains(t, stderr, "Please enter a number between 1 and 2.") +} + func TestChooseMulti_Good_Filter(t *testing.T) { SetStdin(strings.NewReader("ap\n1 2\n")) defer SetStdin(nil) diff --git a/pkg/cli/utils.go b/pkg/cli/utils.go index 8d749d4..aa5cbd9 100644 --- a/pkg/cli/utils.go +++ b/pkg/cli/utils.go @@ -396,7 +396,7 @@ func Choose[T any](prompt string, items []T, opts ...ChooseOption[T]) T { if n >= 1 && n <= len(visible) { return items[visible[n-1]] } - fmt.Printf("Please enter a number between 1 and %d.\n", len(visible)) + promptHint(fmt.Sprintf("Please enter a number between 1 and %d.", len(visible))) continue } -- 2.45.3