cli/pkg/cli/prompt.go
Virgil fcf5f9cfd5 fix(cli): make stdio routing injectable
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 13:39:56 +00:00

110 lines
2.8 KiB
Go

package cli
import (
"bufio"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
// newReader wraps stdin in a bufio.Reader if it isn't one already.
func newReader() *bufio.Reader {
if br, ok := stdinReader().(*bufio.Reader); ok {
return br
}
return bufio.NewReader(stdinReader())
}
// Prompt asks for text input with a default value.
func Prompt(label, defaultVal string) (string, error) {
label = compileGlyphs(label)
defaultVal = compileGlyphs(defaultVal)
if defaultVal != "" {
fmt.Fprintf(stderrWriter(), "%s [%s]: ", label, defaultVal)
} else {
fmt.Fprintf(stderrWriter(), "%s: ", label)
}
r := newReader()
input, err := r.ReadString('\n')
input = strings.TrimSpace(input)
if err != nil {
if !errors.Is(err, io.EOF) {
return "", err
}
if input == "" {
if defaultVal != "" {
return defaultVal, nil
}
return "", err
}
}
if input == "" {
return defaultVal, nil
}
return input, nil
}
// Select presents numbered options and returns the selected value.
func Select(label string, options []string) (string, error) {
if len(options) == 0 {
return "", nil
}
fmt.Fprintln(stderrWriter(), compileGlyphs(label))
for i, opt := range options {
fmt.Fprintf(stderrWriter(), " %d. %s\n", i+1, compileGlyphs(opt))
}
fmt.Fprintf(stderrWriter(), "Choose [1-%d]: ", len(options))
r := newReader()
input, err := r.ReadString('\n')
if err != nil && strings.TrimSpace(input) == "" {
promptHint("No input received. Selection cancelled.")
return "", Wrap(err, "selection cancelled")
}
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 "", Err("invalid selection %q: choose a number between 1 and %d", trimmed, len(options))
}
return options[n-1], nil
}
// MultiSelect presents checkboxes (space-separated numbers).
func MultiSelect(label string, options []string) ([]string, error) {
if len(options) == 0 {
return []string{}, nil
}
fmt.Fprintln(stderrWriter(), compileGlyphs(label))
for i, opt := range options {
fmt.Fprintf(stderrWriter(), " %d. %s\n", i+1, compileGlyphs(opt))
}
fmt.Fprintf(stderrWriter(), "Choose (space-separated) [1-%d]: ", len(options))
r := newReader()
input, err := r.ReadString('\n')
trimmed := strings.TrimSpace(input)
if err != nil && trimmed == "" {
return []string{}, nil
}
if err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
selected, parseErr := parseMultiSelection(trimmed, len(options))
if parseErr != nil {
return nil, Wrap(parseErr, fmt.Sprintf("invalid selection %q", trimmed))
}
selectedOptions := make([]string, 0, len(selected))
for _, idx := range selected {
selectedOptions = append(selectedOptions, options[idx])
}
return selectedOptions, nil
}