diff --git a/libs/cmdio/compat.go b/libs/cmdio/ask.go similarity index 65% rename from libs/cmdio/compat.go rename to libs/cmdio/ask.go index ed2ed2498a..909076d5e6 100644 --- a/libs/cmdio/compat.go +++ b/libs/cmdio/ask.go @@ -7,23 +7,6 @@ import ( "strings" ) -/* -Temporary compatibility layer for the progress logger interfaces. -*/ - -// Log is a compatibility layer for the progress logger interfaces. -// It writes the string representation of the stringer to the error writer. -func Log(ctx context.Context, str fmt.Stringer) { - LogString(ctx, str.String()) -} - -// LogString is a compatibility layer for the progress logger interfaces. -// It writes the string to the error writer. -func LogString(ctx context.Context, str string) { - c := fromContext(ctx) - _, _ = io.WriteString(c.err, str+"\n") -} - // readLine reads a line from the reader and returns it without the trailing newline characters. // It is unbuffered because cmdio's stdin is also unbuffered. // If we were to add a [bufio.Reader] to the mix, we would need to update the other uses of the reader. @@ -51,8 +34,8 @@ func readLine(r io.Reader) (string, error) { return b.String(), nil } -// Ask is a compatibility layer for the progress logger interfaces. -// It prompts the user with a question and returns the answer. +// Ask prompts the user with a question and returns the entered answer. +// If the user just presses enter, defaultVal is returned. func Ask(ctx context.Context, question, defaultVal string) (string, error) { c := fromContext(ctx) @@ -82,8 +65,9 @@ func Ask(ctx context.Context, question, defaultVal string) (string, error) { return ans, nil } -// AskYesOrNo is a compatibility layer for the progress logger interfaces. -// It prompts the user with a question and returns the answer. +// AskYesOrNo prompts the user with a question and returns true if the answer +// is "y" or "yes" (case-insensitive). Any other answer, including an empty +// one, returns false. func AskYesOrNo(ctx context.Context, question string) (bool, error) { ans, err := Ask(ctx, question+" [y/N]", "") if err != nil { diff --git a/libs/cmdio/compat_test.go b/libs/cmdio/ask_test.go similarity index 97% rename from libs/cmdio/compat_test.go rename to libs/cmdio/ask_test.go index 702f25748b..75fc520f8d 100644 --- a/libs/cmdio/compat_test.go +++ b/libs/cmdio/ask_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCompat_readLine(t *testing.T) { +func TestReadLine(t *testing.T) { tests := []struct { name string reader io.Reader @@ -147,7 +147,7 @@ func (e *errorAfterNReader) Read(p []byte) (n int, err error) { return 0, e.err } -func TestCompat_AskYesOrNo(t *testing.T) { +func TestAskYesOrNo(t *testing.T) { tests := []struct { name string input string diff --git a/libs/cmdio/io.go b/libs/cmdio/io.go index d4c4f42e27..c8c61e47f4 100644 --- a/libs/cmdio/io.go +++ b/libs/cmdio/io.go @@ -2,16 +2,13 @@ package cmdio import ( "context" - "fmt" "io" "os" - "slices" "strings" "sync" tea "github.com/charmbracelet/bubbletea" "github.com/databricks/cli/libs/flags" - "github.com/manifoldco/promptui" ) // cmdIO is the private instance, that is not supposed to be accessed @@ -72,74 +69,6 @@ func GetInteractiveMode(ctx context.Context) InteractiveMode { return c.capabilities.InteractiveMode() } -type Tuple struct{ Name, Id string } - -func (c *cmdIO) Select(items []Tuple, label string) (id string, err error) { - if !c.capabilities.SupportsInteractive() { - return "", fmt.Errorf("expected to have %s", label) - } - - idx, _, err := (&promptui.Select{ - Label: label, - Items: items, - HideSelected: true, - StartInSearchMode: true, - Searcher: func(input string, idx int) bool { - lower := strings.ToLower(items[idx].Name) - return strings.Contains(lower, strings.ToLower(input)) - }, - Templates: &promptui.SelectTemplates{ - Active: `{{.Name | bold}} ({{.Id|faint}})`, - Inactive: `{{.Name}}`, - }, - Stdin: c.promptStdin(), - Stdout: nopWriteCloser{c.err}, - }).Run() - if err != nil { - return id, err - } - id = items[idx].Id - return id, err -} - -// Show a selection prompt where the user can pick one of the name/id items. -// The items are sorted alphabetically by name. -func Select[V any](ctx context.Context, names map[string]V, label string) (id string, err error) { - c := fromContext(ctx) - var items []Tuple - for k, v := range names { - items = append(items, Tuple{k, fmt.Sprint(v)}) - } - slices.SortFunc(items, func(a, b Tuple) int { - return strings.Compare(a.Name, b.Name) - }) - return c.Select(items, label) -} - -// Show a selection prompt where the user can pick one of the name/id items. -// The items appear in the order specified in the "items" argument. -func SelectOrdered(ctx context.Context, items []Tuple, label string) (id string, err error) { - c := fromContext(ctx) - return c.Select(items, label) -} - -func (c *cmdIO) Secret(label string) (value string, err error) { - prompt := (promptui.Prompt{ - Label: label, - Mask: '*', - HideEntered: true, - Stdin: c.promptStdin(), - Stdout: nopWriteCloser{c.err}, - }) - - return prompt.Run() -} - -func Secret(ctx context.Context, label string) (value string, err error) { - c := fromContext(ctx) - return c.Secret(label) -} - // promptStdin returns the stdin reader for use with promptui. // If the reader is os.Stdin, it returns nil to let the underlying readline // library use its platform-specific default. On Windows, this is critical diff --git a/libs/cmdio/log.go b/libs/cmdio/log.go new file mode 100644 index 0000000000..cdf33171f3 --- /dev/null +++ b/libs/cmdio/log.go @@ -0,0 +1,18 @@ +package cmdio + +import ( + "context" + "fmt" + "io" +) + +// Log calls [LogString] with the string representation of str. +func Log(ctx context.Context, str fmt.Stringer) { + LogString(ctx, str.String()) +} + +// LogString writes str to the error writer, followed by a newline. +func LogString(ctx context.Context, str string) { + c := fromContext(ctx) + _, _ = io.WriteString(c.err, str+"\n") +} diff --git a/libs/cmdio/prompt.go b/libs/cmdio/prompt.go index dda4d67c0e..760a99af4a 100644 --- a/libs/cmdio/prompt.go +++ b/libs/cmdio/prompt.go @@ -18,6 +18,9 @@ type PromptOptions struct { // (use '*' for password-style input). Mask rune + // HideEntered hides the entered value after the prompt closes. + HideEntered bool + // Validate, when set, is called on every keystroke; returning a non-nil // error keeps the prompt open and shows the error to the user. Validate func(input string) error @@ -27,12 +30,23 @@ type PromptOptions struct { func RunPrompt(ctx context.Context, opts PromptOptions) (string, error) { c := fromContext(ctx) p := promptui.Prompt{ - Label: opts.Label, - Default: opts.Default, - Mask: opts.Mask, - Validate: opts.Validate, - Stdin: c.promptStdin(), - Stdout: nopWriteCloser{c.err}, + Label: opts.Label, + Default: opts.Default, + Mask: opts.Mask, + HideEntered: opts.HideEntered, + Validate: opts.Validate, + Stdin: c.promptStdin(), + Stdout: nopWriteCloser{c.err}, } return p.Run() } + +// Secret prompts the user for a value while masking input with '*' and hiding +// the entered value after submission. +func Secret(ctx context.Context, label string) (string, error) { + return RunPrompt(ctx, PromptOptions{ + Label: label, + Mask: '*', + HideEntered: true, + }) +} diff --git a/libs/cmdio/select.go b/libs/cmdio/select.go index f541f97eb7..c295e76345 100644 --- a/libs/cmdio/select.go +++ b/libs/cmdio/select.go @@ -2,6 +2,9 @@ package cmdio import ( "context" + "fmt" + "slices" + "strings" "github.com/manifoldco/promptui" ) @@ -26,6 +29,9 @@ type SelectOptions struct { // HideHelp hides the navigation help line shown by promptui by default. HideHelp bool + // HideSelected hides the rendered selection after the prompt closes. + HideSelected bool + // LabelTemplate renders Label. Empty uses the default. LabelTemplate string @@ -48,6 +54,7 @@ func RunSelect(ctx context.Context, opts SelectOptions) (int, error) { Searcher: opts.Searcher, StartInSearchMode: opts.StartInSearchMode, HideHelp: opts.HideHelp, + HideSelected: opts.HideSelected, Templates: &promptui.SelectTemplates{ Label: opts.LabelTemplate, Active: opts.Active, @@ -60,3 +67,43 @@ func RunSelect(ctx context.Context, opts SelectOptions) (int, error) { idx, _, err := sel.Run() return idx, err } + +type Tuple struct{ Name, Id string } + +// Select shows a selection prompt where the user can pick one of the name/id +// items. The items are sorted alphabetically by name. +func Select[V any](ctx context.Context, names map[string]V, label string) (string, error) { + items := make([]Tuple, 0, len(names)) + for k, v := range names { + items = append(items, Tuple{k, fmt.Sprint(v)}) + } + slices.SortFunc(items, func(a, b Tuple) int { + return strings.Compare(a.Name, b.Name) + }) + return SelectOrdered(ctx, items, label) +} + +// SelectOrdered shows a selection prompt where the user can pick one of the +// name/id items. The items appear in the order specified in the "items" +// argument. +func SelectOrdered(ctx context.Context, items []Tuple, label string) (string, error) { + c := fromContext(ctx) + if !c.capabilities.SupportsInteractive() { + return "", fmt.Errorf("expected to have %s", label) + } + idx, err := RunSelect(ctx, SelectOptions{ + Label: label, + Items: items, + HideSelected: true, + StartInSearchMode: true, + Searcher: func(input string, idx int) bool { + return strings.Contains(strings.ToLower(items[idx].Name), strings.ToLower(input)) + }, + Active: `{{.Name | bold}} ({{.Id|faint}})`, + Inactive: `{{.Name}}`, + }) + if err != nil { + return "", err + } + return items[idx].Id, nil +}