fix(cli): reprompt required prompts on empty input
All checks were successful
Security Scan / security (push) Successful in 23s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 06:10:37 +00:00
parent a5142dea78
commit 43d4bbd2dc
2 changed files with 27 additions and 2 deletions

View file

@ -94,6 +94,13 @@ func TestConfirm_Bad_EOFUsesDefault(t *testing.T) {
assert.True(t, Confirm("Proceed?", DefaultYes(), Required())) assert.True(t, Confirm("Proceed?", DefaultYes(), Required()))
} }
func TestConfirm_Good_RequiredReprompts(t *testing.T) {
SetStdin(strings.NewReader("\ny\n"))
defer SetStdin(nil)
assert.True(t, Confirm("Proceed?", Required()))
}
func TestQuestion_Good(t *testing.T) { func TestQuestion_Good(t *testing.T) {
SetStdin(strings.NewReader("alice\n")) SetStdin(strings.NewReader("alice\n"))
defer SetStdin(nil) defer SetStdin(nil)
@ -110,6 +117,14 @@ func TestQuestion_Bad_EOFReturnsDefault(t *testing.T) {
assert.Equal(t, "", Question("Name:", RequiredInput())) assert.Equal(t, "", Question("Name:", RequiredInput()))
} }
func TestQuestion_Good_RequiredReprompts(t *testing.T) {
SetStdin(strings.NewReader("\nalice\n"))
defer SetStdin(nil)
val := Question("Name:", RequiredInput())
assert.Equal(t, "alice", val)
}
func TestChoose_Good_DefaultIndex(t *testing.T) { func TestChoose_Good_DefaultIndex(t *testing.T) {
SetStdin(strings.NewReader("\n")) SetStdin(strings.NewReader("\n"))
defer SetStdin(nil) defer SetStdin(nil)

View file

@ -104,17 +104,21 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
fmt.Printf("%s %s", prompt, suffix) fmt.Printf("%s %s", prompt, suffix)
var response string var response string
var readErr error
if cfg.timeout > 0 { if cfg.timeout > 0 {
// Use timeout-based reading // Use timeout-based reading
resultChan := make(chan string, 1) resultChan := make(chan string, 1)
errChan := make(chan error, 1)
go func() { go func() {
line, _ := reader.ReadString('\n') line, err := reader.ReadString('\n')
resultChan <- line resultChan <- line
errChan <- err
}() }()
select { select {
case response = <-resultChan: case response = <-resultChan:
readErr = <-errChan
response = strings.ToLower(strings.TrimSpace(response)) response = strings.ToLower(strings.TrimSpace(response))
case <-time.After(cfg.timeout): case <-time.After(cfg.timeout):
fmt.Println() // New line after timeout fmt.Println() // New line after timeout
@ -122,6 +126,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
} }
} else { } else {
line, err := reader.ReadString('\n') line, err := reader.ReadString('\n')
readErr = err
if err != nil && line == "" { if err != nil && line == "" {
return cfg.defaultYes return cfg.defaultYes
} }
@ -131,6 +136,10 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
// Handle empty response // Handle empty response
if response == "" { if response == "" {
if readErr == nil && cfg.required {
fmt.Println("Please enter 'y' or 'n'")
continue
}
if cfg.required { if cfg.required {
return cfg.defaultYes return cfg.defaultYes
} }
@ -241,7 +250,8 @@ func Question(prompt string, opts ...QuestionOption) string {
// Handle empty response // Handle empty response
if response == "" { if response == "" {
if cfg.required { if cfg.required {
return cfg.defaultValue fmt.Println("Please enter a value")
continue
} }
response = cfg.defaultValue response = cfg.defaultValue
} }