Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 5 additions & 21 deletions libs/cmdio/compat.go → libs/cmdio/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions libs/cmdio/compat_test.go → libs/cmdio/ask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
71 changes: 0 additions & 71 deletions libs/cmdio/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions libs/cmdio/log.go
Original file line number Diff line number Diff line change
@@ -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")
}
26 changes: 20 additions & 6 deletions libs/cmdio/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
})
}
47 changes: 47 additions & 0 deletions libs/cmdio/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package cmdio

import (
"context"
"fmt"
"slices"
"strings"

"github.com/manifoldco/promptui"
)
Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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
}
Loading