feat(theme): carry terminal ANSI palette in Theme
- commit
- c6464801b49e4fef4deaf19c4ea08abaa2537d5b
- author
- brice · · 2026-07-17 08:08 UTC
- parent
- 12c4fc5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 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 | 206 | func TestNewStylesComposesSemanticStyles(t *testing.T) { | |
| 152 | 207 | theme := Theme{ | |
| 153 | 208 | 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 ( | |||
| 11 | 11 | ||
| 12 | 12 | 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 | 15 | 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 | |
| 24 | 36 | } | |
| 25 | 37 | ||
| 26 | 38 | var ( | |
| @@ -11,16 +11,28 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | const maxPending = 64 | 12 | 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.RGB | 16 | + | Foreground renderer.RGB |
| 17 | - | Background renderer.RGB | 17 | + | Background renderer.RGB |
| 18 | - | HasFG bool | 18 | + | Palette [16]renderer.RGB |
| 19 | - | HasBG bool | 19 | + | PaletteKnown uint16 |
| 20 | - | TrueColor bool | 20 | + | HasFG bool |
| 21 | - | Known bool | 21 | + | HasBG bool |
| 22 | - | SchemeKnown bool | 22 | + | TrueColor bool |
| 23 | - | Light 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 | |||
| 24 | } | 36 | } | ||
| 25 | 37 | ||||
| 26 | var ( | 38 | var ( | ||