detached cfdaba6

fix(theme): strengthen active grayscale session contrast

commit
cfdaba61e8d86928bab119829a9297b4850a2903
author
brice · · 2026-10-05 11:58 UTC
parent
93f7e39
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +44 -5
M internal/usecase/daemon/theme_integration_test.go +2 -2
@@ -161,12 +161,12 @@ func TestAccentLifecycleNeutralChromeByteSnapshots(t *testing.T) {
161161 {
162162 name: "forced dark",
163163 config: domain.Config{Theme: domain.ThemeDark},
164- want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;216;216;216;48;2;72;72;72mD\x1b[0;38;2;139;139;139mEF\x1b[0m",
164+ want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;24;24;24;48;2;216;216;216mD\x1b[0;38;2;139;139;139mEF\x1b[0m",
165165 },
166166 {
167167 name: "forced light",
168168 config: domain.Config{Theme: domain.ThemeLight},
169- want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;32;32;32;48;2;194;194;194mD\x1b[0;38;2;118;118;118mEF\x1b[0m",
169+ want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;248;248;248;48;2;32;32;32mD\x1b[0;38;2;118;118;118mEF\x1b[0m",
170170 },
171171 }
172172 for _, tt := range tests {
@@ -161,12 +161,12 @@ func TestAccentLifecycleNeutralChromeByteSnapshots(t *testing.T) {
161 {161 {
162 name: "forced dark",162 name: "forced dark",
163 config: domain.Config{Theme: domain.ThemeDark},163 config: domain.Config{Theme: domain.ThemeDark},
164- want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;216;216;216;48;2;72;72;72mD\x1b[0;38;2;139;139;139mEF\x1b[0m",164+ want: "\x1b[1;1H\x1b[0;38;2;216;216;216;48;2;47;47;47mABC\x1b[0;38;2;24;24;24;48;2;216;216;216mD\x1b[0;38;2;139;139;139mEF\x1b[0m",
165 },165 },
166 {166 {
167 name: "forced light",167 name: "forced light",
168 config: domain.Config{Theme: domain.ThemeLight},168 config: domain.Config{Theme: domain.ThemeLight},
169- want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;32;32;32;48;2;194;194;194mD\x1b[0;38;2;118;118;118mEF\x1b[0m",169+ want: "\x1b[1;1H\x1b[0;38;2;32;32;32;48;2;222;222;222mABC\x1b[0;38;2;248;248;248;48;2;32;32;32mD\x1b[0;38;2;118;118;118mEF\x1b[0m",
170 },170 },
171 }171 }
172 for _, tt := range tests {172 for _, tt := range tests {
A internal/usecase/theme/neutral_session_test.go +34 -0
@@ -0,0 +1,34 @@
1+package theme
2+
3+import (
4+ "testing"
5+
6+ renderer "github.com/bnema/vev-vt"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestNeutralSessionSurfaceHierarchy(t *testing.T) {
11+ black := renderer.RGB{}
12+ white := renderer.RGB{R: 255, G: 255, B: 255}
13+ for _, tt := range []struct {
14+ name string
15+ foreground, background renderer.RGB
16+ }{
17+ {name: "dark", foreground: white, background: black},
18+ {name: "light", foreground: black, background: white},
19+ } {
20+ t.Run(tt.name, func(t *testing.T) {
21+ theme := Theme{Known: true, Foreground: tt.foreground, Background: tt.background, HasFG: true, HasBG: true}
22+ styles := neutralStyles(theme)
23+ require.Equal(t, tt.foreground, styles.SurfaceActive.BackgroundRGB)
24+ require.Equal(t, tt.background, styles.SurfaceActive.ForegroundRGB)
25+ require.Equal(t, styles.TabActive.BackgroundRGB, styles.SurfaceActive.BackgroundRGB)
26+ for count := 1; count <= 9; count++ {
27+ for index := range count {
28+ want := Blend(styles.SurfaceActive.BackgroundRGB, styles.SurfaceBar.BackgroundRGB, 1-mruWeight(index, count))
29+ require.Equal(t, want, styles.MRUStyle(index, count).BackgroundRGB)
30+ }
31+ }
32+ })
33+ }
34+}
@@ -0,0 +1,34 @@
1+package theme
2+
3+import (
4+ "testing"
5+
6+ renderer "github.com/bnema/vev-vt"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestNeutralSessionSurfaceHierarchy(t *testing.T) {
11+ black := renderer.RGB{}
12+ white := renderer.RGB{R: 255, G: 255, B: 255}
13+ for _, tt := range []struct {
14+ name string
15+ foreground, background renderer.RGB
16+ }{
17+ {name: "dark", foreground: white, background: black},
18+ {name: "light", foreground: black, background: white},
19+ } {
20+ t.Run(tt.name, func(t *testing.T) {
21+ theme := Theme{Known: true, Foreground: tt.foreground, Background: tt.background, HasFG: true, HasBG: true}
22+ styles := neutralStyles(theme)
23+ require.Equal(t, tt.foreground, styles.SurfaceActive.BackgroundRGB)
24+ require.Equal(t, tt.background, styles.SurfaceActive.ForegroundRGB)
25+ require.Equal(t, styles.TabActive.BackgroundRGB, styles.SurfaceActive.BackgroundRGB)
26+ for count := 1; count <= 9; count++ {
27+ for index := range count {
28+ want := Blend(styles.SurfaceActive.BackgroundRGB, styles.SurfaceBar.BackgroundRGB, 1-mruWeight(index, count))
29+ require.Equal(t, want, styles.MRUStyle(index, count).BackgroundRGB)
30+ }
31+ }
32+ })
33+ }
34+}
M internal/usecase/theme/phase3_review_test.go +2 -1
@@ -120,7 +120,8 @@ func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) {
120120 // PickerSeparator is now a semantic role with the SurfaceBar
121121 // background; it was never a compatibility alias.
122122 delete(want, "PickerSeparator")
123- // Active neutral tab names use a high-contrast history anchor.
123+ // Active neutral sessions and tab names share the history anchor.
124+ want["Accent"] = rgbSurface(tt.theme.Background, tt.theme.Foreground)
124125 want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme)
125126 for name, style := range want {
126127 require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name)
@@ -120,7 +120,8 @@ func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) {
120 // PickerSeparator is now a semantic role with the SurfaceBar120 // PickerSeparator is now a semantic role with the SurfaceBar
121 // background; it was never a compatibility alias.121 // background; it was never a compatibility alias.
122 delete(want, "PickerSeparator")122 delete(want, "PickerSeparator")
123- // Active neutral tab names use a high-contrast history anchor.123+ // Active neutral sessions and tab names share the history anchor.
124+ want["Accent"] = rgbSurface(tt.theme.Background, tt.theme.Foreground)
124 want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme)125 want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme)
125 for name, style := range want {126 for name, style := range want {
126 require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name)127 require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name)
M internal/usecase/theme/resolved_styles.go +5 -1
@@ -162,7 +162,11 @@ func neutralStyles(t Theme) Styles {
162162 styles.PaletteDesc = legacyMutedText(t)
163163 styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent})
164164 if usable(t) {
165- styles.TabActive = EmphasisStyle(rgbSurface(t.Background, t.Foreground), t)
165+ // Sessions share the full-strength neutral endpoint with active tabs;
166+ // overlay selections retain their softer neutral surface.
167+ styles.SurfaceActive = rgbSurface(t.Background, t.Foreground)
168+ styles.Accent = styles.SurfaceActive
169+ styles.TabActive = EmphasisStyle(styles.SurfaceActive, t)
166170 styles.TabNameActive = styles.TabActive
167171 // Without a palette accent the history uses the same mruWeight
168172 // placement as MRUStyle, between the active tab and the bar.
@@ -162,7 +162,11 @@ func neutralStyles(t Theme) Styles {
162 styles.PaletteDesc = legacyMutedText(t)162 styles.PaletteDesc = legacyMutedText(t)
163 styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent})163 styles = withMRUStyles(styles, Ramp{SurfaceBar: styles.SurfaceBar, SurfaceRecent: styles.SurfaceRecent})
164 if usable(t) {164 if usable(t) {
165- styles.TabActive = EmphasisStyle(rgbSurface(t.Background, t.Foreground), t)165+ // Sessions share the full-strength neutral endpoint with active tabs;
166+ // overlay selections retain their softer neutral surface.
167+ styles.SurfaceActive = rgbSurface(t.Background, t.Foreground)
168+ styles.Accent = styles.SurfaceActive
169+ styles.TabActive = EmphasisStyle(styles.SurfaceActive, t)
166 styles.TabNameActive = styles.TabActive170 styles.TabNameActive = styles.TabActive
167 // Without a palette accent the history uses the same mruWeight171 // Without a palette accent the history uses the same mruWeight
168 // placement as MRUStyle, between the active tab and the bar.172 // placement as MRUStyle, between the active tab and the bar.
M internal/usecase/theme/theme_test.go +1 -1
@@ -592,7 +592,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) {
592592 t.Fatalf("status themed=%+v", status)
593593 }
594594 accent := AccentStyle(theme)
595- if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != (renderer.RGB{R: 58, G: 65, B: 73}) {
595+ if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != theme.Foreground || accent.ForegroundRGB != theme.Background {
596596 t.Fatalf("accent themed=%+v", accent)
597597 }
598598 border := BorderStyle(theme)
@@ -592,7 +592,7 @@ func TestStyleHelpersFallbackAndThemed(t *testing.T) {
592 t.Fatalf("status themed=%+v", status)592 t.Fatalf("status themed=%+v", status)
593 }593 }
594 accent := AccentStyle(theme)594 accent := AccentStyle(theme)
595- if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != (renderer.RGB{R: 58, G: 65, B: 73}) {595+ if accent.Inverse || !accent.HasBackgroundRGB || accent.BackgroundRGB != theme.Foreground || accent.ForegroundRGB != theme.Background {
596 t.Fatalf("accent themed=%+v", accent)596 t.Fatalf("accent themed=%+v", accent)
597 }597 }
598 border := BorderStyle(theme)598 border := BorderStyle(theme)

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