Skip to content

Commit cd6aecd

Browse files
authored
cmdio: replace promptui with bubbletea-backed Prompt and Select (#5232)
## Summary - Reimplement `RunPrompt` and `RunSelect` as bubbletea models that reproduce promptui's rendering and key handling end-to-end. `RunPrompt` supports cursor editing, mask, validate (with inline glyph and `>> <err>` line surfaced after a failed Enter), `HideEntered`, Delete/Ctrl+D as EOF, and the Ctrl+B/F/H/J aliases. `RunSelect` supports templated rows, viewport scroll, filter with `/` toggle and vim-style nav, Ctrl+P/N item nav and Ctrl+B/F page nav, and an empty post-submit frame when `HideSelected` is set. - Drop `manifoldco/promptui` and the transitive `chzyer/readline` dependency. - Move the `SupportsPrompt` capability check into the primitives themselves so callers no longer have to gate on `IsPromptSupported`; `SelectOrdered` drops its now-redundant guard. `SelectOptions.Items` is validated at construction and normalized to `[]any` so the render path doesn't reflect on every row. ## Test plan - Behavior is verified against the cmdiotest pty/vt10x baseline suite developed in #5231. That suite is kept on a separate branch and **not merged here** because it pulls in test-only deps (`creack/pty`, `hinshun/vt10x`) we'd prefer not to land in the main module. - Side-by-side check of `databricks selftest tui` commands against the released CLI: same visible output, same exit codes.
1 parent dec925d commit cd6aecd

11 files changed

Lines changed: 685 additions & 86 deletions

File tree

NOTICE

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ google/uuid - https://github.com/google/uuid
6666
Copyright (c) 2009,2014 Google Inc. All rights reserved.
6767
License - https://github.com/google/uuid/blob/master/LICENSE
6868

69-
manifoldco/promptui - https://github.com/manifoldco/promptui
70-
Copyright (c) 2017, Arigato Machine Inc. All rights reserved.
71-
License - https://github.com/manifoldco/promptui/blob/master/LICENSE.md
72-
7369
hexops/gotextdiff - https://github.com/hexops/gotextdiff
7470
Copyright (c) 2009 The Go Authors. All rights reserved.
7571
License - https://github.com/hexops/gotextdiff/blob/main/LICENSE

experimental/ssh/internal/setup/setup_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func TestSetup_PromptsForClusterWhenNotProvided(t *testing.T) {
295295
configPath := filepath.Join(tmpDir, "ssh_config")
296296

297297
// Replace the cluster picker with a stub returning a fixed ID. This lets the
298-
// test exercise the empty-ClusterID path of Setup without driving promptui.
298+
// test exercise the empty-ClusterID path of Setup without prompting.
299299
origPrompt := clusterSelectionPrompt
300300
t.Cleanup(func() { clusterSelectionPrompt = origPrompt })
301301
promptCalled := false

go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ require (
2222
github.com/hashicorp/terraform-json v0.27.2 // MPL-2.0
2323
github.com/hexops/gotextdiff v1.0.3 // BSD-3-Clause
2424
github.com/jackc/pgx/v5 v5.9.2 // MIT
25-
github.com/manifoldco/promptui v0.9.0 // BSD-3-Clause
2625
github.com/mattn/go-isatty v0.0.22 // MIT
2726
github.com/palantir/pkg/yamlpatch v1.5.0 // BSD-3-Clause
2827
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // BSD-2-Clause
@@ -58,7 +57,6 @@ require (
5857
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
5958
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
6059
github.com/charmbracelet/x/term v0.2.2 // indirect
61-
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
6260
github.com/clipperhouse/displaywidth v0.9.0 // indirect
6361
github.com/clipperhouse/stringish v0.1.1 // indirect
6462
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect

go.sum

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
5656
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
5757
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
5858
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
59-
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
60-
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
61-
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
62-
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
63-
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
64-
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
6559
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
6660
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
6761
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
@@ -162,8 +156,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
162156
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
163157
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
164158
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
165-
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
166-
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
167159
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
168160
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
169161
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
@@ -261,7 +253,6 @@ golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
261253
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
262254
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
263255
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
264-
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
265256
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
266257
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
267258
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=

libs/cmdio/capabilities.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,11 @@ func (c Capabilities) SupportsPager() bool {
6363
return c.SupportsPrompt() && c.stdoutIsTTY
6464
}
6565

66-
// detectGitBash returns true if running in Git Bash on Windows (has broken promptui support).
67-
// We do not allow prompting in Git Bash on Windows.
68-
// Likely due to fact that Git Bash does not correctly support ANSI escape sequences,
69-
// we cannot use promptui package there.
70-
// See known issues:
71-
// - https://github.com/manifoldco/promptui/issues/208
72-
// - https://github.com/chzyer/readline/issues/191
66+
// detectGitBash returns true if running under a Cygwin/MSYS2 environment on
67+
// Windows (Git Bash is the common case).
68+
//
69+
// We disable prompting there because bubbletea is not compatible with the
70+
// Cygwin/MSYS2 pty emulation; making it work is a follow-up.
7371
func detectGitBash(ctx context.Context) bool {
7472
// Check if the MSYSTEM environment variable is set to "MINGW64"
7573
msystem := env.Get(ctx, "MSYSTEM")

libs/cmdio/color.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
const (
1212
ansiReset = "\x1b[0m"
1313
ansiBold = "\x1b[1m"
14+
ansiFaint = "\x1b[2m"
1415
ansiItalic = "\x1b[3m"
16+
ansiUnderline = "\x1b[4m"
1517
ansiRed = "\x1b[31m"
1618
ansiGreen = "\x1b[32m"
1719
ansiYellow = "\x1b[33m"

libs/cmdio/io.go

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,33 @@ package cmdio
22

33
import (
44
"context"
5+
"errors"
56
"io"
6-
"os"
77
"strings"
88
"sync"
99

1010
tea "github.com/charmbracelet/bubbletea"
1111
"github.com/databricks/cli/libs/flags"
1212
)
1313

14+
// errCtrlC is returned when the user cancels a TUI prompt with Ctrl+C. The
15+
// "^C" string matches the historical wire format; goldens depend on it.
16+
var errCtrlC = errors.New("^C")
17+
18+
// runTUI runs a tea.Program through cmdIO's tea program slot so spinners and
19+
// pagers can't fight a prompt for the terminal. Blocks until the model quits.
20+
func (c *cmdIO) runTUI(m tea.Model) (tea.Model, error) {
21+
p := tea.NewProgram(m,
22+
tea.WithInput(c.in),
23+
tea.WithOutput(c.err),
24+
// Ctrl+C is delivered as a key event so the model can return errCtrlC.
25+
tea.WithoutSignalHandler(),
26+
)
27+
c.acquireTeaProgram(p)
28+
defer c.releaseTeaProgram()
29+
return p.Run()
30+
}
31+
1432
// cmdIO is the private instance, that is not supposed to be accessed
1533
// outside of `cmdio` package. Use the public package-level functions
1634
// to access the inner state.
@@ -69,27 +87,6 @@ func GetInteractiveMode(ctx context.Context) InteractiveMode {
6987
return c.capabilities.InteractiveMode()
7088
}
7189

72-
// promptStdin returns the stdin reader for use with promptui.
73-
// If the reader is os.Stdin, it returns nil to let the underlying readline
74-
// library use its platform-specific default. On Windows, this is critical
75-
// because readline's default uses ReadConsoleInputW to read arrow keys
76-
// as virtual key events. Passing a wrapped os.Stdin would bypass this
77-
// and break arrow key navigation in selection prompts.
78-
func (c *cmdIO) promptStdin() io.ReadCloser {
79-
if c.in == os.Stdin {
80-
return nil
81-
}
82-
return io.NopCloser(c.in)
83-
}
84-
85-
type nopWriteCloser struct {
86-
io.Writer
87-
}
88-
89-
func (nopWriteCloser) Close() error {
90-
return nil
91-
}
92-
9390
// NewSpinner creates a new spinner for displaying progress indicators.
9491
// The returned spinner should be closed when done to release resources.
9592
//

0 commit comments

Comments
 (0)