detached c646480

feat(theme): carry terminal ANSI palette in Theme

commit
c6464801b49e4fef4deaf19c4ea08abaa2537d5b
author
brice · · 2026-07-17 08:08 UTC
parent
12c4fc5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +76 -9
M internal/usecase/theme/styles_test.go +55 -0
@@ -148,6 +148,61 @@ func TestMRUFade(t *testing.T) {
148148 }
149149 }
150150
151+func TestThemePaletteColor(t *testing.T) {
152+ palette := [16]renderer.RGB{
153+ 3: {R: 0x11, G: 0x22, B: 0x33},
154+ 7: {R: 0xaa, G: 0xbb, B: 0xcc},
155+ }
156+ theme := Theme{
157+ UsePalette: true,
158+ Palette: palette,
159+ PaletteKnown: 1<<3 | 1<<7,
160+ }
161+
162+ tests := []struct {
163+ name string
164+ slot int
165+ want renderer.RGB
166+ ok bool
167+ }{
168+ {name: "known slot", slot: 3, want: palette[3], ok: true},
169+ {name: "unknown slot", slot: 4},
170+ {name: "negative slot", slot: -1},
171+ {name: "slot above palette", slot: 16},
172+ }
173+
174+ for _, tt := range tests {
175+ t.Run(tt.name, func(t *testing.T) {
176+ got, ok := theme.PaletteColor(tt.slot)
177+ require.Equal(t, tt.ok, ok)
178+ require.Equal(t, tt.want, got)
179+ })
180+ }
181+
182+ theme.UsePalette = false
183+ got, ok := theme.PaletteColor(3)
184+ require.False(t, ok)
185+ require.Equal(t, renderer.RGB{}, got)
186+}
187+
188+func TestBuiltinThemesDoNotUsePalette(t *testing.T) {
189+ for name, theme := range map[string]Theme{
190+ "dark": BuiltinDark,
191+ "light": BuiltinLight,
192+ } {
193+ t.Run(name, func(t *testing.T) {
194+ require.False(t, theme.UsePalette)
195+ require.Zero(t, theme.PaletteKnown)
196+ require.Equal(t, [16]renderer.RGB{}, theme.Palette)
197+ })
198+ }
199+}
200+
201+func TestThemeIsComparable(t *testing.T) {
202+ themes := map[Theme]bool{BuiltinDark: true}
203+ require.True(t, themes[BuiltinDark])
204+}
205+
151206 func TestNewStylesComposesSemanticStyles(t *testing.T) {
152207 theme := Theme{
153208 Foreground: renderer.RGB{R: 0xc8, G: 0xc8, B: 0xc8},
@@ -148,6 +148,61 @@ func TestMRUFade(t *testing.T) {
148 }148 }
149 }149 }
150 150
151+func TestThemePaletteColor(t *testing.T) {
152+ palette := [16]renderer.RGB{
153+ 3: {R: 0x11, G: 0x22, B: 0x33},
154+ 7: {R: 0xaa, G: 0xbb, B: 0xcc},
155+ }
156+ theme := Theme{
157+ UsePalette: true,
158+ Palette: palette,
159+ PaletteKnown: 1<<3 | 1<<7,
160+ }
161+
162+ tests := []struct {
163+ name string
164+ slot int
165+ want renderer.RGB
166+ ok bool
167+ }{
168+ {name: "known slot", slot: 3, want: palette[3], ok: true},
169+ {name: "unknown slot", slot: 4},
170+ {name: "negative slot", slot: -1},
171+ {name: "slot above palette", slot: 16},
172+ }
173+
174+ for _, tt := range tests {
175+ t.Run(tt.name, func(t *testing.T) {
176+ got, ok := theme.PaletteColor(tt.slot)
177+ require.Equal(t, tt.ok, ok)
178+ require.Equal(t, tt.want, got)
179+ })
180+ }
181+
182+ theme.UsePalette = false
183+ got, ok := theme.PaletteColor(3)
184+ require.False(t, ok)
185+ require.Equal(t, renderer.RGB{}, got)
186+}
187+
188+func TestBuiltinThemesDoNotUsePalette(t *testing.T) {
189+ for name, theme := range map[string]Theme{
190+ "dark": BuiltinDark,
191+ "light": BuiltinLight,
192+ } {
193+ t.Run(name, func(t *testing.T) {
194+ require.False(t, theme.UsePalette)
195+ require.Zero(t, theme.PaletteKnown)
196+ require.Equal(t, [16]renderer.RGB{}, theme.Palette)
197+ })
198+ }
199+}
200+
201+func TestThemeIsComparable(t *testing.T) {
202+ themes := map[Theme]bool{BuiltinDark: true}
203+ require.True(t, themes[BuiltinDark])
204+}
205+
151 func TestNewStylesComposesSemanticStyles(t *testing.T) {206 func TestNewStylesComposesSemanticStyles(t *testing.T) {
152 theme := Theme{207 theme := Theme{
153 Foreground: renderer.RGB{R: 0xc8, G: 0xc8, B: 0xc8},208 Foreground: renderer.RGB{R: 0xc8, G: 0xc8, B: 0xc8},
M internal/usecase/theme/theme.go +21 -9
@@ -11,16 +11,28 @@ import (
1111
1212 const maxPending = 64
1313
14-// Theme describes terminal default colors reported by OSC 10/11.
14+// Theme describes terminal default colors and ANSI palette entries.
1515 type Theme struct {
16- Foreground renderer.RGB
17- Background renderer.RGB
18- HasFG bool
19- HasBG bool
20- TrueColor bool
21- Known bool
22- SchemeKnown bool
23- Light bool
16+ Foreground renderer.RGB
17+ Background renderer.RGB
18+ Palette [16]renderer.RGB
19+ PaletteKnown uint16
20+ HasFG bool
21+ HasBG bool
22+ TrueColor bool
23+ Known bool
24+ SchemeKnown bool
25+ Light bool
26+ UsePalette bool
27+}
28+
29+// PaletteColor returns a palette color only when palette inheritance is
30+// enabled and the terminal reported the requested ANSI slot.
31+func (t Theme) PaletteColor(slot int) (renderer.RGB, bool) {
32+ if !t.UsePalette || slot < 0 || slot >= len(t.Palette) || t.PaletteKnown&(uint16(1)<<slot) == 0 {
33+ return renderer.RGB{}, false
34+ }
35+ return t.Palette[slot], true
2436 }
2537
2638 var (
@@ -11,16 +11,28 @@ import (
11 11
12 const maxPending = 6412 const maxPending = 64
13 13
14-// Theme describes terminal default colors reported by OSC 10/11.14+// Theme describes terminal default colors and ANSI palette entries.
15 type Theme struct {15 type Theme struct {
16- Foreground renderer.RGB16+ Foreground renderer.RGB
17- Background renderer.RGB17+ Background renderer.RGB
18- HasFG bool18+ Palette [16]renderer.RGB
19- HasBG bool19+ PaletteKnown uint16
20- TrueColor bool20+ HasFG bool
21- Known bool21+ HasBG bool
22- SchemeKnown bool22+ TrueColor bool
23- Light bool23+ Known bool
24+ SchemeKnown bool
25+ Light bool
26+ UsePalette bool
27+}
28+
29+// PaletteColor returns a palette color only when palette inheritance is
30+// enabled and the terminal reported the requested ANSI slot.
31+func (t Theme) PaletteColor(slot int) (renderer.RGB, bool) {
32+ if !t.UsePalette || slot < 0 || slot >= len(t.Palette) || t.PaletteKnown&(uint16(1)<<slot) == 0 {
33+ return renderer.RGB{}, false
34+ }
35+ return t.Palette[slot], true
24 }36 }
25 37
26 var (38 var (

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