theme_integration_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/require"
7
8	renderer "github.com/bnema/vev-vt/ansi"
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/domain/terminalcap"
11	"github.com/bnema/vev/internal/protocol"
12	themeui "github.com/bnema/vev/internal/usecase/theme"
13)
14
15func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) protocol.Theme {
16	out := protocol.Theme{
17		Foreground: foreground, Background: background,
18		HasForeground: true, HasBackground: true, SchemeKnown: true,
19	}
20	for slot, color := range palette {
21		out.Palette[slot] = color
22		out.PaletteKnown |= 1 << slot
23	}
24	return out
25}
26
27func chromeStyleSnapshot(t *testing.T, styles themeui.Styles) []byte {
28	t.Helper()
29	frame := renderer.NewFrame(6, 1)
30	for x, style := range []renderer.Style{
31		styles.SurfaceBar, styles.SurfaceInactive, styles.SurfaceRecent,
32		styles.SurfaceActive, styles.BorderMuted, styles.BorderActive,
33	} {
34		frame.Set(x, 0, renderer.Cell{Rune: rune('A' + x), Style: style})
35	}
36	out, err := renderer.New(renderer.Capabilities{}).Draw(frame, []renderer.Damage{renderer.FullRedraw()})
37	require.NoError(t, err)
38	return out
39}
40
41func TestAccentLifecycleGenerationDaemonResolutionAndReload(t *testing.T) {
42	p, release := newBlockingPTY(t)
43	defer release()
44	d, sess, ac, _ := newManualSessionWithPTYs(t, p)
45
46	oldForeground, oldBackground := renderer.RGB{R: 230, G: 230, B: 230}, renderer.RGB{R: 8, G: 9, B: 10}
47	old := lifecycleTheme(oldForeground, oldBackground, map[uint8]renderer.RGB{
48		4: {R: 108, G: 155, B: 217}, 12: {R: 108, G: 155, B: 217},
49	})
50	d.applyTheme(sess, ac, old)
51	beforeClear := ac.getAppliedTheme()
52	require.True(t, beforeClear.Resolved.Accent.Known)
53	require.Equal(t, uint8(4), beforeClear.Resolved.Accent.Slot)
54
55	// A replacement generation's first publication retains pane defaults but
56	// clears every palette bit before daemon style resolution.
57	cleared := old
58	cleared.PaletteKnown = 0
59	cleared.Palette = [16]renderer.RGB{}
60	cleared.Light = true
61	d.applyTheme(sess, ac, cleared)
62	clearedApplied := ac.getAppliedTheme()
63	require.Equal(t, oldForeground, clearedApplied.Raw.Foreground)
64	require.Equal(t, oldBackground, clearedApplied.Raw.Background)
65	require.True(t, clearedApplied.Raw.Light)
66	require.Zero(t, clearedApplied.Raw.PaletteKnown)
67	require.False(t, clearedApplied.Resolved.Accent.Known)
68	require.Equal(t, beforeClear.Generation+1, clearedApplied.Generation)
69	assertSessionDefaultColors(t, sess, oldForeground, oldBackground)
70
71	newForeground, newBackground := renderer.RGB{R: 240, G: 240, B: 240}, renderer.RGB{R: 12, G: 13, B: 14}
72	teal := renderer.RGB{R: 125, G: 181, B: 181}
73	definitive := lifecycleTheme(newForeground, newBackground, map[uint8]renderer.RGB{
74		2: teal, 10: teal, 14: teal,
75		4: {R: 108, G: 155, B: 217}, 12: {R: 108, G: 155, B: 217},
76	})
77	d.applyTheme(sess, ac, definitive)
78	resolved := ac.getAppliedTheme()
79	require.Equal(t, clearedApplied.Generation+1, resolved.Generation)
80	require.True(t, resolved.Resolved.Accent.Known)
81	require.Equal(t, uint8(2), resolved.Resolved.Accent.Slot)
82	require.Equal(t, teal, resolved.Resolved.Accent.RGB)
83	assertSessionDefaultColors(t, sess, newForeground, newBackground)
84
85	// Policy-only reloads resolve the retained definitive snapshot: no client
86	// query or intermediate Theme publication is involved.
87	explicit := domain.Defaults()
88	explicit.ThemeAccent = domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 4}
89	d.ApplyConfig(explicit)
90	reloaded := ac.getAppliedTheme()
91	require.Equal(t, resolved.Raw, reloaded.Raw)
92	require.Greater(t, reloaded.Generation, resolved.Generation)
93	require.Equal(t, uint8(4), reloaded.Resolved.Accent.Slot)
94	require.Equal(t, definitive.Palette, ac.getClientTheme().Palette)
95
96	unknown := explicit
97	unknown.ThemeAccent = domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 6}
98	d.ApplyConfig(unknown)
99	require.False(t, ac.getAppliedTheme().Resolved.Accent.Known, "an explicit unknown slot must not substitute another slot")
100
101	paletteOff := explicit
102	paletteOff.ThemePalette = false
103	d.ApplyConfig(paletteOff)
104	off := ac.getAppliedTheme()
105	require.False(t, off.Raw.UsePalette)
106	require.False(t, off.Resolved.Accent.Known)
107	assertSessionDefaultColors(t, sess, newForeground, newBackground)
108
109	for _, tc := range []struct {
110		name string
111		mode domain.ThemeMode
112		want themeui.Theme
113	}{
114		{name: "forced dark", mode: domain.ThemeDark, want: themeui.BuiltinDark},
115		{name: "forced light", mode: domain.ThemeLight, want: themeui.BuiltinLight},
116	} {
117		t.Run(tc.name, func(t *testing.T) {
118			cfg := domain.Defaults()
119			cfg.Theme = tc.mode
120			d.ApplyConfig(cfg)
121			applied := ac.getAppliedTheme()
122			require.Equal(t, tc.want, applied.Raw)
123			require.False(t, applied.Resolved.Accent.Known)
124			assertSessionDefaultColors(t, sess, tc.want.Foreground, tc.want.Background)
125		})
126	}
127
128	// A timeout-finalized partial report is still one definitive daemon apply;
129	// an unavailable explicit slot remains neutral.
130	partialCfg := domain.Defaults()
131	partialCfg.ThemeAccent = domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 6}
132	d.ApplyConfig(partialCfg)
133	beforePartial := ac.getAppliedTheme()
134	partial := lifecycleTheme(newForeground, newBackground, map[uint8]renderer.RGB{2: teal})
135	d.applyTheme(sess, ac, partial)
136	partialApplied := ac.getAppliedTheme()
137	require.Equal(t, beforePartial.Generation+1, partialApplied.Generation)
138	require.Equal(t, uint16(1<<2), ac.getClientTheme().PaletteKnown)
139	require.False(t, partialApplied.Resolved.Accent.Known)
140}
141
142func TestAccentLifecycleNeutralChromeByteSnapshots(t *testing.T) {
143	raw := lifecycleTheme(
144		renderer.RGB{R: 230, G: 230, B: 230}, renderer.RGB{R: 8, G: 9, B: 10},
145		map[uint8]renderer.RGB{2: {R: 125, G: 181, B: 181}, 10: {R: 125, G: 181, B: 181}},
146	)
147	tests := []struct {
148		name   string
149		config domain.Config
150		want   string
151	}{
152		{
153			name: "palette off",
154			config: func() domain.Config {
155				cfg := domain.Defaults()
156				cfg.ThemePalette = false
157				return cfg
158			}(),
159			want: "\x1b[1;1HABC\x1b[0;7mD\x1b[0mEF\x1b[0m",
160		},
161		{
162			name:   "forced dark",
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;24;24;24;48;2;216;216;216mD\x1b[0;38;2;139;139;139mEF\x1b[0m",
165		},
166		{
167			name:   "forced light",
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;248;248;248;48;2;32;32;32mD\x1b[0;38;2;118;118;118mEF\x1b[0m",
170		},
171	}
172	for _, tt := range tests {
173		t.Run(tt.name, func(t *testing.T) {
174			d := newTestDaemon(t, nil, stubClock{})
175			d.ApplyConfig(tt.config)
176			got := chromeStyleSnapshot(t, d.resolveAppliedTheme(themeui.Theme{
177				Foreground: raw.Foreground, Background: raw.Background, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown,
178				HasFG: true, HasBG: true}, terminalcap.ColorCapabilities{}).Resolved.Styles)
179			require.Equal(t, []byte(tt.want), got)
180		})
181	}
182}

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