styles_test.go RawHistoryFolder

1package theme
2
3import (
4	"testing"
5
6	renderer "github.com/bnema/vev-vt"
7	"github.com/stretchr/testify/require"
8)
9
10func TestRelativeLuminance(t *testing.T) {
11	tests := []struct {
12		name  string
13		color renderer.RGB
14		want  float64
15	}{
16		{name: "black", color: renderer.RGB{}, want: 0},
17		{name: "white", color: renderer.RGB{R: 255, G: 255, B: 255}, want: 1},
18		{name: "sRGB linearization breakpoint", color: renderer.RGB{R: 10}, want: 0.2126 * (10.0 / 255.0 / 12.92)},
19		{name: "nonlinear sRGB", color: renderer.RGB{R: 128, G: 128, B: 128}, want: 0.21586050011389923},
20		{name: "red", color: renderer.RGB{R: 255}, want: 0.2126},
21		{name: "green", color: renderer.RGB{G: 255}, want: 0.7152},
22		{name: "blue", color: renderer.RGB{B: 255}, want: 0.0722},
23	}
24
25	for _, tt := range tests {
26		t.Run(tt.name, func(t *testing.T) {
27			got := RelativeLuminance(tt.color)
28			require.InDelta(t, tt.want, got, 1e-12)
29			require.Equal(t, got, RelativeLuminance(tt.color))
30		})
31	}
32}
33
34func TestContrastRatio(t *testing.T) {
35	black := renderer.RGB{}
36	white := renderer.RGB{R: 255, G: 255, B: 255}
37	middle := renderer.RGB{R: 123, G: 45, B: 67}
38
39	require.InDelta(t, 21, ContrastRatio(black, white), 1e-12)
40	require.InDelta(t, 1, ContrastRatio(middle, middle), 1e-12)
41	require.Equal(t, ContrastRatio(black, middle), ContrastRatio(middle, black))
42}
43
44func TestAccentContrastMinimum(t *testing.T) {
45	require.Equal(t, 3.0, accentContrastMin)
46}
47
48func TestPulseColor(t *testing.T) {
49	rgbBase := renderer.Style{
50		Italic:               true,
51		Foreground:           7,
52		Background:           8,
53		HasForegroundRGB:     true,
54		ForegroundRGB:        renderer.RGB{R: 200, G: 100, B: 50},
55		HasBackgroundRGB:     true,
56		BackgroundRGB:        renderer.RGB{R: 20, G: 40, B: 60},
57		HasUnderlineColor:    true,
58		UnderlineColor:       4,
59		HasUnderlineColorRGB: false,
60	}
61	indexedBase := renderer.Style{Foreground: -1, Background: -1, Inverse: true}
62	mixedBase := rgbBase
63	mixedBase.HasBackgroundRGB = false
64
65	tests := []struct {
66		name      string
67		base      renderer.Style
68		intensity float64
69		want      renderer.Style
70	}{
71		{
72			name:      "RGB starts at the background",
73			base:      rgbBase,
74			intensity: 0,
75			want: func() renderer.Style {
76				style := rgbBase
77				style.Bold = true
78				style.ForegroundRGB = renderer.RGB{R: 20, G: 40, B: 60}
79				return style
80			}(),
81		},
82		{
83			name:      "RGB blends halfway with rounding",
84			base:      rgbBase,
85			intensity: 0.5,
86			want: func() renderer.Style {
87				style := rgbBase
88				style.Bold = true
89				style.ForegroundRGB = renderer.RGB{R: 110, G: 70, B: 55}
90				return style
91			}(),
92		},
93		{
94			name:      "RGB reaches the foreground",
95			base:      rgbBase,
96			intensity: 1,
97			want: func() renderer.Style {
98				style := rgbBase
99				style.Bold = true
100				return style
101			}(),
102		},
103		{
104			name:      "RGB foreground without RGB background uses indexed grayscale",
105			base:      mixedBase,
106			intensity: 0.5,
107			want: func() renderer.Style {
108				style := mixedBase
109				style.Bold = true
110				style.HasForegroundRGB = false
111				style.Foreground = 249
112				return style
113			}(),
114		},
115		{
116			name:      "indexed color uses xterm grayscale start",
117			base:      indexedBase,
118			intensity: 0,
119			want:      renderer.Style{Bold: true, Inverse: true, Foreground: 244, Background: -1},
120		},
121		{
122			name:      "indexed color truncates the grayscale ramp",
123			base:      indexedBase,
124			intensity: 0.5,
125			want:      renderer.Style{Bold: true, Inverse: true, Foreground: 249, Background: -1},
126		},
127		{
128			name:      "indexed color reaches xterm grayscale end",
129			base:      indexedBase,
130			intensity: 1,
131			want:      renderer.Style{Bold: true, Inverse: true, Foreground: 255, Background: -1},
132		},
133	}
134
135	for _, tt := range tests {
136		t.Run(tt.name, func(t *testing.T) {
137			require.Equal(t, tt.want, PulseColor(tt.base, tt.intensity))
138		})
139	}
140}
141
142func TestIndexedForegroundClearsInverseForNonTrueColorFallback(t *testing.T) {
143	fallback := indexedForeground(renderer.Style{
144		Bold:             true,
145		Inverse:          true,
146		Foreground:       -1,
147		HasForegroundRGB: true,
148		ForegroundRGB:    renderer.RGB{R: 1, G: 2, B: 3},
149	}, 2)
150
151	require.False(t, fallback.Inverse, "an indexed foreground must not become an effective background")
152	require.False(t, fallback.HasForegroundRGB)
153	require.Equal(t, 2, fallback.Foreground)
154	require.True(t, fallback.Bold)
155}
156
157func TestMRUFade(t *testing.T) {
158	base := renderer.Style{
159		Bold:                 true,
160		Foreground:           7,
161		Background:           8,
162		HasForegroundRGB:     true,
163		ForegroundRGB:        renderer.RGB{R: 200, G: 100, B: 50},
164		HasBackgroundRGB:     true,
165		BackgroundRGB:        renderer.RGB{R: 40, G: 60, B: 80},
166		HasUnderlineColorRGB: true,
167		UnderlineColorRGB:    renderer.RGB{R: 1, G: 2, B: 3},
168	}
169	terminalTheme := Theme{HasBG: true, Background: renderer.RGB{R: 10, G: 20, B: 30}}
170
171	tests := []struct {
172		name  string
173		base  renderer.Style
174		theme Theme
175		i     int
176		count int
177		want  renderer.Style
178	}{
179		{name: "single entry is unchanged", base: base, theme: terminalTheme, count: 1, want: base},
180		{name: "missing theme background is unchanged", base: base, theme: Theme{}, i: 1, count: 3, want: base},
181		{name: "indexed base is unchanged", base: renderer.DefaultStyle(), theme: terminalTheme, i: 1, count: 3, want: renderer.DefaultStyle()},
182		{name: "newest entry starts unfaded", base: base, theme: terminalTheme, count: 3, want: base},
183		{
184			name:  "middle entry fades thirty percent toward terminal background",
185			base:  base,
186			theme: terminalTheme,
187			i:     1,
188			count: 3,
189			want: func() renderer.Style {
190				style := base
191				style.ForegroundRGB = renderer.RGB{R: 143, G: 76, B: 44}
192				style.BackgroundRGB = renderer.RGB{R: 31, G: 48, B: 65}
193				return style
194			}(),
195		},
196		{
197			name:  "oldest entry fades sixty percent toward terminal background",
198			base:  base,
199			theme: terminalTheme,
200			i:     2,
201			count: 3,
202			want: func() renderer.Style {
203				style := base
204				style.ForegroundRGB = renderer.RGB{R: 86, G: 52, B: 38}
205				style.BackgroundRGB = renderer.RGB{R: 22, G: 36, B: 50}
206				return style
207			}(),
208		},
209	}
210
211	for _, tt := range tests {
212		t.Run(tt.name, func(t *testing.T) {
213			require.Equal(t, tt.want, MRUFade(tt.base, tt.theme, tt.i, tt.count))
214		})
215	}
216}
217
218func TestThemePaletteColor(t *testing.T) {
219	palette := [16]renderer.RGB{
220		3: {R: 0x11, G: 0x22, B: 0x33},
221		7: {R: 0xaa, G: 0xbb, B: 0xcc},
222	}
223	theme := Theme{
224		UsePalette:   true,
225		Palette:      palette,
226		PaletteKnown: 1<<3 | 1<<7,
227	}
228
229	tests := []struct {
230		name string
231		slot int
232		want renderer.RGB
233		ok   bool
234	}{
235		{name: "known slot", slot: 3, want: palette[3], ok: true},
236		{name: "unknown slot", slot: 4},
237		{name: "negative slot", slot: -1},
238		{name: "slot above palette", slot: 16},
239	}
240
241	for _, tt := range tests {
242		t.Run(tt.name, func(t *testing.T) {
243			got, ok := theme.PaletteColor(tt.slot)
244			require.Equal(t, tt.ok, ok)
245			require.Equal(t, tt.want, got)
246		})
247	}
248
249	theme.UsePalette = false
250	got, ok := theme.PaletteColor(3)
251	require.False(t, ok)
252	require.Equal(t, renderer.RGB{}, got)
253}
254
255func TestBuiltinThemesDoNotUsePalette(t *testing.T) {
256	for name, theme := range map[string]Theme{
257		"dark":  BuiltinDark,
258		"light": BuiltinLight,
259	} {
260		t.Run(name, func(t *testing.T) {
261			require.False(t, theme.UsePalette)
262			require.Zero(t, theme.PaletteKnown)
263			require.Equal(t, [16]renderer.RGB{}, theme.Palette)
264		})
265	}
266}
267
268func TestThemeIsComparable(t *testing.T) {
269	themes := map[Theme]bool{BuiltinDark: true}
270	require.True(t, themes[BuiltinDark])
271}

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