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

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