[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/cli/RFC.md fully. Find features des... #79
5 changed files with 80 additions and 23 deletions
|
|
@ -114,15 +114,15 @@ func Dim(msg string) {
|
|||
func Progress(verb string, current, total int, item ...string) {
|
||||
msg := i18n.Progress(verb)
|
||||
if len(item) > 0 && item[0] != "" {
|
||||
fmt.Printf("\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0])
|
||||
fmt.Fprintf(os.Stderr, "\033[2K\r%s %d/%d %s", DimStyle.Render(msg), current, total, item[0])
|
||||
} else {
|
||||
fmt.Printf("\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total)
|
||||
fmt.Fprintf(os.Stderr, "\033[2K\r%s %d/%d", DimStyle.Render(msg), current, total)
|
||||
}
|
||||
}
|
||||
|
||||
// ProgressDone clears the progress line.
|
||||
func ProgressDone() {
|
||||
fmt.Print("\033[2K\r")
|
||||
fmt.Fprint(os.Stderr, "\033[2K\r")
|
||||
}
|
||||
|
||||
// Label prints a "Label: value" line.
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ func Prompt(label, defaultVal string) (string, error) {
|
|||
label = compileGlyphs(label)
|
||||
defaultVal = compileGlyphs(defaultVal)
|
||||
if defaultVal != "" {
|
||||
fmt.Printf("%s [%s]: ", label, defaultVal)
|
||||
fmt.Fprintf(os.Stderr, "%s [%s]: ", label, defaultVal)
|
||||
} else {
|
||||
fmt.Printf("%s: ", label)
|
||||
fmt.Fprintf(os.Stderr, "%s: ", label)
|
||||
}
|
||||
|
||||
r := newReader()
|
||||
|
|
@ -66,11 +66,11 @@ func Select(label string, options []string) (string, error) {
|
|||
return "", nil
|
||||
}
|
||||
|
||||
fmt.Println(compileGlyphs(label))
|
||||
fmt.Fprintln(os.Stderr, compileGlyphs(label))
|
||||
for i, opt := range options {
|
||||
fmt.Printf(" %d. %s\n", i+1, compileGlyphs(opt))
|
||||
fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, compileGlyphs(opt))
|
||||
}
|
||||
fmt.Printf("Choose [1-%d]: ", len(options))
|
||||
fmt.Fprintf(os.Stderr, "Choose [1-%d]: ", len(options))
|
||||
|
||||
r := newReader()
|
||||
input, err := r.ReadString('\n')
|
||||
|
|
@ -94,11 +94,11 @@ func MultiSelect(label string, options []string) ([]string, error) {
|
|||
return []string{}, nil
|
||||
}
|
||||
|
||||
fmt.Println(compileGlyphs(label))
|
||||
fmt.Fprintln(os.Stderr, compileGlyphs(label))
|
||||
for i, opt := range options {
|
||||
fmt.Printf(" %d. %s\n", i+1, compileGlyphs(opt))
|
||||
fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, compileGlyphs(opt))
|
||||
}
|
||||
fmt.Printf("Choose (space-separated) [1-%d]: ", len(options))
|
||||
fmt.Fprintf(os.Stderr, "Choose (space-separated) [1-%d]: ", len(options))
|
||||
|
||||
r := newReader()
|
||||
input, err := r.ReadString('\n')
|
||||
|
|
|
|||
|
|
@ -38,6 +38,49 @@ func captureStderr(t *testing.T, fn func()) string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
func captureStdoutStderr(t *testing.T, fn func()) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
oldOut := os.Stdout
|
||||
oldErr := os.Stderr
|
||||
rOut, wOut, err := os.Pipe()
|
||||
if !assert.NoError(t, err) {
|
||||
return "", ""
|
||||
}
|
||||
rErr, wErr, err := os.Pipe()
|
||||
if !assert.NoError(t, err) {
|
||||
return "", ""
|
||||
}
|
||||
os.Stdout = wOut
|
||||
os.Stderr = wErr
|
||||
|
||||
defer func() {
|
||||
os.Stdout = oldOut
|
||||
os.Stderr = oldErr
|
||||
}()
|
||||
|
||||
fn()
|
||||
|
||||
if !assert.NoError(t, wOut.Close()) {
|
||||
return "", ""
|
||||
}
|
||||
if !assert.NoError(t, wErr.Close()) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
var outBuf bytes.Buffer
|
||||
var errBuf bytes.Buffer
|
||||
_, err = io.Copy(&outBuf, rOut)
|
||||
if !assert.NoError(t, err) {
|
||||
return "", ""
|
||||
}
|
||||
_, err = io.Copy(&errBuf, rErr)
|
||||
if !assert.NoError(t, err) {
|
||||
return "", ""
|
||||
}
|
||||
return outBuf.String(), errBuf.String()
|
||||
}
|
||||
|
||||
func TestPrompt_Good(t *testing.T) {
|
||||
SetStdin(strings.NewReader("hello\n"))
|
||||
defer SetStdin(nil) // reset
|
||||
|
|
@ -353,3 +396,17 @@ func TestPromptHints_Good_UseStderr(t *testing.T) {
|
|||
assert.Contains(t, stderr.String(), "try again")
|
||||
assert.Contains(t, stderr.String(), "invalid")
|
||||
}
|
||||
|
||||
func TestPrompt_Good_WritesToStderr(t *testing.T) {
|
||||
SetStdin(strings.NewReader("hello\n"))
|
||||
defer SetStdin(nil)
|
||||
|
||||
stdout, stderr := captureStdoutStderr(t, func() {
|
||||
val, err := Prompt("Name", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", val)
|
||||
})
|
||||
|
||||
assert.Empty(t, stdout)
|
||||
assert.Contains(t, stderr, "Name:")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ func (tr *TaskTracker) Snapshots() iter.Seq2[string, string] {
|
|||
|
||||
// NewTaskTracker creates a new parallel task tracker.
|
||||
func NewTaskTracker() *TaskTracker {
|
||||
return &TaskTracker{out: os.Stdout}
|
||||
return &TaskTracker{out: os.Stderr}
|
||||
}
|
||||
|
||||
// Add registers a task and returns it for goroutine use.
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
|||
reader := newReader()
|
||||
|
||||
for {
|
||||
fmt.Printf("%s %s", prompt, suffix)
|
||||
fmt.Fprintf(os.Stderr, "%s %s", prompt, suffix)
|
||||
|
||||
var response string
|
||||
var readErr error
|
||||
|
|
@ -130,7 +130,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
|||
readErr = <-errChan
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
case <-time.After(cfg.timeout):
|
||||
fmt.Println() // New line after timeout
|
||||
fmt.Fprintln(os.Stderr) // New line after timeout
|
||||
return cfg.defaultYes
|
||||
}
|
||||
} else {
|
||||
|
|
@ -245,9 +245,9 @@ func Question(prompt string, opts ...QuestionOption) string {
|
|||
for {
|
||||
// Build prompt with default
|
||||
if cfg.defaultValue != "" {
|
||||
fmt.Printf("%s [%s] ", prompt, compileGlyphs(cfg.defaultValue))
|
||||
fmt.Fprintf(os.Stderr, "%s [%s] ", prompt, compileGlyphs(cfg.defaultValue))
|
||||
} else {
|
||||
fmt.Printf("%s ", prompt)
|
||||
fmt.Fprintf(os.Stderr, "%s ", prompt)
|
||||
}
|
||||
|
||||
response, err := reader.ReadString('\n')
|
||||
|
|
@ -364,9 +364,9 @@ func Choose[T any](prompt string, items []T, opts ...ChooseOption[T]) T {
|
|||
renderChoices(prompt, items, visible, cfg.displayFn, cfg.defaultN, cfg.filter)
|
||||
|
||||
if cfg.filter {
|
||||
fmt.Printf("Enter number [1-%d] or filter: ", len(visible))
|
||||
fmt.Fprintf(os.Stderr, "Enter number [1-%d] or filter: ", len(visible))
|
||||
} else {
|
||||
fmt.Printf("Enter number [1-%d]: ", len(visible))
|
||||
fmt.Fprintf(os.Stderr, "Enter number [1-%d]: ", len(visible))
|
||||
}
|
||||
response, err := reader.ReadString('\n')
|
||||
response = strings.TrimSpace(response)
|
||||
|
|
@ -458,9 +458,9 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
|
|||
renderChoices(prompt, items, visible, cfg.displayFn, -1, cfg.filter)
|
||||
|
||||
if cfg.filter {
|
||||
fmt.Printf("Enter numbers (e.g., 1 3 5 or 1-3), or filter text, or empty for none: ")
|
||||
fmt.Fprint(os.Stderr, "Enter numbers (e.g., 1 3 5 or 1-3), or filter text, or empty for none: ")
|
||||
} else {
|
||||
fmt.Printf("Enter numbers (e.g., 1 3 5 or 1-3) or empty for none: ")
|
||||
fmt.Fprint(os.Stderr, "Enter numbers (e.g., 1 3 5 or 1-3) or empty for none: ")
|
||||
}
|
||||
response, _ := reader.ReadString('\n')
|
||||
response = strings.TrimSpace(response)
|
||||
|
|
@ -503,16 +503,16 @@ func ChooseMulti[T any](prompt string, items []T, opts ...ChooseOption[T]) []T {
|
|||
}
|
||||
|
||||
func renderChoices[T any](prompt string, items []T, visible []int, displayFn func(T) string, defaultN int, filter bool) {
|
||||
fmt.Println(prompt)
|
||||
fmt.Fprintln(os.Stderr, prompt)
|
||||
for i, idx := range visible {
|
||||
marker := " "
|
||||
if defaultN >= 0 && idx == defaultN {
|
||||
marker = "*"
|
||||
}
|
||||
fmt.Printf(" %s%d. %s\n", marker, i+1, compileGlyphs(displayFn(items[idx])))
|
||||
fmt.Fprintf(os.Stderr, " %s%d. %s\n", marker, i+1, compileGlyphs(displayFn(items[idx])))
|
||||
}
|
||||
if filter {
|
||||
fmt.Println(" (type to filter the list)")
|
||||
fmt.Fprintln(os.Stderr, " (type to filter the list)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue