detached d4234db

refactor(theme): remove redundant applied theme mirror

commit
d4234db1b8cd7c314d6c23d587fba580f099926c
author
brice · · 2026-07-18 08:40 UTC
parent
810e9f1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +19 -23
M internal/usecase/daemon/client.go +8 -12
@@ -57,7 +57,6 @@ type attachedClient struct {
5757 sess Guarded[*session]
5858 mouseScan mouse.Scanner
5959 themeMu sync.Mutex
60- theme themeui.Theme // compatibility mirror of applied.Raw
6160 clientTheme themeui.Theme
6261 appliedTheme appliedTheme
6362 lastCursor cursorOut
@@ -147,19 +146,16 @@ func (ac *attachedClient) clearCaptureFrames() {
147146 ac.sendMu.Unlock()
148147 }
149148
150-func (ac *attachedClient) getTheme() themeui.Theme {
151- return ac.getAppliedTheme().Raw
152-}
153-
154149 func (ac *attachedClient) getAppliedTheme() appliedTheme {
155150 ac.themeMu.Lock()
156151 defer ac.themeMu.Unlock()
157- if ac.appliedTheme.Generation == 0 {
152+ applied := ac.appliedTheme
153+ if applied.Generation == 0 {
158154 // An unattached client has no terminal report yet. Reuse the static
159155 // neutral cache rather than resolving from a render path.
160- return appliedTheme{Raw: ac.theme, Resolved: themeui.ResolvedTheme{Theme: ac.theme, Styles: fallbackChromeStyles}}
156+ applied.Resolved = themeui.ResolvedTheme{Theme: applied.Raw, Styles: fallbackChromeStyles}
161157 }
162- return ac.appliedTheme
158+ return applied
163159 }
164160
165161 func (ac *attachedClient) getClientTheme() themeui.Theme {
@@ -168,9 +164,10 @@ func (ac *attachedClient) getClientTheme() themeui.Theme {
168164 return ac.clientTheme
169165 }
170166
171-func (ac *attachedClient) setTheme(t themeui.Theme) {
172- // Test and legacy setup paths have no daemon policy context. Keep their
173- // neutral automatic behavior while publishing a complete immutable value.
167+// setThemeForTest publishes a complete applied snapshot for tests that do
168+// not exercise daemon configuration. Production theme changes use
169+// applyHostThemeLocked, which supplies the active policy.
170+func (ac *attachedClient) setThemeForTest(t themeui.Theme) {
174171 ac.setAppliedTheme(appliedTheme{Raw: t, Resolved: themeui.Resolve(t, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})})
175172 }
176173
@@ -178,7 +175,6 @@ func (ac *attachedClient) setAppliedTheme(next appliedTheme) {
178175 ac.themeMu.Lock()
179176 next.Generation = ac.appliedTheme.Generation + 1
180177 ac.appliedTheme = next
181- ac.theme = next.Raw
182178 ac.themeMu.Unlock()
183179 }
184180
@@ -57,7 +57,6 @@ type attachedClient struct {
57 sess Guarded[*session]57 sess Guarded[*session]
58 mouseScan mouse.Scanner58 mouseScan mouse.Scanner
59 themeMu sync.Mutex59 themeMu sync.Mutex
60- theme themeui.Theme // compatibility mirror of applied.Raw
61 clientTheme themeui.Theme60 clientTheme themeui.Theme
62 appliedTheme appliedTheme61 appliedTheme appliedTheme
63 lastCursor cursorOut62 lastCursor cursorOut
@@ -147,19 +146,16 @@ func (ac *attachedClient) clearCaptureFrames() {
147 ac.sendMu.Unlock()146 ac.sendMu.Unlock()
148 }147 }
149 148
150-func (ac *attachedClient) getTheme() themeui.Theme {
151- return ac.getAppliedTheme().Raw
152-}
153-
154 func (ac *attachedClient) getAppliedTheme() appliedTheme {149 func (ac *attachedClient) getAppliedTheme() appliedTheme {
155 ac.themeMu.Lock()150 ac.themeMu.Lock()
156 defer ac.themeMu.Unlock()151 defer ac.themeMu.Unlock()
157- if ac.appliedTheme.Generation == 0 {152+ applied := ac.appliedTheme
153+ if applied.Generation == 0 {
158 // An unattached client has no terminal report yet. Reuse the static154 // An unattached client has no terminal report yet. Reuse the static
159 // neutral cache rather than resolving from a render path.155 // neutral cache rather than resolving from a render path.
160- return appliedTheme{Raw: ac.theme, Resolved: themeui.ResolvedTheme{Theme: ac.theme, Styles: fallbackChromeStyles}}156+ applied.Resolved = themeui.ResolvedTheme{Theme: applied.Raw, Styles: fallbackChromeStyles}
161 }157 }
162- return ac.appliedTheme158+ return applied
163 }159 }
164 160
165 func (ac *attachedClient) getClientTheme() themeui.Theme {161 func (ac *attachedClient) getClientTheme() themeui.Theme {
@@ -168,9 +164,10 @@ func (ac *attachedClient) getClientTheme() themeui.Theme {
168 return ac.clientTheme164 return ac.clientTheme
169 }165 }
170 166
171-func (ac *attachedClient) setTheme(t themeui.Theme) {167+// setThemeForTest publishes a complete applied snapshot for tests that do
172- // Test and legacy setup paths have no daemon policy context. Keep their168+// not exercise daemon configuration. Production theme changes use
173- // neutral automatic behavior while publishing a complete immutable value.169+// applyHostThemeLocked, which supplies the active policy.
170+func (ac *attachedClient) setThemeForTest(t themeui.Theme) {
174 ac.setAppliedTheme(appliedTheme{Raw: t, Resolved: themeui.Resolve(t, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})})171 ac.setAppliedTheme(appliedTheme{Raw: t, Resolved: themeui.Resolve(t, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})})
175 }172 }
176 173
@@ -178,7 +175,6 @@ func (ac *attachedClient) setAppliedTheme(next appliedTheme) {
178 ac.themeMu.Lock()175 ac.themeMu.Lock()
179 next.Generation = ac.appliedTheme.Generation + 1176 next.Generation = ac.appliedTheme.Generation + 1
180 ac.appliedTheme = next177 ac.appliedTheme = next
181- ac.theme = next.Raw
182 ac.themeMu.Unlock()178 ac.themeMu.Unlock()
183 }179 }
184 180
M internal/usecase/daemon/config_test.go +2 -2
@@ -72,7 +72,7 @@ func TestApplyThemeCopiesTerminalPaletteAndHotReloadGatesIt(t *testing.T) {
7272
7373 require.Equal(t, clientPalette, ac.getClientTheme().Palette)
7474 require.Equal(t, uint16(1<<1|1<<12), ac.getClientTheme().PaletteKnown)
75- require.True(t, ac.getTheme().UsePalette)
75+ require.True(t, ac.getAppliedTheme().Raw.UsePalette)
7676
7777 cfg := domain.Defaults()
7878 cfg.ThemePalette = false
@@ -80,7 +80,7 @@ func TestApplyThemeCopiesTerminalPaletteAndHotReloadGatesIt(t *testing.T) {
8080
8181 // ApplyConfig reuses the live client report and repaints without requiring
8282 // a reconnect; only palette inheritance changes.
83- reloaded := ac.getTheme()
83+ reloaded := ac.getAppliedTheme().Raw
8484 require.Equal(t, clientPalette, reloaded.Palette)
8585 require.Equal(t, uint16(1<<1|1<<12), reloaded.PaletteKnown)
8686 require.False(t, reloaded.UsePalette)
@@ -72,7 +72,7 @@ func TestApplyThemeCopiesTerminalPaletteAndHotReloadGatesIt(t *testing.T) {
72 72
73 require.Equal(t, clientPalette, ac.getClientTheme().Palette)73 require.Equal(t, clientPalette, ac.getClientTheme().Palette)
74 require.Equal(t, uint16(1<<1|1<<12), ac.getClientTheme().PaletteKnown)74 require.Equal(t, uint16(1<<1|1<<12), ac.getClientTheme().PaletteKnown)
75- require.True(t, ac.getTheme().UsePalette)75+ require.True(t, ac.getAppliedTheme().Raw.UsePalette)
76 76
77 cfg := domain.Defaults()77 cfg := domain.Defaults()
78 cfg.ThemePalette = false78 cfg.ThemePalette = false
@@ -80,7 +80,7 @@ func TestApplyThemeCopiesTerminalPaletteAndHotReloadGatesIt(t *testing.T) {
80 80
81 // ApplyConfig reuses the live client report and repaints without requiring81 // ApplyConfig reuses the live client report and repaints without requiring
82 // a reconnect; only palette inheritance changes.82 // a reconnect; only palette inheritance changes.
83- reloaded := ac.getTheme()83+ reloaded := ac.getAppliedTheme().Raw
84 require.Equal(t, clientPalette, reloaded.Palette)84 require.Equal(t, clientPalette, reloaded.Palette)
85 require.Equal(t, uint16(1<<1|1<<12), reloaded.PaletteKnown)85 require.Equal(t, uint16(1<<1|1<<12), reloaded.PaletteKnown)
86 require.False(t, reloaded.UsePalette)86 require.False(t, reloaded.UsePalette)
M internal/usecase/daemon/render_test.go +3 -3
@@ -30,7 +30,7 @@ func TestPaletteBackdropDimsSimultaneousCopyMode(t *testing.T) {
3030 defer release()
3131 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
3232 theme := backdropTheme()
33- ac.setTheme(theme)
33+ ac.setThemeForTest(theme)
3434 client := vt.NewScreen(80, 25)
3535 pane := sess.tabs[0].focusedPane()
3636 pane.screen.Write([]byte("\x1b[38;2;180;90;30mX"))
@@ -96,7 +96,7 @@ func TestPaletteBackdropKeepsSimultaneousPickerCrisp(t *testing.T) {
9696 p, release := newBlockingPTY(t)
9797 defer release()
9898 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
99- ac.setTheme(backdropTheme())
99+ ac.setThemeForTest(backdropTheme())
100100 client := vt.NewScreen(80, 25)
101101 pane := sess.tabs[0].focusedPane()
102102 pane.screen.Write([]byte("X"))
@@ -117,7 +117,7 @@ func TestPaletteBackdropProductionRenderAndDismissal(t *testing.T) {
117117 p, release := newBlockingPTY(t)
118118 defer release()
119119 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
120- ac.setTheme(backdropTheme())
120+ ac.setThemeForTest(backdropTheme())
121121 client := vt.NewScreen(80, 25)
122122 pane := sess.tabs[0].focusedPane()
123123 pane.screen.Write([]byte("X"))
@@ -30,7 +30,7 @@ func TestPaletteBackdropDimsSimultaneousCopyMode(t *testing.T) {
30 defer release()30 defer release()
31 d, sess, ac, sends := newManualSessionWithPTYs(t, p)31 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
32 theme := backdropTheme()32 theme := backdropTheme()
33- ac.setTheme(theme)33+ ac.setThemeForTest(theme)
34 client := vt.NewScreen(80, 25)34 client := vt.NewScreen(80, 25)
35 pane := sess.tabs[0].focusedPane()35 pane := sess.tabs[0].focusedPane()
36 pane.screen.Write([]byte("\x1b[38;2;180;90;30mX"))36 pane.screen.Write([]byte("\x1b[38;2;180;90;30mX"))
@@ -96,7 +96,7 @@ func TestPaletteBackdropKeepsSimultaneousPickerCrisp(t *testing.T) {
96 p, release := newBlockingPTY(t)96 p, release := newBlockingPTY(t)
97 defer release()97 defer release()
98 d, sess, ac, sends := newManualSessionWithPTYs(t, p)98 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
99- ac.setTheme(backdropTheme())99+ ac.setThemeForTest(backdropTheme())
100 client := vt.NewScreen(80, 25)100 client := vt.NewScreen(80, 25)
101 pane := sess.tabs[0].focusedPane()101 pane := sess.tabs[0].focusedPane()
102 pane.screen.Write([]byte("X"))102 pane.screen.Write([]byte("X"))
@@ -117,7 +117,7 @@ func TestPaletteBackdropProductionRenderAndDismissal(t *testing.T) {
117 p, release := newBlockingPTY(t)117 p, release := newBlockingPTY(t)
118 defer release()118 defer release()
119 d, sess, ac, sends := newManualSessionWithPTYs(t, p)119 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
120- ac.setTheme(backdropTheme())120+ ac.setThemeForTest(backdropTheme())
121 client := vt.NewScreen(80, 25)121 client := vt.NewScreen(80, 25)
122 pane := sess.tabs[0].focusedPane()122 pane := sess.tabs[0].focusedPane()
123 pane.screen.Write([]byte("X"))123 pane.screen.Write([]byte("X"))
M internal/usecase/daemon/status_test.go +6 -6
@@ -408,7 +408,7 @@ func TestStatusCompositionUsesTruecolorTheme(t *testing.T) {
408408 win := sess.activeTab()
409409 win.focusedPane().screen.Resize(12, 2)
410410 win.size = domain.Size{Cols: 12, Rows: 2}
411- ac.setTheme(themeui.Theme{
411+ ac.setThemeForTest(themeui.Theme{
412412 Foreground: renderer.RGB{R: 220, G: 220, B: 220},
413413 Background: renderer.RGB{R: 10, G: 20, B: 30},
414414 HasFG: true,
@@ -438,9 +438,9 @@ func TestStatusApplyThemeStoresClientAndPropagatesScreens(t *testing.T) {
438438
439439 d.applyTheme(sess, ac, msg)
440440
441- require.Equal(t, renderer.RGB{R: 1, G: 2, B: 3}, ac.getTheme().Foreground)
442- require.True(t, ac.getTheme().SchemeKnown)
443- require.True(t, ac.getTheme().Light)
441+ require.Equal(t, renderer.RGB{R: 1, G: 2, B: 3}, ac.getAppliedTheme().Raw.Foreground)
442+ require.True(t, ac.getAppliedTheme().Raw.SchemeKnown)
443+ require.True(t, ac.getAppliedTheme().Raw.Light)
444444 assertSessionDefaultColors(t, sess, renderer.RGB{R: 1, G: 2, B: 3}, renderer.RGB{R: 4, G: 5, B: 6})
445445 assertSessionColorScheme(t, sess, true)
446446 }
@@ -494,7 +494,7 @@ func TestApplyThemeForcedBuiltinThemePropagatesToChromeAndPanes(t *testing.T) {
494494 TrueColor: true, SchemeKnown: true, Light: !tc.want.Light,
495495 })
496496
497- require.Equal(t, tc.want, ac.getTheme())
497+ require.Equal(t, tc.want, ac.getAppliedTheme().Raw)
498498 assertSessionDefaultColors(t, sess, tc.want.Foreground, tc.want.Background)
499499 assertSessionColorScheme(t, sess, tc.want.Light)
500500 })
@@ -530,7 +530,7 @@ func TestAttachClientAppliesForcedThemeBeforeMsgTheme(t *testing.T) {
530530
531531 ac, _ := d.attachClient(sess, tr, domain.Size{Cols: 80, Rows: 24}, attachClientOptions{})
532532
533- require.Equal(t, tc.want, ac.getTheme())
533+ require.Equal(t, tc.want, ac.getAppliedTheme().Raw)
534534 assertSessionDefaultColors(t, sess, tc.want.Foreground, tc.want.Background)
535535 assertSessionColorScheme(t, sess, tc.want.Light)
536536 })
@@ -408,7 +408,7 @@ func TestStatusCompositionUsesTruecolorTheme(t *testing.T) {
408 win := sess.activeTab()408 win := sess.activeTab()
409 win.focusedPane().screen.Resize(12, 2)409 win.focusedPane().screen.Resize(12, 2)
410 win.size = domain.Size{Cols: 12, Rows: 2}410 win.size = domain.Size{Cols: 12, Rows: 2}
411- ac.setTheme(themeui.Theme{411+ ac.setThemeForTest(themeui.Theme{
412 Foreground: renderer.RGB{R: 220, G: 220, B: 220},412 Foreground: renderer.RGB{R: 220, G: 220, B: 220},
413 Background: renderer.RGB{R: 10, G: 20, B: 30},413 Background: renderer.RGB{R: 10, G: 20, B: 30},
414 HasFG: true,414 HasFG: true,
@@ -438,9 +438,9 @@ func TestStatusApplyThemeStoresClientAndPropagatesScreens(t *testing.T) {
438 438
439 d.applyTheme(sess, ac, msg)439 d.applyTheme(sess, ac, msg)
440 440
441- require.Equal(t, renderer.RGB{R: 1, G: 2, B: 3}, ac.getTheme().Foreground)441+ require.Equal(t, renderer.RGB{R: 1, G: 2, B: 3}, ac.getAppliedTheme().Raw.Foreground)
442- require.True(t, ac.getTheme().SchemeKnown)442+ require.True(t, ac.getAppliedTheme().Raw.SchemeKnown)
443- require.True(t, ac.getTheme().Light)443+ require.True(t, ac.getAppliedTheme().Raw.Light)
444 assertSessionDefaultColors(t, sess, renderer.RGB{R: 1, G: 2, B: 3}, renderer.RGB{R: 4, G: 5, B: 6})444 assertSessionDefaultColors(t, sess, renderer.RGB{R: 1, G: 2, B: 3}, renderer.RGB{R: 4, G: 5, B: 6})
445 assertSessionColorScheme(t, sess, true)445 assertSessionColorScheme(t, sess, true)
446 }446 }
@@ -494,7 +494,7 @@ func TestApplyThemeForcedBuiltinThemePropagatesToChromeAndPanes(t *testing.T) {
494 TrueColor: true, SchemeKnown: true, Light: !tc.want.Light,494 TrueColor: true, SchemeKnown: true, Light: !tc.want.Light,
495 })495 })
496 496
497- require.Equal(t, tc.want, ac.getTheme())497+ require.Equal(t, tc.want, ac.getAppliedTheme().Raw)
498 assertSessionDefaultColors(t, sess, tc.want.Foreground, tc.want.Background)498 assertSessionDefaultColors(t, sess, tc.want.Foreground, tc.want.Background)
499 assertSessionColorScheme(t, sess, tc.want.Light)499 assertSessionColorScheme(t, sess, tc.want.Light)
500 })500 })
@@ -530,7 +530,7 @@ func TestAttachClientAppliesForcedThemeBeforeMsgTheme(t *testing.T) {
530 530
531 ac, _ := d.attachClient(sess, tr, domain.Size{Cols: 80, Rows: 24}, attachClientOptions{})531 ac, _ := d.attachClient(sess, tr, domain.Size{Cols: 80, Rows: 24}, attachClientOptions{})
532 532
533- require.Equal(t, tc.want, ac.getTheme())533+ require.Equal(t, tc.want, ac.getAppliedTheme().Raw)
534 assertSessionDefaultColors(t, sess, tc.want.Foreground, tc.want.Background)534 assertSessionDefaultColors(t, sess, tc.want.Foreground, tc.want.Background)
535 assertSessionColorScheme(t, sess, tc.want.Light)535 assertSessionColorScheme(t, sess, tc.want.Light)
536 })536 })

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close