detached 4604c71

fix(theme): keep the active tab title readable with the indexed accent fallback

When a terminal reports default colors but no palette, the scheme blue slot recolored the active title on the full-strength neutral surface (about 1.7:1 on light themes). The title keeps its RGB secondary text there.
commit
4604c719b00d5c493ce5f906b15831644964693f
author
brice · · 2026-10-08 05:38 UTC
committer
brice · · 2026-10-08 05:46 UTC
parent
f0478a2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +36 -1
M internal/usecase/theme/neutral_session_test.go +31 -0
@@ -4,9 +4,40 @@ import (
44 "testing"
55
66 renderer "github.com/bnema/vev-vt"
7+ "github.com/bnema/vev/internal/domain"
78 "github.com/stretchr/testify/require"
89 )
910
11+// TestIndexedFallbackKeepsActiveTitleReadable covers a terminal that reports
12+// its default colors but no palette: the scheme blue fallback must not recolor
13+// the title drawn on the full-strength active surface.
14+func TestIndexedFallbackKeepsActiveTitleReadable(t *testing.T) {
15+ black := renderer.RGB{}
16+ white := renderer.RGB{R: 255, G: 255, B: 255}
17+ for _, tt := range []struct {
18+ name string
19+ foreground, background renderer.RGB
20+ light bool
21+ }{
22+ {name: "dark", foreground: white, background: black},
23+ {name: "light", foreground: black, background: white, light: true},
24+ } {
25+ t.Run(tt.name, func(t *testing.T) {
26+ theme := Theme{
27+ Known: true, SchemeKnown: true, Light: tt.light, UsePalette: true,
28+ Foreground: tt.foreground, Background: tt.background, HasFG: true, HasBG: true,
29+ }
30+ resolved := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})
31+ require.True(t, resolved.Accent.IndexedOnly)
32+
33+ want := MutedVariantStyle(resolved.Styles.SurfaceActive, theme)
34+ require.Equal(t, want, resolved.Styles.TabActiveTitle)
35+ require.Equal(t, want, resolved.Styles.TabTitleActive)
36+ require.Equal(t, int(resolved.Accent.Slot), resolved.Styles.TabInactiveTitle.Foreground)
37+ })
38+ }
39+}
40+
1041 func TestNeutralSessionSurfaceHierarchy(t *testing.T) {
1142 black := renderer.RGB{}
1243 white := renderer.RGB{R: 255, G: 255, B: 255}
@@ -4,9 +4,40 @@ import (
4 "testing"4 "testing"
5 5
6 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
7+ "github.com/bnema/vev/internal/domain"
7 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
8 )9 )
9 10
11+// TestIndexedFallbackKeepsActiveTitleReadable covers a terminal that reports
12+// its default colors but no palette: the scheme blue fallback must not recolor
13+// the title drawn on the full-strength active surface.
14+func TestIndexedFallbackKeepsActiveTitleReadable(t *testing.T) {
15+ black := renderer.RGB{}
16+ white := renderer.RGB{R: 255, G: 255, B: 255}
17+ for _, tt := range []struct {
18+ name string
19+ foreground, background renderer.RGB
20+ light bool
21+ }{
22+ {name: "dark", foreground: white, background: black},
23+ {name: "light", foreground: black, background: white, light: true},
24+ } {
25+ t.Run(tt.name, func(t *testing.T) {
26+ theme := Theme{
27+ Known: true, SchemeKnown: true, Light: tt.light, UsePalette: true,
28+ Foreground: tt.foreground, Background: tt.background, HasFG: true, HasBG: true,
29+ }
30+ resolved := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})
31+ require.True(t, resolved.Accent.IndexedOnly)
32+
33+ want := MutedVariantStyle(resolved.Styles.SurfaceActive, theme)
34+ require.Equal(t, want, resolved.Styles.TabActiveTitle)
35+ require.Equal(t, want, resolved.Styles.TabTitleActive)
36+ require.Equal(t, int(resolved.Accent.Slot), resolved.Styles.TabInactiveTitle.Foreground)
37+ })
38+ }
39+}
40+
10 func TestNeutralSessionSurfaceHierarchy(t *testing.T) {41 func TestNeutralSessionSurfaceHierarchy(t *testing.T) {
11 black := renderer.RGB{}42 black := renderer.RGB{}
12 white := renderer.RGB{R: 255, G: 255, B: 255}43 white := renderer.RGB{R: 255, G: 255, B: 255}
M internal/usecase/theme/resolved_styles.go +5 -1
@@ -95,7 +95,11 @@ func indexedStyles(styles Styles, slot uint8) Styles {
9595 styles.BorderActive = indexedForeground(styles.BorderActive, slot)
9696 styles.BorderWarn = indexedForeground(styles.BorderWarn, slot)
9797 styles.TabInactiveTitle = indexedForeground(styles.TabInactiveTitle, slot)
98- styles.TabActiveTitle = indexedForeground(styles.TabActiveTitle, slot)
98+ // On the full-strength neutral surface an indexed title color cannot be
99+ // kept readable, so the active title keeps its RGB secondary text.
100+ if !styles.TabActiveTitle.HasBackgroundRGB {
101+ styles.TabActiveTitle = indexedForeground(styles.TabActiveTitle, slot)
102+ }
99103 styles.PickerDescription = indexedForeground(styles.PickerDescription, slot)
100104 styles.PickerSeparator = indexedForeground(styles.PickerSeparator, slot)
101105
@@ -95,7 +95,11 @@ func indexedStyles(styles Styles, slot uint8) Styles {
95 styles.BorderActive = indexedForeground(styles.BorderActive, slot)95 styles.BorderActive = indexedForeground(styles.BorderActive, slot)
96 styles.BorderWarn = indexedForeground(styles.BorderWarn, slot)96 styles.BorderWarn = indexedForeground(styles.BorderWarn, slot)
97 styles.TabInactiveTitle = indexedForeground(styles.TabInactiveTitle, slot)97 styles.TabInactiveTitle = indexedForeground(styles.TabInactiveTitle, slot)
98- styles.TabActiveTitle = indexedForeground(styles.TabActiveTitle, slot)98+ // On the full-strength neutral surface an indexed title color cannot be
99+ // kept readable, so the active title keeps its RGB secondary text.
100+ if !styles.TabActiveTitle.HasBackgroundRGB {
101+ styles.TabActiveTitle = indexedForeground(styles.TabActiveTitle, slot)
102+ }
99 styles.PickerDescription = indexedForeground(styles.PickerDescription, slot)103 styles.PickerDescription = indexedForeground(styles.PickerDescription, slot)
100 styles.PickerSeparator = indexedForeground(styles.PickerSeparator, slot)104 styles.PickerSeparator = indexedForeground(styles.PickerSeparator, slot)
101 105

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