phase3_review_test.go RawHistoryFolder

1package theme
2
3import (
4	"testing"
5
6	renderer "github.com/bnema/vev-vt"
7	"github.com/bnema/vev/internal/domain"
8	"github.com/stretchr/testify/require"
9)
10
11func TestResolveAccentIndexedFallbackTruthTable(t *testing.T) {
12	teal := renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5}
13	explicit := domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 5}
14	tests := []struct {
15		name   string
16		theme  Theme
17		policy domain.ThemeAccent
18		want   Accent
19	}{
20		{
21			name:   "explicit unknown slot decorates only with its exact index",
22			theme:  paletteTheme(map[int]renderer.RGB{2: teal, 10: teal}),
23			policy: explicit,
24			want:   Accent{Slot: 5, IndexedOnly: true},
25		},
26		{
27			name: "explicit known slot without truecolor remains semantic",
28			theme: func() Theme {
29				t := paletteTheme(map[int]renderer.RGB{5: teal})
30				t.TrueColor = false
31				return t
32			}(),
33			policy: explicit,
34			want:   Accent{RGB: teal, Slot: 5, Known: true},
35		},
36		{
37			name: "automatic absent osc palette keeps dark scheme indexed blue fallback",
38			theme: Theme{
39				Foreground: renderer.RGB{R: 0xd8, G: 0xdc, B: 0xe8}, Background: renderer.RGB{R: 8, G: 9, B: 10},
40				HasFG: true, HasBG: true, TrueColor: true, Known: true, SchemeKnown: true, UsePalette: true,
41			},
42			policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
43			want:   Accent{Slot: 12, IndexedOnly: true},
44		},
45		{
46			name: "automatic absent defaults keeps light scheme indexed blue fallback",
47			theme: Theme{
48				TrueColor: true, Known: true, SchemeKnown: true, Light: true, UsePalette: true,
49			},
50			policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
51			want:   Accent{Slot: 4, IndexedOnly: true},
52		},
53		{
54			name:   "automatic absent palette and unknown scheme stays neutral",
55			theme:  Theme{TrueColor: true, Known: true, UsePalette: true},
56			policy: domain.ThemeAccent{Mode: domain.ThemeAccentAuto},
57			want:   Accent{},
58		},
59		{
60			name: "palette off suppresses explicit indexed decoration",
61			theme: func() Theme {
62				t := paletteTheme(map[int]renderer.RGB{5: teal})
63				t.UsePalette = false
64				return t
65			}(),
66			policy: explicit,
67			want:   Accent{},
68		},
69	}
70
71	for _, tt := range tests {
72		t.Run(tt.name, func(t *testing.T) {
73			require.Equal(t, tt.want, ResolveAccent(tt.theme, tt.policy))
74		})
75	}
76}
77
78func TestResolveIndexedAccentNeverFillsAnIndexedBackground(t *testing.T) {
79	theme := paletteTheme(map[int]renderer.RGB{2: {R: 0x7d, G: 0xb5, B: 0xb5}})
80	resolved := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 5})
81	require.Equal(t, Accent{Slot: 5, IndexedOnly: true}, resolved.Accent)
82
83	for name, style := range legacyAndSemanticStyles(resolved.Styles) {
84		require.NotEqualf(t, 5, style.Background, "%s must not use an indexed accent background", name)
85	}
86	require.Equal(t, 5, resolved.Styles.BorderMuted.Foreground)
87	require.Equal(t, 5, resolved.Styles.PickerSeparator.Foreground)
88}
89
90func TestPickerSeparatorUsesSecondaryTextContrast(t *testing.T) {
91	theme := rampTheme(false)
92	theme.Palette[2] = renderer.RGB{R: 0x7d, G: 0xb5, B: 0xb5}
93	theme.Palette[10] = theme.Palette[2]
94	theme.PaletteKnown = 1<<2 | 1<<10
95	resolved := Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})
96
97	want, ok := secondaryText(theme.Foreground, theme.Background)
98	require.True(t, ok)
99	require.Equal(t, want, resolved.Styles.PickerSeparator.ForegroundRGB)
100	require.False(t, resolved.Styles.PickerSeparator.HasBackgroundRGB)
101	require.Equal(t, want, resolved.Styles.PickerDescription.ForegroundRGB)
102	require.False(t, resolved.Styles.PickerDescription.HasBackgroundRGB)
103	require.GreaterOrEqual(t, ContrastRatio(resolved.Styles.PickerSeparator.ForegroundRGB, theme.Background), normalTextContrast)
104}
105
106func TestLegacyAliasesPaletteOffAndForcedThemesRemainExact(t *testing.T) {
107	paletteOff := paletteTheme(map[int]renderer.RGB{2: {R: 0x7d, G: 0xb5, B: 0xb5}})
108	paletteOff.UsePalette = false
109	tests := []struct {
110		name  string
111		theme Theme
112	}{
113		{name: "palette off", theme: paletteOff},
114		{name: "forced dark", theme: BuiltinDark},
115		{name: "forced light", theme: BuiltinLight},
116	}
117	for _, tt := range tests {
118		t.Run(tt.name, func(t *testing.T) {
119			got := Resolve(tt.theme, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}).Styles
120			want := prePhase3LegacyAliases(tt.theme)
121			// PickerSeparator is now a semantic role with the SurfaceBar
122			// background; it was never a compatibility alias.
123			delete(want, "PickerSeparator")
124			// Active neutral tab names use a high-contrast history anchor.
125			want["TabNameActive"] = EmphasisStyle(rgbSurface(tt.theme.Background, tt.theme.Foreground), tt.theme)
126			for name, style := range want {
127				require.Equalf(t, style, legacyAndSemanticStyles(got)[name], "%s", name)
128			}
129		})
130	}
131}
132
133func prePhase3LegacyAliases(t Theme) map[string]renderer.Style {
134	status := renderer.DefaultStyle()
135	accent := inverseStyle()
136	border := renderer.DefaultStyle()
137	muted := renderer.DefaultStyle()
138	if usable(t) {
139		status = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, 0.12))
140		accent = rgbSurface(t.Foreground, Blend(t.Background, t.Foreground, neutralAccentBlend))
141		border = foregroundStyle(Blend(t.Foreground, t.Background, 0.40))
142		muted = foregroundStyle(Blend(t.Foreground, t.Background, 0.45))
143	}
144	selection := accent
145	return map[string]renderer.Style{
146		"StatusBar":            status,
147		"Accent":               accent,
148		"Border":               border,
149		"Selection":            selection,
150		"CopyStatus":           selection,
151		"PaletteDesc":          muted,
152		"TabName":              EmphasisStyle(status, t),
153		"TabNameActive":        EmphasisStyle(accent, t),
154		"TabTitle":             MutedVariantStyle(status, t),
155		"TabTitleActive":       MutedVariantStyle(accent, t),
156		"PickerName":           EmphasisStyle(renderer.DefaultStyle(), t),
157		"PickerSelectionName":  EmphasisStyle(selection, t),
158		"PickerSelectionMuted": MutedVariantStyle(selection, t),
159		"PickerSeparator":      muted,
160	}
161}
162
163func legacyAndSemanticStyles(styles Styles) map[string]renderer.Style {
164	return map[string]renderer.Style{
165		"SurfaceBar": styles.SurfaceBar, "SurfaceInactive": styles.SurfaceInactive,
166		"SurfaceRecent": styles.SurfaceRecent, "SurfaceActive": styles.SurfaceActive,
167		"BorderMuted": styles.BorderMuted, "BorderActive": styles.BorderActive,
168		"NeutralBorder": styles.NeutralBorder, "PickerSeparator": styles.PickerSeparator, "StatusBar": styles.StatusBar,
169		"Accent": styles.Accent, "Border": styles.Border, "Selection": styles.Selection,
170		"CopyStatus": styles.CopyStatus, "PaletteDesc": styles.PaletteDesc,
171		"TabName": styles.TabName, "TabNameActive": styles.TabNameActive,
172		"TabTitle": styles.TabTitle, "TabTitleActive": styles.TabTitleActive,
173		"PickerName": styles.PickerName, "PickerSelectionName": styles.PickerSelectionName,
174		"PickerSelectionMuted": styles.PickerSelectionMuted,
175	}
176}

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