Skip to content

Commit 7ba2fc7

Browse files
authored
ui: migrate charmbracelet stack to v2 (#1054)
Upgrades to charmbracelet's v2 libraries (lipgloss, bubbletea, and bubbles, https://charm.land/blog/v2/). This required significantly reworking our rendering pipeline, as well as some workarounds on the test/simulation side to accommodate improvements made on the Bubble Tea side. ## Render-time styling Upgrade the TUI stack to `bubbles`, `bubbletea`, and `lipgloss` v2, and reshape the rendering pipeline around the API changes those releases require. Lip Gloss v2 resolves adaptive colors against the active output stream at render time, so the UI now carries semantic `ui.Style` and `ui.Theme` values until it writes output. That keeps call sites expressive while letting the output layer decide how much styling the destination can support. ## Bubble Tea v2 integration Bubble Tea v2 also changes the terminal contract. Forms now implement `View() tea.View`, key handling moves to `tea.KeyPressMsg`, and startup sizing is configured through program options. Those updates are mostly mechanical, but they require the interactive layer to pass theme and window-size state through every field consistently. ## Test harness and terminal emulation The migration also forced the scripted UI harnesses to behave more like real terminals. The emulator now preserves raw-terminal newline semantics, treats tabs as cursor movement, and waits for the screen to settle before assertions snapshot the result. That keeps Bubble Tea v2 redraw bursts from leaking intermediate frames into fixtures. Robot-driven prompt fixtures need the same output-capability handling. `lipgloss.Style.Render` still returns ANSI sequences even when tests are recording prompt text into plain files. `uitest.RobotView` now captures prompt and log output through a `colorprofile.Writer` forced to `NoTTY`, so script fixtures observe the same unstyled text that a non-terminal destination would receive. A regression test locks that behavior down for future UI changes. The PTY-based `with-term` harness also needs a deterministic theme. Its scripted terminal does not answer background-color probes, so detecting the active theme can block interactive startup before the first prompt is drawn. `ui.detectTheme` now honors a test-only `__GIT_SPICE_THEME` override, and `with-term` sets it explicitly so PTY-driven script tests can boot without stalling on terminal queries. ## Output capability detection The migration also changes how non-interactive output is written. `ui.NewFileView` wraps outputs with `colorprofile.Writer` and records the detected theme alongside the writer. That preserves colored branch-tree and log output when the destination supports it, while still stripping or down-sampling styles when it does not.
1 parent 228eb05 commit 7ba2fc7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1597
-493
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Changed
2+
body: >-
3+
Upgrade TUI rendering to v2 of underlying libraries.
4+
No user-facing changes are expected, but significant internal refactoring was required.
5+
If you encounter unexpected side effects, please [report them here](https://github.com/abhinav/git-spice/issues).
6+
time: 2026-03-10T14:28:52.586798-07:00

.golangci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ linters:
3636
- fmt.Fprintf
3737
- fmt.Fprintln
3838

39+
# Usually used in the same capacity as fmt.Print*.
40+
- charm.land/lipgloss/v2.Fprint
41+
- charm.land/lipgloss/v2.Fprintf
42+
- charm.land/lipgloss/v2.Fprintln
43+
3944
# This is always a strings.Builder, and can't fail.
4045
- (go.abhg.dev/gs/internal/ui.Writer).WriteString
4146
- (go.abhg.dev/gs/internal/ui.Writer).Write

branch_split.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,9 @@ func (cmd *branchSplitCmd) Run(
149149
}
150150

151151
fields := make([]ui.Field, 0, len(selected)+1) // +1 for deferred HEAD field
152+
theme := view.Theme()
152153
for i, commit := range selected {
153-
desc := cmd.commitDescription(commit, false /* head */)
154+
desc := cmd.commitDescription(theme, commit, false /* head */)
154155
input := branchNameWidget(desc, &branchNames[i])
155156
fields = append(fields, input)
156157
}
@@ -168,7 +169,7 @@ func (cmd *branchSplitCmd) Run(
168169
return nil
169170
}
170171

171-
desc := cmd.commitDescription(headCommit, true /* head */) +
172+
desc := cmd.commitDescription(theme, headCommit, true /* head */) +
172173
" [" + cmd.Branch + "]"
173174
return branchNameWidget(desc, &headBranchName)
174175
}))
@@ -213,7 +214,11 @@ func (cmd *branchSplitCmd) Run(
213214
return nil
214215
}
215216

216-
func (cmd *branchSplitCmd) commitDescription(c git.CommitDetail, head bool) string {
217+
func (cmd *branchSplitCmd) commitDescription(
218+
theme ui.Theme,
219+
c git.CommitDetail,
220+
head bool,
221+
) string {
217222
var desc strings.Builder
218223
if head {
219224
desc.WriteString(" ■ ")
@@ -224,7 +229,7 @@ func (cmd *branchSplitCmd) commitDescription(c git.CommitDetail, head bool) stri
224229
ShortHash: c.ShortHash,
225230
Subject: c.Subject,
226231
AuthorDate: c.AuthorDate,
227-
}).Render(&desc, commit.DefaultSummaryStyle, nil)
232+
}).Render(&desc, theme, commit.DefaultSummaryStyle, nil)
228233

229234
return desc.String()
230235
}

commit_amend.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ func (cmd *commitAmendCmd) Run(
8888
WithItems(
8989
ui.ListItem[bool]{
9090
Title: "Yes",
91-
Description: func(bool) string {
91+
Description: func(ui.Theme, bool) string {
9292
return "Amend the commit on trunk"
9393
},
9494
Value: true,
9595
},
9696
ui.ListItem[bool]{
9797
Title: "No",
98-
Description: func(bool) string {
98+
Description: func(ui.Theme, bool) string {
9999
return "Create a branch and commit there instead"
100100
},
101101
Value: false,
@@ -162,12 +162,12 @@ func (cmd *commitAmendCmd) Run(
162162
WithItems(
163163
ui.ListItem[bool]{
164164
Title: "Yes",
165-
Description: func(bool) string { return "Continue with commit amend" },
165+
Description: func(ui.Theme, bool) string { return "Continue with commit amend" },
166166
Value: true,
167167
},
168168
ui.ListItem[bool]{
169169
Title: "No",
170-
Description: func(bool) string { return "Abort the operation" },
170+
Description: func(ui.Theme, bool) string { return "Abort the operation" },
171171
Value: false,
172172
},
173173
).

commit_pick.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,14 @@ func (cmd *commitPickCmd) Run(
9494
WithItems(
9595
ui.ListItem[bool]{
9696
Title: "Yes",
97-
Description: func(bool) string {
97+
Description: func(ui.Theme, bool) string {
9898
return fmt.Sprintf("Cherry-pick commit %v on trunk", commit.Short())
9999
},
100100
Value: true,
101101
},
102102
ui.ListItem[bool]{
103103
Title: "No",
104-
Description: func(bool) string {
104+
Description: func(ui.Theme, bool) string {
105105
return "Abort the operation"
106106
},
107107
Value: false,

doc/mise.lock

Lines changed: 57 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ module go.abhg.dev/gs
33
go 1.26.0
44

55
require (
6+
charm.land/bubbles/v2 v2.0.0
7+
charm.land/bubbletea/v2 v2.0.1
8+
charm.land/lipgloss/v2 v2.0.0
69
github.com/alecthomas/kong v1.14.0
710
github.com/buildkite/shellwords v1.0.1
8-
github.com/charmbracelet/bubbles v1.0.0
9-
github.com/charmbracelet/bubbletea v1.3.10
10-
github.com/charmbracelet/lipgloss v1.1.0
11+
github.com/charmbracelet/colorprofile v0.4.3
12+
github.com/charmbracelet/x/ansi v0.11.6
13+
github.com/charmbracelet/x/term v0.2.2
1114
github.com/cli/browser v1.3.0
1215
github.com/creack/pty v1.1.24
1316
github.com/dustin/go-humanize v1.0.1
@@ -18,13 +21,13 @@ require (
1821
github.com/shurcooL/githubv4 v0.0.0-20260209031235-2402fdf4a9ed
1922
github.com/stretchr/testify v1.11.1
2023
github.com/tidwall/gjson v1.18.0
21-
github.com/vito/midterm v0.2.3
24+
github.com/vito/midterm v0.2.4
2225
github.com/zalando/go-keyring v0.2.6
2326
gitlab.com/gitlab-org/api/client-go v1.46.0
2427
go.abhg.dev/container/ring v0.3.0
2528
go.abhg.dev/io/ioutil v0.1.0
2629
go.abhg.dev/komplete v0.1.0
27-
go.abhg.dev/log/silog v0.2.0
30+
go.abhg.dev/log/silog v0.3.0
2831
go.abhg.dev/testing/stub v0.2.0
2932
go.uber.org/mock v0.6.0
3033
golang.org/x/oauth2 v0.35.0
@@ -37,20 +40,17 @@ require (
3740
al.essio.dev/pkg/shellescape v1.6.0 // indirect
3841
github.com/atotto/clipboard v0.1.4 // indirect
3942
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
40-
github.com/charmbracelet/colorprofile v0.4.1 // indirect
41-
github.com/charmbracelet/x/ansi v0.11.6 // indirect
42-
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
43-
github.com/charmbracelet/x/term v0.2.2 // indirect
44-
github.com/clipperhouse/displaywidth v0.9.0 // indirect
45-
github.com/clipperhouse/stringish v0.1.1 // indirect
46-
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
43+
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
44+
github.com/charmbracelet/x/termios v0.1.1 // indirect
45+
github.com/charmbracelet/x/windows v0.2.2 // indirect
46+
github.com/clipperhouse/displaywidth v0.11.0 // indirect
47+
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
4748
github.com/danielgatis/go-ansicode v1.0.7 // indirect
4849
github.com/danielgatis/go-iterator v0.0.1 // indirect
4950
github.com/danielgatis/go-utf8 v1.0.0 // indirect
5051
github.com/danielgatis/go-vte v1.0.8 // indirect
5152
github.com/danieljoos/wincred v1.2.3 // indirect
5253
github.com/davecgh/go-spew v1.1.1 // indirect
53-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
5454
github.com/fatih/color v1.18.0 // indirect
5555
github.com/godbus/dbus/v5 v5.2.2 // indirect
5656
github.com/google/go-cmp v0.7.0 // indirect
@@ -61,9 +61,7 @@ require (
6161
github.com/hexops/valast v1.5.0 // indirect
6262
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
6363
github.com/mattn/go-colorable v0.1.14 // indirect
64-
github.com/mattn/go-localereader v0.0.1 // indirect
65-
github.com/mattn/go-runewidth v0.0.19 // indirect
66-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
64+
github.com/mattn/go-runewidth v0.0.20 // indirect
6765
github.com/muesli/cancelreader v0.2.2 // indirect
6866
github.com/muesli/termenv v0.16.0 // indirect
6967
github.com/nightlyone/lockfile v1.0.0 // indirect
@@ -76,8 +74,7 @@ require (
7674
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
7775
golang.org/x/mod v0.33.0 // indirect
7876
golang.org/x/sync v0.19.0 // indirect
79-
golang.org/x/sys v0.41.0 // indirect
80-
golang.org/x/text v0.32.0 // indirect
77+
golang.org/x/sys v0.42.0 // indirect
8178
golang.org/x/time v0.14.0 // indirect
8279
golang.org/x/tools v0.41.0 // indirect
8380
mvdan.cc/gofumpt v0.9.2 // indirect

0 commit comments

Comments
 (0)