From e4455e936aa367752669647246ca6221be24ea64 Mon Sep 17 00:00:00 2001 From: Anton Ovchinnikov Date: Mon, 28 Sep 2026 02:16:45 +0200 Subject: [PATCH] fix(cli): stop leaking terminal capability replies into the shell (#435) * chore(deps): bump bubbletea to v2.0.10, drop the uncloud fork v2.0.10 carries the upstream fix for terminal capability replies leaking into the shell (charmbracelet/bubbletea#1801). It skips every query that expects a reply when input is disabled: the startup probe for synchronized output and Unicode core, clipboard reads, background, foreground and cursor colour requests, cursor position reports, and terminal version and termcap queries. That supersedes the unlabs-dev fork, whose patch covered only the startup probe, so the replace directive goes away and uncloud tracks upstream again. The bump on its own does not fix the leak. Bubble Tea skips the queries only when a program declares that it has no input, and nothing in uncloud declares that yet. Measured: this tree still leaks "^[[?2026;2$y^[[?2027;0$y" under a PTY harness. The next commit is what makes the spinners declare it. Refs: #286, charmbracelet/bubbletea#1801 --- go.mod | 6 ++---- go.sum | 8 ++++---- internal/cli/connect.go | 19 +++++++++++-------- internal/cli/tui/spinner.go | 2 ++ 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index d0e3c690..b5e1a116 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( charm.land/bubbles/v2 v2.0.0 - charm.land/bubbletea/v2 v2.0.2 + charm.land/bubbletea/v2 v2.0.10 charm.land/huh/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.1 github.com/BurntSushi/toml v1.6.0 @@ -90,7 +90,7 @@ require ( github.com/catppuccin/go v0.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect github.com/charmbracelet/x/term v0.2.2 // indirect @@ -301,5 +301,3 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) - -replace charm.land/bubbletea/v2 => github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1 diff --git a/go.sum b/go.sum index 1b943443..aa5c3192 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= +charm.land/bubbletea/v2 v2.0.10 h1:oolvo20VBpI0PfqE7iFjkZ1bx0WpmXGfnKz5Yldjq5o= +charm.land/bubbletea/v2 v2.0.10/go.mod h1:QOatcnhOjYIfxzUSTz6raF7Ex4R/rIuHa3SnBdCCpMc= charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98= charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys= @@ -127,8 +129,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek= -github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= @@ -689,8 +691,6 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= -github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1 h1:898LVw5QvmRX/y+pgzCo2pE/NbN9CZfdpiVnzAeAFDY= -github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= diff --git a/internal/cli/connect.go b/internal/cli/connect.go index 59151c31..632fef10 100644 --- a/internal/cli/connect.go +++ b/internal/cli/connect.go @@ -45,7 +45,17 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti } // Run the connection TUI model. Render to stderr so stdout stays clean for command output. - p := tea.NewProgram(newConnectModel(ctx, conn), tea.WithOutput(os.Stderr)) + p := tea.NewProgram(newConnectModel(ctx, conn), + tea.WithOutput(os.Stderr), + tea.WithInput(nil), + // Ctrl+C handling + tea.WithFilter(func(_ tea.Model, msg tea.Msg) tea.Msg { + if _, ok := msg.(tea.InterruptMsg); ok { + return connectResultMsg{err: errors.New("connection cancelled")} + } + return msg + }), + ) model, err := p.Run() if err != nil { return nil, fmt.Errorf("run connection TUI: %w", err) @@ -190,13 +200,6 @@ func (m connectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd - - case tea.KeyPressMsg: - if msg.String() == "ctrl+c" { - m.result.err = fmt.Errorf("connection cancelled") - m.done = true - return m, tea.Quit - } } return m, nil diff --git a/internal/cli/tui/spinner.go b/internal/cli/tui/spinner.go index 10ec79f3..2383dba2 100644 --- a/internal/cli/tui/spinner.go +++ b/internal/cli/tui/spinner.go @@ -31,6 +31,8 @@ func RunSpinner(ctx context.Context, title string, action func(ctx context.Conte } })). WithOutput(os.Stderr). + // A spinner reads no keys, saying so makes Bubble Tea skip unnecessary terminal capability queries. + WithInput(nil). Context(ctx). ActionWithErr(action). Run()