Skip to content

Commit 55f2cab

Browse files
authored
cmdio: relocate helpers into dedicated files; build Secret/Select on Run* (#5221)
## Summary - `compat.go` is gone. `Log`/`LogString` move to `log.go`, and `readLine`/`Ask`/`AskYesOrNo` move to `ask.go`. The "compatibility layer" doc comments (a transitional shim from #3818) are replaced with descriptions of what each function does, since these are now their permanent home. - `Tuple`/`Select`/`SelectOrdered` move from `io.go` to `select.go` and are reimplemented on top of `RunSelect`. A new `HideSelected` option on `SelectOptions` preserves the existing post-prompt display behavior. - `Secret` moves from `io.go` to `prompt.go` and becomes a thin wrapper over `RunPrompt`. A new `HideEntered` option on `PromptOptions` carries the masked-input post-submission behavior. ## Result The `github.com/manifoldco/promptui` import is confined to `libs/cmdio/prompt.go` and `libs/cmdio/select.go`; every other prompt and selection helper in the package builds on those two entry points. ## Test plan - [x] Manual: `databricks bundle run` from a bundle with multiple resources triggers the resource picker (`cmdio.Select`). - [x] Manual: `databricks secrets put-secret <scope> <key>` triggers the masked secret prompt (`cmdio.Secret`).
1 parent 3fe2d04 commit 55f2cab

6 files changed

Lines changed: 92 additions & 100 deletions

File tree

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,6 @@ import (
77
"strings"
88
)
99

10-
/*
11-
Temporary compatibility layer for the progress logger interfaces.
12-
*/
13-
14-
// Log is a compatibility layer for the progress logger interfaces.
15-
// It writes the string representation of the stringer to the error writer.
16-
func Log(ctx context.Context, str fmt.Stringer) {
17-
LogString(ctx, str.String())
18-
}
19-
20-
// LogString is a compatibility layer for the progress logger interfaces.
21-
// It writes the string to the error writer.
22-
func LogString(ctx context.Context, str string) {
23-
c := fromContext(ctx)
24-
_, _ = io.WriteString(c.err, str+"\n")
25-
}
26-
2710
// readLine reads a line from the reader and returns it without the trailing newline characters.
2811
// It is unbuffered because cmdio's stdin is also unbuffered.
2912
// 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) {
5134
return b.String(), nil
5235
}
5336

54-
// Ask is a compatibility layer for the progress logger interfaces.
55-
// It prompts the user with a question and returns the answer.
37+
// Ask prompts the user with a question and returns the entered answer.
38+
// If the user just presses enter, defaultVal is returned.
5639
func Ask(ctx context.Context, question, defaultVal string) (string, error) {
5740
c := fromContext(ctx)
5841

@@ -82,8 +65,9 @@ func Ask(ctx context.Context, question, defaultVal string) (string, error) {
8265
return ans, nil
8366
}
8467

85-
// AskYesOrNo is a compatibility layer for the progress logger interfaces.
86-
// It prompts the user with a question and returns the answer.
68+
// AskYesOrNo prompts the user with a question and returns true if the answer
69+
// is "y" or "yes" (case-insensitive). Any other answer, including an empty
70+
// one, returns false.
8771
func AskYesOrNo(ctx context.Context, question string) (bool, error) {
8872
ans, err := Ask(ctx, question+" [y/N]", "")
8973
if err != nil {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/stretchr/testify/require"
1212
)
1313

14-
func TestCompat_readLine(t *testing.T) {
14+
func TestReadLine(t *testing.T) {
1515
tests := []struct {
1616
name string
1717
reader io.Reader
@@ -147,7 +147,7 @@ func (e *errorAfterNReader) Read(p []byte) (n int, err error) {
147147
return 0, e.err
148148
}
149149

150-
func TestCompat_AskYesOrNo(t *testing.T) {
150+
func TestAskYesOrNo(t *testing.T) {
151151
tests := []struct {
152152
name string
153153
input string

libs/cmdio/io.go

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@ package cmdio
22

33
import (
44
"context"
5-
"fmt"
65
"io"
76
"os"
8-
"slices"
97
"strings"
108
"sync"
119

1210
tea "github.com/charmbracelet/bubbletea"
1311
"github.com/databricks/cli/libs/flags"
14-
"github.com/manifoldco/promptui"
1512
)
1613

1714
// cmdIO is the private instance, that is not supposed to be accessed
@@ -72,74 +69,6 @@ func GetInteractiveMode(ctx context.Context) InteractiveMode {
7269
return c.capabilities.InteractiveMode()
7370
}
7471

75-
type Tuple struct{ Name, Id string }
76-
77-
func (c *cmdIO) Select(items []Tuple, label string) (id string, err error) {
78-
if !c.capabilities.SupportsInteractive() {
79-
return "", fmt.Errorf("expected to have %s", label)
80-
}
81-
82-
idx, _, err := (&promptui.Select{
83-
Label: label,
84-
Items: items,
85-
HideSelected: true,
86-
StartInSearchMode: true,
87-
Searcher: func(input string, idx int) bool {
88-
lower := strings.ToLower(items[idx].Name)
89-
return strings.Contains(lower, strings.ToLower(input))
90-
},
91-
Templates: &promptui.SelectTemplates{
92-
Active: `{{.Name | bold}} ({{.Id|faint}})`,
93-
Inactive: `{{.Name}}`,
94-
},
95-
Stdin: c.promptStdin(),
96-
Stdout: nopWriteCloser{c.err},
97-
}).Run()
98-
if err != nil {
99-
return id, err
100-
}
101-
id = items[idx].Id
102-
return id, err
103-
}
104-
105-
// Show a selection prompt where the user can pick one of the name/id items.
106-
// The items are sorted alphabetically by name.
107-
func Select[V any](ctx context.Context, names map[string]V, label string) (id string, err error) {
108-
c := fromContext(ctx)
109-
var items []Tuple
110-
for k, v := range names {
111-
items = append(items, Tuple{k, fmt.Sprint(v)})
112-
}
113-
slices.SortFunc(items, func(a, b Tuple) int {
114-
return strings.Compare(a.Name, b.Name)
115-
})
116-
return c.Select(items, label)
117-
}
118-
119-
// Show a selection prompt where the user can pick one of the name/id items.
120-
// The items appear in the order specified in the "items" argument.
121-
func SelectOrdered(ctx context.Context, items []Tuple, label string) (id string, err error) {
122-
c := fromContext(ctx)
123-
return c.Select(items, label)
124-
}
125-
126-
func (c *cmdIO) Secret(label string) (value string, err error) {
127-
prompt := (promptui.Prompt{
128-
Label: label,
129-
Mask: '*',
130-
HideEntered: true,
131-
Stdin: c.promptStdin(),
132-
Stdout: nopWriteCloser{c.err},
133-
})
134-
135-
return prompt.Run()
136-
}
137-
138-
func Secret(ctx context.Context, label string) (value string, err error) {
139-
c := fromContext(ctx)
140-
return c.Secret(label)
141-
}
142-
14372
// promptStdin returns the stdin reader for use with promptui.
14473
// If the reader is os.Stdin, it returns nil to let the underlying readline
14574
// library use its platform-specific default. On Windows, this is critical

libs/cmdio/log.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package cmdio
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
)
8+
9+
// Log calls [LogString] with the string representation of str.
10+
func Log(ctx context.Context, str fmt.Stringer) {
11+
LogString(ctx, str.String())
12+
}
13+
14+
// LogString writes str to the error writer, followed by a newline.
15+
func LogString(ctx context.Context, str string) {
16+
c := fromContext(ctx)
17+
_, _ = io.WriteString(c.err, str+"\n")
18+
}

libs/cmdio/prompt.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ type PromptOptions struct {
1818
// (use '*' for password-style input).
1919
Mask rune
2020

21+
// HideEntered hides the entered value after the prompt closes.
22+
HideEntered bool
23+
2124
// Validate, when set, is called on every keystroke; returning a non-nil
2225
// error keeps the prompt open and shows the error to the user.
2326
Validate func(input string) error
@@ -27,12 +30,23 @@ type PromptOptions struct {
2730
func RunPrompt(ctx context.Context, opts PromptOptions) (string, error) {
2831
c := fromContext(ctx)
2932
p := promptui.Prompt{
30-
Label: opts.Label,
31-
Default: opts.Default,
32-
Mask: opts.Mask,
33-
Validate: opts.Validate,
34-
Stdin: c.promptStdin(),
35-
Stdout: nopWriteCloser{c.err},
33+
Label: opts.Label,
34+
Default: opts.Default,
35+
Mask: opts.Mask,
36+
HideEntered: opts.HideEntered,
37+
Validate: opts.Validate,
38+
Stdin: c.promptStdin(),
39+
Stdout: nopWriteCloser{c.err},
3640
}
3741
return p.Run()
3842
}
43+
44+
// Secret prompts the user for a value while masking input with '*' and hiding
45+
// the entered value after submission.
46+
func Secret(ctx context.Context, label string) (string, error) {
47+
return RunPrompt(ctx, PromptOptions{
48+
Label: label,
49+
Mask: '*',
50+
HideEntered: true,
51+
})
52+
}

libs/cmdio/select.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package cmdio
22

33
import (
44
"context"
5+
"fmt"
6+
"slices"
7+
"strings"
58

69
"github.com/manifoldco/promptui"
710
)
@@ -26,6 +29,9 @@ type SelectOptions struct {
2629
// HideHelp hides the navigation help line shown by promptui by default.
2730
HideHelp bool
2831

32+
// HideSelected hides the rendered selection after the prompt closes.
33+
HideSelected bool
34+
2935
// LabelTemplate renders Label. Empty uses the default.
3036
LabelTemplate string
3137

@@ -48,6 +54,7 @@ func RunSelect(ctx context.Context, opts SelectOptions) (int, error) {
4854
Searcher: opts.Searcher,
4955
StartInSearchMode: opts.StartInSearchMode,
5056
HideHelp: opts.HideHelp,
57+
HideSelected: opts.HideSelected,
5158
Templates: &promptui.SelectTemplates{
5259
Label: opts.LabelTemplate,
5360
Active: opts.Active,
@@ -60,3 +67,43 @@ func RunSelect(ctx context.Context, opts SelectOptions) (int, error) {
6067
idx, _, err := sel.Run()
6168
return idx, err
6269
}
70+
71+
type Tuple struct{ Name, Id string }
72+
73+
// Select shows a selection prompt where the user can pick one of the name/id
74+
// items. The items are sorted alphabetically by name.
75+
func Select[V any](ctx context.Context, names map[string]V, label string) (string, error) {
76+
items := make([]Tuple, 0, len(names))
77+
for k, v := range names {
78+
items = append(items, Tuple{k, fmt.Sprint(v)})
79+
}
80+
slices.SortFunc(items, func(a, b Tuple) int {
81+
return strings.Compare(a.Name, b.Name)
82+
})
83+
return SelectOrdered(ctx, items, label)
84+
}
85+
86+
// SelectOrdered shows a selection prompt where the user can pick one of the
87+
// name/id items. The items appear in the order specified in the "items"
88+
// argument.
89+
func SelectOrdered(ctx context.Context, items []Tuple, label string) (string, error) {
90+
c := fromContext(ctx)
91+
if !c.capabilities.SupportsInteractive() {
92+
return "", fmt.Errorf("expected to have %s", label)
93+
}
94+
idx, err := RunSelect(ctx, SelectOptions{
95+
Label: label,
96+
Items: items,
97+
HideSelected: true,
98+
StartInSearchMode: true,
99+
Searcher: func(input string, idx int) bool {
100+
return strings.Contains(strings.ToLower(items[idx].Name), strings.ToLower(input))
101+
},
102+
Active: `{{.Name | bold}} ({{.Id|faint}})`,
103+
Inactive: `{{.Name}}`,
104+
})
105+
if err != nil {
106+
return "", err
107+
}
108+
return items[idx].Id, nil
109+
}

0 commit comments

Comments
 (0)