fix(cli): route interactive ui to stderr
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f8ba7be626
commit
4f7a4c3a20
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) {
|
func Progress(verb string, current, total int, item ...string) {
|
||||||
msg := i18n.Progress(verb)
|
msg := i18n.Progress(verb)
|
||||||
if len(item) > 0 && item[0] != "" {
|
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 {
|
} 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.
|
// ProgressDone clears the progress line.
|
||||||
func ProgressDone() {
|
func ProgressDone() {
|
||||||
fmt.Print("\033[2K\r")
|
fmt.Fprint(os.Stderr, "\033[2K\r")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label prints a "Label: value" line.
|
// Label prints a "Label: value" line.
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ func Prompt(label, defaultVal string) (string, error) {
|
||||||
label = compileGlyphs(label)
|
label = compileGlyphs(label)
|
||||||
defaultVal = compileGlyphs(defaultVal)
|
defaultVal = compileGlyphs(defaultVal)
|
||||||
if defaultVal != "" {
|
if defaultVal != "" {
|
||||||
fmt.Printf("%s [%s]: ", label, defaultVal)
|
fmt.Fprintf(os.Stderr, "%s [%s]: ", label, defaultVal)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s: ", label)
|
fmt.Fprintf(os.Stderr, "%s: ", label)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := newReader()
|
r := newReader()
|
||||||
|
|
@ -66,11 +66,11 @@ func Select(label string, options []string) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(compileGlyphs(label))
|
fmt.Fprintln(os.Stderr, compileGlyphs(label))
|
||||||
for i, opt := range options {
|
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()
|
r := newReader()
|
||||||
input, err := r.ReadString('\n')
|
input, err := r.ReadString('\n')
|
||||||
|
|
@ -94,11 +94,11 @@ func MultiSelect(label string, options []string) ([]string, error) {
|
||||||
return []string{}, nil
|
return []string{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(compileGlyphs(label))
|
fmt.Fprintln(os.Stderr, compileGlyphs(label))
|
||||||
for i, opt := range options {
|
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()
|
r := newReader()
|
||||||
input, err := r.ReadString('\n')
|
input, err := r.ReadString('\n')
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,49 @@ func captureStderr(t *testing.T, fn func()) string {
|
||||||
return buf.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) {
|
func TestPrompt_Good(t *testing.T) {
|
||||||
SetStdin(strings.NewReader("hello\n"))
|
SetStdin(strings.NewReader("hello\n"))
|
||||||
defer SetStdin(nil) // reset
|
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(), "try again")
|
||||||
assert.Contains(t, stderr.String(), "invalid")
|
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.
|
// NewTaskTracker creates a new parallel task tracker.
|
||||||
func NewTaskTracker() *TaskTracker {
|
func NewTaskTracker() *TaskTracker {
|
||||||
return &TaskTracker{out: os.Stdout}
|
return &TaskTracker{out: os.Stderr}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add registers a task and returns it for goroutine use.
|
// Add registers a task and returns it for goroutine use.
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
||||||
reader := newReader()
|
reader := newReader()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fmt.Printf("%s %s", prompt, suffix)
|
fmt.Fprintf(os.Stderr, "%s %s", prompt, suffix)
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
var readErr error
|
var readErr error
|
||||||
|
|
@ -130,7 +130,7 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
||||||
readErr = <-errChan
|
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.Fprintln(os.Stderr) // New line after timeout
|
||||||
return cfg.defaultYes
|
return cfg.defaultYes
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -245,9 +245,9 @@ func Question(prompt string, opts ...QuestionOption) string {
|
||||||
for {
|
for {
|
||||||
// Build prompt with default
|
// Build prompt with default
|
||||||
if cfg.defaultValue != "" {
|
if cfg.defaultValue != "" {
|
||||||
fmt.Printf("%s [%s] ", prompt, compileGlyphs(cfg.defaultValue))
|
fmt.Fprintf(os.Stderr, "%s [%s] ", prompt, compileGlyphs(cfg.defaultValue))
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("%s ", prompt)
|
fmt.Fprintf(os.Stderr, "%s ", prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := reader.ReadString('\n')
|
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)
|
renderChoices(prompt, items, visible, cfg.displayFn, cfg.defaultN, cfg.filter)
|
||||||
|
|
||||||
if 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 {
|
} 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, err := reader.ReadString('\n')
|
||||||
response = strings.TrimSpace(response)
|
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)
|
renderChoices(prompt, items, visible, cfg.displayFn, -1, cfg.filter)
|
||||||
|
|
||||||
if 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 {
|
} 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, _ := reader.ReadString('\n')
|
||||||
response = strings.TrimSpace(response)
|
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) {
|
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 {
|
for i, idx := range visible {
|
||||||
marker := " "
|
marker := " "
|
||||||
if defaultN >= 0 && idx == defaultN {
|
if defaultN >= 0 && idx == defaultN {
|
||||||
marker = "*"
|
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 {
|
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