main 5041353

css_test.go RawHistoryFolder

1package theme
2
3import (
4	"regexp"
5	"strconv"
6	"strings"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10)
11
12func TestGenerateCSS_StackedTitlebarUsesReadableControlText(t *testing.T) {
13	tests := []struct {
14		name       string
15		background string
16		text       string
17		want       string
18	}{
19		{"dark background", "#775f79", "#3d303f", "#ffffff"},
20		{"light background", "#f0f0f0", "#dddddd", "#000000"},
21		{"readable configured text", "#282828", "#eeeeee", "#eeeeee"},
22	}
23	for _, tt := range tests {
24		t.Run(tt.name, func(t *testing.T) {
25			palette := DefaultLightPalette()
26			palette.SurfaceVariant = tt.background
27			palette.Text = tt.text
28			css := GenerateCSS(palette)
29
30			assert.Contains(t, css, "--control-text: "+tt.want+";")
31			for _, selector := range []string{
32				".stacked-pane-titlebar",
33				".stacked-pane-titlebar label",
34				".stacked-pane-titlebar image",
35			} {
36				block := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{[^}]*\}`).FindString(css)
37				assert.Contains(t, block, "color: var(--control-text);", selector)
38			}
39
40			// Hover and active states must not invalidate the computed contrast.
41			stackedCSS := generateStackedPaneCSS(palette)
42			assert.NotContains(t, stackedCSS, "shade(var(--surface-variant)")
43			hover := regexp.MustCompile(`\.stacked-pane-titlebar\.stacked-pane-title-clickable:hover label\s*\{[^}]*\}`).FindString(css)
44			assert.Contains(t, hover, "text-decoration-line: underline;")
45			assert.NotContains(t, hover, "color:")
46			assert.Contains(t, stackedCSS, "border-left: 0.1875em solid var(--accent);")
47		})
48	}
49}
50
51func TestGenerateCSS_UsesGTKFontForNativeWidgets(t *testing.T) {
52	css := GenerateCSSWithScaleAndFonts(DefaultDarkPalette(), 1.0, FontConfig{
53		SansFont:      "Fira Sans",
54		MonospaceFont: "Fira Code",
55		GtkFont:       "Adwaita Sans",
56	})
57
58	assert.Contains(t, css, `--font-sans: "Fira Sans", sans-serif;`)
59	assert.Contains(t, css, `--font-mono: "Fira Code", monospace;`)
60	assert.Contains(t, css, `--font-gtk: "Adwaita Sans", sans-serif;`)
61	assert.Contains(t, css, "window, tooltip, popover {\n\tfont-family: var(--font-gtk);\n}")
62	assert.NotContains(t, css, "window, tooltip, popover {\n\tfont-family: var(--font-sans);\n}")
63}
64
65func TestGenerateCSS_StandaloneOmniboxWindowUsesTransparentBackground(t *testing.T) {
66	css := GenerateCSS(DefaultDarkPalette())
67
68	if !strings.Contains(css, "window.standalone-omnibox-window") {
69		t.Fatalf("expected standalone omnibox window selector in CSS, got %q", css)
70	}
71	re := regexp.MustCompile(`window\.standalone-omnibox-window\s*\{\s*background-color:\s*transparent\s*;?\s*\}`)
72	if !re.MatchString(css) {
73		t.Fatalf("expected standalone omnibox window background to be transparent")
74	}
75}
76
77func TestGenerateCSSFullWithTiming_VimModePulseUsesConfiguredTransitionDuration(t *testing.T) {
78	// Vim Mode overlay pulse timings scale from the base transition duration:
79	// normal pulse = 3× (150ms -> 450ms), fast pulse = 6× (150ms -> 900ms).
80	css := GenerateCSSFullWithTiming(DefaultDarkPalette(), 1.0, DefaultFontConfig(), DefaultModeColors(), 150)
81
82	assert.Contains(t, css, "animation: vim-mode-overlay-pulse-anim-a 450ms ease-in-out;")
83	assert.Contains(t, css, "animation: vim-mode-overlay-pulse-fast-anim-a 900ms ease-in-out;")
84	assert.NotContains(t, css, "vim-mode-indicator")
85}
86
87func TestGenerateCSS_OmniboxHeaderIsOpaque(t *testing.T) {
88	css := GenerateCSS(DefaultDarkPalette())
89
90	if !strings.Contains(css, ".omnibox-header {") {
91		t.Fatalf("expected omnibox header selector in CSS")
92	}
93	if !strings.Contains(css, "background-color: var(--surface);") {
94		t.Fatalf("expected omnibox header to use an opaque surface background")
95	}
96	if !strings.Contains(css, "background-image: none;") {
97		t.Fatalf("expected omnibox header gradient to be disabled")
98	}
99}
100
101func TestGenerateCSS_OmniboxHeaderBadgeSelectorsExist(t *testing.T) {
102	css := GenerateCSS(DefaultDarkPalette())
103
104	if !strings.Contains(css, ".omnibox-header-badge {") {
105		t.Fatalf("expected omnibox header badge selector in CSS")
106	}
107}
108
109func TestGenerateCSS_OmniboxHeaderBadgeUsesPassiveNeutralChipStyling(t *testing.T) {
110	css := GenerateCSS(DefaultDarkPalette())
111
112	badgeRe := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*background-color:\s*alpha\(var\(--surface-variant\),\s*0\.65\);[^}]*color:\s*var\(--muted\);[^}]*border:\s*0\.0625em solid alpha\(var\(--border\),\s*0\.85\);[^}]*border-radius:\s*0\.1875em;[^}]*padding:\s*0\.0625em 0\.375em;[^}]*font-size:\s*0\.6875em;`)
113	if !badgeRe.MatchString(css) {
114		t.Fatalf("expected omnibox header badge to use passive neutral chip styling")
115	}
116
117	badgeBlock := regexp.MustCompile(`(?s)\.omnibox-header-badge\s*\{[^}]*\}`).FindString(css)
118	if strings.Contains(badgeBlock, "background-color: alpha(var(--accent)") {
119		t.Fatalf("expected omnibox header badge resting state to avoid accent-based styling")
120	}
121	if strings.Contains(badgeBlock, "color: var(--accent)") {
122		t.Fatalf("expected omnibox header badge resting text to avoid accent color")
123	}
124}
125
126func TestGenerateCSS_FavoriteRowsOnlyTintTrailingAffordance(t *testing.T) {
127	css := GenerateCSS(DefaultDarkPalette())
128
129	if !strings.Contains(css, ".omnibox-row.omnibox-row-favorite {") {
130		t.Fatalf("expected favorite row selector in CSS")
131	}
132	if !strings.Contains(css, "border-left: 0.1875em solid var(--warning);") {
133		t.Fatalf("expected favorite rows to mark the left edge with the warning color")
134	}
135	if strings.Contains(css, "border-right: 0.125em solid alpha(var(--warning), 0.55);") {
136		t.Fatalf("expected favorite indicator to move off the right edge")
137	}
138	favoriteMarker := cssRuleBlock(t, css, ".omnibox-row .omnibox-favorite-star")
139	if !strings.Contains(favoriteMarker, "color: mix(var(--warning), var(--muted), 0.45);") {
140		t.Fatalf("expected favorite marker color to be softened")
141	}
142	if strings.Contains(css, ".omnibox-row.omnibox-row-favorite .omnibox-favorite-star-slot") {
143		t.Fatalf("expected favorite highlight to move off the star slot")
144	}
145}
146
147func TestGenerateCSS_OmniboxSearchEntryUsesControlSurface(t *testing.T) {
148	css := GenerateCSS(DefaultDarkPalette())
149
150	containerRe := regexp.MustCompile(`(?s)\.omnibox-container\s*\{[^}]*background-color:\s*var\(--surface\);`)
151	if !containerRe.MatchString(css) {
152		t.Fatalf("expected omnibox container to use the header surface color")
153	}
154	entryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\s*,\s*entry\.omnibox-entry\s*>\s*text\s*\{[^}]*background-color:\s*var\(--surface-variant\);[^}]*background-image:\s*none;[^}]*color:\s*var\(--control-text\);`)
155	if !entryRe.MatchString(css) {
156		t.Fatalf("expected omnibox entry and its text node to use the control surface fill and readable text")
157	}
158	ghostBlock := cssRuleBlock(t, css, ".omnibox-ghost")
159	assert.Contains(t, ghostBlock, "color: var(--control-text);")
160	assert.NotContains(t, ghostBlock, "alpha(var(--control-text)")
161	focusedEntryRe := regexp.MustCompile(`(?s)entry\.omnibox-entry:focus[^\{]*,\s*entry\.omnibox-entry:focus-within[^\{]*,\s*entry\.omnibox-entry:focus-visible[^\{]*,\s*entry\.omnibox-entry:focus\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-within\s*>\s*text[^\{]*,\s*entry\.omnibox-entry:focus-visible\s*>\s*text\s*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`)
162	if !focusedEntryRe.MatchString(css) {
163		t.Fatalf("expected focused omnibox entry text node to keep a distinct control-surface fill")
164	}
165	bangActiveRe := regexp.MustCompile(`(?s)entry\.omnibox-entry\.omnibox-entry-bang-active[^\{]*\{[^}]*background-color:\s*shade\(var\(--surface-variant\),\s*1\.08\);`)
166	if !bangActiveRe.MatchString(css) {
167		t.Fatalf("expected bang-active omnibox entry to keep a distinct control-surface fill")
168	}
169	if strings.Contains(css, "background-color: alpha(var(--bg), 0.88);") || strings.Contains(css, "background-color: shade(var(--bg), 1.05);") {
170		t.Fatalf("expected omnibox entry fills not to derive from the page background")
171	}
172	scrolledRe := regexp.MustCompile(`(?s)\.omnibox-scrolled\s*\{[^}]*background-color:\s*var\(--surface\);`)
173	if !scrolledRe.MatchString(css) {
174		t.Fatalf("expected omnibox results area to use the header surface color")
175	}
176}
177
178func TestGenerateCSS_ContextMenuUsesCompactSingleFrameStyling(t *testing.T) {
179	css := GenerateCSS(DefaultDarkPalette())
180
181	assert.Contains(t, css, "popover.context-menu-popover > contents {")
182	assert.Contains(t, css, ".context-menu {")
183	assert.Contains(t, css, ".context-menu button.context-menu-item {")
184	assert.Contains(t, css, ".context-menu button.context-menu-item:hover,")
185	assert.Contains(t, css, ".context-menu button.context-menu-item:disabled {")
186	assert.Contains(t, css, ".context-menu-separator {")
187
188	popoverBlockRe := regexp.MustCompile(`(?s)popover\.context-menu-popover\s*>\s*contents\s*\{[^}]*\}`)
189	popoverBlock := popoverBlockRe.FindString(css)
190	if popoverBlock == "" {
191		t.Fatal("expected context menu popover contents block in CSS")
192	}
193
194	assert.Contains(t, popoverBlock, "background-color: var(--surface);")
195	assert.Contains(t, popoverBlock, "border: 0.0625em solid alpha(var(--border), 0.85);")
196	assert.Contains(t, popoverBlock, "border-radius: 0.25em;")
197	assert.Contains(t, popoverBlock, "box-shadow:")
198
199	blockRe := regexp.MustCompile(`(?s)\.context-menu\s*\{[^}]*\}`)
200	block := blockRe.FindString(css)
201	if block == "" {
202		t.Fatal("expected context menu block in CSS")
203	}
204
205	assert.NotContains(t, block, "background-color:")
206	assert.Contains(t, block, "padding: 0.125em 0;")
207	assert.Contains(t, block, "min-width: 10.5em;")
208	assert.NotContains(t, block, "border:")
209	assert.NotContains(t, block, "border-radius:")
210	assert.NotContains(t, block, "box-shadow:")
211	assert.NotContains(t, block, "#")
212	assert.NotContains(t, block, "black")
213	assert.NotContains(t, block, "white")
214
215	itemBlockRe := regexp.MustCompile(`(?s)\.context-menu button\.context-menu-item\s*\{[^}]*\}`)
216	itemBlock := itemBlockRe.FindString(css)
217	if itemBlock == "" {
218		t.Fatal("expected context menu item block in CSS")
219	}
220
221	assert.Contains(t, itemBlock, "padding: 0.25em 0.75em;")
222	assert.Contains(t, itemBlock, "min-height: 1.75em;")
223}
224
225func TestGenerateCSSWithScale_DoesNotEmitRootTextScaling(t *testing.T) {
226	css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
227
228	assert.NotContains(t, css, "/* UI Scaling (130%) */")
229	assert.NotContains(t, css, "window {\n\tfont-size:")
230}
231
232func TestGenerateCSSWithScale_DoesNotEmitLineHeightOverrides(t *testing.T) {
233	css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
234
235	assert.NotContains(t, css, "line-height:")
236	assert.NotContains(t, css, "label,\nentry,\nentry > text {\n\tline-height:")
237}
238
239func TestGenerateCSSWithScale_UsesOpaqueOmniboxRowBaseBackground(t *testing.T) {
240	css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
241
242	assert.Contains(t, css, ".omnibox-row {")
243	assert.Contains(t, css, "background-color: var(--surface);")
244	assert.NotContains(t, css, ".omnibox-row {\n\tpadding: 0.5em 0.75em;\n\tmargin: 0;\n\tborder-radius: 0;\n\tborder-left: 0.1875em solid transparent;\n\tborder-bottom: 0.0625em solid alpha(var(--border), 0.35);\n\tbackground-color: transparent;")
245}
246
247func TestGenerateCSSWithScale_UsesMediumWeightForOmniboxSuggestionTitle(t *testing.T) {
248	css := GenerateCSSWithScale(DefaultDarkPalette(), 1.3)
249
250	assert.Contains(t, css, ".omnibox-suggestion-title {")
251	assert.Contains(t, css, "font-weight: 500;")
252	assert.NotContains(t, css, ".omnibox-suggestion-title {\n\tfont-size: 0.875em;\n\tcolor: var(--text);\n\tfont-weight: 400;")
253}
254
255func TestGenerateCSS_AuxiliaryWidgetDimensionsUseScalableUnits(t *testing.T) {
256	css := GenerateCSS(DefaultDarkPalette())
257
258	selectors := []string{
259		".loading-skeleton-spinner",
260		".loading-skeleton-logo",
261		".loading-skeleton-version",
262		"progressbar.osd",
263		"progressbar.osd trough",
264		"progressbar.osd progress",
265	}
266	pxDimension := regexp.MustCompile(`(?m)^\s*(?:-gtk-icon-size|font-size|min-width|min-height|padding|margin|border-radius):[^;]*\dpx`)
267	for _, selector := range selectors {
268		block := cssRuleBlock(t, css, selector)
269		assert.NotRegexp(t, pxDimension, block, selector)
270		assert.Regexp(t, `\d(?:\.\d+)?em`, block, selector)
271	}
272}
273
274func TestGenerateCSS_StackedPaneTitlebarKeepsCompactVerticalChrome(t *testing.T) {
275	css := GenerateCSS(DefaultDarkPalette())
276
277	titlebar := cssRuleBlock(t, css, ".stacked-pane-titlebar")
278	assert.Contains(t, titlebar, "padding: 4px 0.5em;")
279	assert.Contains(t, titlebar, "min-height: 24px;")
280	assert.NotRegexp(t, `(?m)^\s*(?:padding-(?:top|bottom)|min-height):[^;]*em`, titlebar)
281
282	closeButton := cssRuleBlock(t, css, ".stacked-pane-close-button")
283	assert.Contains(t, closeButton, "padding: 3px;")
284	assert.Contains(t, closeButton, "margin: 0 2px;")
285	assert.Contains(t, closeButton, "min-height: 15px;")
286	assert.NotRegexp(t, `(?m)^\s*(?:padding|margin|min-height):[^;]*em`, closeButton)
287
288	// Text and horizontal sizing still follow GTK font scaling.
289	label := cssRuleBlock(t, css, ".stacked-pane-titlebar label")
290	assert.Contains(t, label, "font-size: 0.75em;")
291	assert.Contains(t, closeButton, "min-width: 1.3636em;")
292}
293
294func TestGenerateCSS_OmniboxOuterBordersUseScalableEmUnits(t *testing.T) {
295	css := GenerateCSS(DefaultDarkPalette())
296
297	base := cssRuleBlock(t, css, ".omnibox-container")
298	baseWidth := cssBorderWidthEm(t, base)
299	assert.InDelta(t, 0.09375, baseWidth, 0.000001)
300
301	command := cssRuleBlock(t, css, ".omnibox-container.omnibox-style-command")
302	commandWidth := cssBorderWidthEm(t, command)
303	assert.Greater(t, commandWidth, baseWidth)
304
305	for _, style := range []string{"multiplexer", "minimal"} {
306		block := cssRuleBlock(t, css, ".omnibox-container.omnibox-style-"+style)
307		assert.NotRegexp(t, `(?m)^\s*border(?:-width)?:`, block)
308	}
309}
310
311func cssBorderWidthEm(t *testing.T, block string) float64 {
312	t.Helper()
313	match := regexp.MustCompile(`(?m)^\s*border:\s*([0-9.]+)em\s`).FindStringSubmatch(block)
314	if len(match) != 2 {
315		t.Fatalf("CSS rule does not define an em border: %s", block)
316	}
317	width, err := strconv.ParseFloat(match[1], 64)
318	if err != nil {
319		t.Fatalf("parse border width %q: %v", match[1], err)
320	}
321	return width
322}
323
324func TestGenerateCSS_SelectedFavoritesKeepWarningBorderInEveryStyle(t *testing.T) {
325	css := GenerateCSS(DefaultDarkPalette())
326
327	for _, style := range []string{"multiplexer", "command", "minimal"} {
328		selectedSelector := ".omnibox-style-" + style + " .omnibox-row:selected"
329		favoriteSelector := ".omnibox-style-" + style + " .omnibox-row.omnibox-row-favorite:selected"
330		favoriteBlock := cssRuleBlock(t, css, favoriteSelector)
331		assert.Contains(t, favoriteBlock, "var(--warning)", style)
332		assert.Greater(t, strings.Index(css, favoriteSelector), strings.Index(css, selectedSelector), style)
333	}
334
335	minimal := cssRuleBlock(t, css, ".omnibox-style-minimal .omnibox-row.omnibox-row-favorite:selected")
336	assert.Contains(t, minimal, "border-left: 0.125em solid var(--warning);")
337
338	multiplexer := cssRuleBlock(t, css, ".omnibox-style-multiplexer .omnibox-row.omnibox-row-favorite:selected")
339	assert.Contains(t, multiplexer, "box-shadow: 0.1875em 0 0 var(--warning) inset;")
340}
341
342func TestGenerateCSS_OmniboxIncludesVisualConcepts(t *testing.T) {
343	css := GenerateCSS(DefaultDarkPalette())
344
345	for _, style := range []string{"multiplexer", "command", "minimal"} {
346		assert.Contains(t, css, ".omnibox-container.omnibox-style-"+style)
347	}
348	assert.Contains(t, css, ".omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active")
349	assert.Contains(t, css, ".omnibox-style-command .omnibox-header-badge")
350	assert.Contains(t, css, ".omnibox-style-command .omnibox-favorite-star")
351	assert.Contains(t, css, ".omnibox-style-command entry.omnibox-entry")
352	minimalFavorite := cssRuleBlock(t, css, ".omnibox-style-minimal .omnibox-favorite-star")
353	assert.Contains(t, minimalFavorite, "color: var(--warning);")
354	assert.Contains(t, css, ".omnibox-style-minimal .omnibox-shortcut-badge")
355}
356
357func TestGenerateCSS_ContentAreaTabBarInset(t *testing.T) {
358	css := GenerateCSS(DefaultDarkPalette())
359
360	topBlock := cssRuleBlock(t, css, ".content-area-tabbar-inset-top")
361	if !strings.Contains(topBlock, "padding-top: 2em;") {
362		t.Fatal("expected padding-top: 2em in .content-area-tabbar-inset-top block")
363	}
364	bottomBlock := cssRuleBlock(t, css, ".content-area-tabbar-inset-bottom")
365	if !strings.Contains(bottomBlock, "padding-bottom: 2em;") {
366		t.Fatal("expected padding-bottom: 2em in .content-area-tabbar-inset-bottom block")
367	}
368	if !strings.Contains(css, ".tab-bar") {
369		t.Fatal("expected .tab-bar selector in generated CSS")
370	}
371	if !strings.Contains(css, "min-height: 2em;") {
372		t.Fatal("expected min-height: 2em in .tab-bar block")
373	}
374}
375
376func cssRuleBlock(t *testing.T, css, selector string) string {
377	t.Helper()
378	re := regexp.MustCompile(regexp.QuoteMeta(selector) + `\s*\{([^}]*)\}`)
379	matches := re.FindStringSubmatch(css)
380	if len(matches) != 2 {
381		t.Fatalf("expected %s selector in generated CSS", selector)
382	}
383	return matches[1]
384}
385
386func TestGenerateCSS_TouchpadNavigationIndicatorFadesWholeOverlay(t *testing.T) {
387	css := GenerateCSS(DefaultDarkPalette())
388
389	baseBlock := cssRuleBlock(t, css, ".touchpad-navigation-indicator")
390	assert.Contains(t, baseBlock, "opacity: 1;")
391	assert.Contains(t, baseBlock, "filter: blur(0px);")
392	assert.Contains(t, baseBlock, "backdrop-filter: blur(0.75em);")
393	assert.Contains(t, baseBlock, "transition: opacity 220ms ease-out,")
394	assert.Contains(t, baseBlock, "filter 220ms ease-out,")
395	assert.Contains(t, baseBlock, "backdrop-filter 220ms ease-out,")
396	assert.Contains(t, baseBlock, "box-shadow 220ms ease-out;")
397
398	hidingBlock := cssRuleBlock(t, css, ".touchpad-navigation-indicator.hiding")
399	assert.Contains(t, hidingBlock, "opacity: 0;")
400	assert.Contains(t, hidingBlock, "filter: blur(0.0625em);")
401	assert.Contains(t, hidingBlock, "box-shadow:")
402}
403
404func TestGenerateCSS_DoesNotEmitUnsupportedGTKProperties(t *testing.T) {
405	css := GenerateCSS(DefaultDarkPalette())
406
407	assert.NotContains(t, css, "pointer-events:")
408	assert.NotContains(t, css, "text-align:")
409}
410
411func TestGenerateCSS_SessionContainerHasNoMinWidth(t *testing.T) {
412	css := GenerateCSS(DefaultDarkPalette())
413	block := cssRuleBlock(t, css, ".session-manager-container")
414	assert.NotContains(t, block, "min-width:")
415	assert.Contains(t, block, "background-color: var(--surface-variant);")
416}
417
418func TestVimModeUsesOwnColor(t *testing.T) {
419	css := generateVimModeCSS(120)
420	if strings.Contains(css, "--pane-mode-color") || !strings.Contains(css, "--vim-mode-color") {
421		t.Fatalf("Vim mode CSS must use Vim color")
422	}
423	if !strings.Contains(generateModeLegendCSS(), "var(--vim-mode-color)") {
424		t.Fatal("legend missing Vim color")
425	}
426	if !strings.Contains(DefaultModeColors().ToCSSVars(), "--vim-mode-color: #F7768E") {
427		t.Fatal("palette missing Vim color")
428	}
429}
430
431func TestModeLegendCSSStylesToasterPillAndBand(t *testing.T) {
432	css := generateModeLegendCSS()
433	for _, mode := range []string{"pane", "vim", "resize", "tab", "session", "prefix"} {
434		assert.Contains(t, css, ".mode-toaster-pill.mode-toaster-"+mode+" {", mode)
435		assert.Contains(t, css, ".omnibox-container.mode-legend-panel.mode-legend-"+mode+" {", mode)
436	}
437	for _, class := range []string{".mode-toaster-chevron", ".mode-toaster-interactive", ".mode-legend-columns", ".mode-legend-column", ".mode-legend-hint"} {
438		assert.Contains(t, css, class)
439	}
440	assert.NotContains(t, css, "mode-legend-title", "the legend no longer has its own heading")
441	assert.NotContains(t, css, "mode-legend-content", "the flow box is gone")
442	assert.NotContains(t, GenerateCSS(DefaultDarkPalette()), ".toast-vim-mode", "modes no longer reuse the generic toast")
443}

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 8

Tags 42

↑↓ moveenter openesc close