css.go RawHistoryFolder

1// Package theme provides GTK CSS styling for UI components.
2package theme
3
4import (
5	"fmt"
6	"strings"
7)
8
9// GenerateCSS creates GTK4 CSS using the provided palette with default scale.
10func GenerateCSS(p Palette) string {
11	return GenerateCSSWithScale(p, 1.0)
12}
13
14// GenerateCSSWithScale creates GTK4 CSS using the provided palette and UI scale factor.
15// Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings.
16func GenerateCSSWithScale(p Palette, scale float64) string {
17	return GenerateCSSWithScaleAndFonts(p, scale, DefaultFontConfig())
18}
19
20// FontConfig holds font settings for CSS generation.
21type FontConfig struct {
22	SansFont      string
23	MonospaceFont string
24	GtkFont       string
25}
26
27// DefaultFontConfig returns safe font fallbacks.
28func DefaultFontConfig() FontConfig {
29	return FontConfig{
30		SansFont:      "sans-serif",
31		MonospaceFont: "monospace",
32		GtkFont:       DefaultGTKFont(),
33	}
34}
35
36// FontCSSVars generates CSS custom properties for fonts.
37func FontCSSVars(fonts FontConfig) string {
38	var sb strings.Builder
39	// Quote the configured family; include generic fallback.
40	fmt.Fprintf(&sb, "  --font-sans: %q, sans-serif;\n", fonts.SansFont)
41	fmt.Fprintf(&sb, "  --font-mono: %q, monospace;\n", fonts.MonospaceFont)
42	fmt.Fprintf(&sb, "  --font-gtk: %q, sans-serif;\n", fonts.GtkFont)
43	return sb.String()
44}
45
46func generateFontCSS() string {
47	return `/* Font styling */
48window, tooltip, popover {
49	font-family: var(--font-gtk);
50}
51`
52}
53
54const defaultTransitionDurationMs = 120
55
56// GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts.
57// Scale affects widget sizing that uses relative units; text scaling is handled via GtkSettings.
58// Uses default mode colors.
59func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string {
60	return GenerateCSSFull(p, scale, fonts, DefaultModeColors())
61}
62
63// GenerateCSSFull creates GTK4 CSS using all provided configuration with the
64// default transition duration.
65// Text scaling is handled via GtkSettings; CSS here stays scale-independent.
66func GenerateCSSFull(p Palette, _ float64, fonts FontConfig, modeColors ModeColors) string {
67	return GenerateCSSFullWithTiming(p, 1.0, fonts, modeColors, defaultTransitionDurationMs)
68}
69
70// GenerateCSSFullWithTiming creates GTK4 CSS using all provided configuration
71// and an explicit transition duration from workspace styling config.
72func GenerateCSSFullWithTiming(p Palette, _ float64, fonts FontConfig, modeColors ModeColors, transitionDurationMs int) string {
73	defaults := DefaultFontConfig()
74	fonts = FontConfig{
75		SansFont:      Coalesce(fonts.SansFont, defaults.SansFont),
76		MonospaceFont: Coalesce(fonts.MonospaceFont, defaults.MonospaceFont),
77		GtkFont:       Coalesce(fonts.GtkFont, defaults.GtkFont),
78	}
79
80	var sb strings.Builder
81
82	// CSS custom properties (variables) - GTK4 uses :root selector
83	sb.WriteString("/* Theme variables */\n")
84	sb.WriteString(":root {\n")
85	sb.WriteString(p.ToCSSVars())
86	sb.WriteString(FontCSSVars(fonts))
87	sb.WriteString(modeColors.ToCSSVars())
88	sb.WriteString("}\n\n")
89
90	// Global font styling
91	sb.WriteString(generateFontCSS())
92	sb.WriteString("\n")
93
94	// Tab bar styling
95	sb.WriteString(generateTabBarCSS(p))
96	sb.WriteString("\n")
97
98	// Omnibox and modal legend styling
99	sb.WriteString(generateOmniboxCSS(p))
100	sb.WriteString(generateModeLegendCSS())
101	sb.WriteString("\n")
102
103	// Find bar styling
104	sb.WriteString(generateFindBarCSS(p))
105	sb.WriteString("\n")
106
107	// Pane styling
108	sb.WriteString(generatePaneCSS(p))
109	sb.WriteString("\n")
110
111	// Stacked pane styling
112	sb.WriteString(generateStackedPaneCSS(p))
113	sb.WriteString("\n")
114
115	// Vim mode styling
116	sb.WriteString(generateVimModeCSS(transitionDurationMs))
117	sb.WriteString("\n")
118
119	// Progress bar styling
120	sb.WriteString(generateProgressBarCSS(p))
121	sb.WriteString("\n")
122
123	// Touchpad history navigation indicator styling
124	sb.WriteString(generateTouchpadNavigationIndicatorCSS(p))
125	sb.WriteString("\n")
126
127	// Toaster styling
128	sb.WriteString(generateToasterCSS(p))
129	sb.WriteString("\n")
130
131	// Link status overlay styling
132	sb.WriteString(generateLinkStatusCSS(p))
133	sb.WriteString("\n")
134
135	// Session manager styling
136	sb.WriteString(generateSessionManagerCSS(p))
137	sb.WriteString("\n")
138
139	// Context menu styling
140	sb.WriteString(generateContextMenuCSS())
141	sb.WriteString("\n")
142
143	// Tab picker styling
144	sb.WriteString(generateTabPickerCSS(p))
145	sb.WriteString("\n")
146
147	// Permission popup styling
148	sb.WriteString(generatePermissionPopupCSS(p))
149	sb.WriteString("\n")
150
151	// Floating pane styling
152	sb.WriteString(generateFloatingPaneCSS(p))
153	sb.WriteString("\n")
154
155	// WebRTC permission activity indicator styling
156	sb.WriteString(generateWebRTCPermissionIndicatorCSS(p))
157	sb.WriteString("\n")
158
159	// Accent picker styling
160	sb.WriteString(generateAccentPickerCSS(p))
161	sb.WriteString("\n")
162
163	appendNativeSidebarCSS(&sb, p)
164
165	return sb.String()
166}
167
168func appendNativeSidebarCSS(sb *strings.Builder, p Palette) {
169	// Native sidebar styling
170	sb.WriteString(generateSidebarCSS(p))
171	sb.WriteString("\n")
172	sb.WriteString(generateHistorySidebarCSS(p))
173	sb.WriteString("\n")
174	sb.WriteString(generateFavoritesSidebarCSS(p))
175}
176
177// generateTabBarCSS creates tab bar styles.
178// Uses em units for scalable UI.
179func generateTabBarCSS(p Palette) string {
180	return `/* Tab bar styling */
181/* Content area inset for overlaid tab bars — uses the same 2em token as
182   .tab-bar min-height and remains static to avoid OSR vertical reallocations. */
183.content-area-tabbar-inset-top {
184	padding-top: 2em;
185}
186
187.content-area-tabbar-inset-bottom {
188	padding-bottom: 2em;
189}
190
191.tab-bar {
192	background-color: var(--surface);
193	border-top: 0.125em solid var(--border);
194	padding: 0;
195	min-height: 2em;
196}
197
198/* Tab button styling */
199button.tab-button {
200	background-color: var(--surface-variant);
201	background-image: none;
202	border: none;
203	border-right: 0.0625em solid var(--border);
204	border-radius: 0;
205	padding: 0.25em 0.5em;
206	transition: background-color 200ms ease-in-out;
207}
208
209button.tab-button:hover {
210	background-color: shade(var(--surface-variant), 1.2);
211}
212
213button.tab-button.tab-button-active {
214	background-color: shade(var(--surface-variant), 1.2);
215	border-bottom: 0.125em solid var(--accent);
216	font-weight: 600;
217}
218
219/* Tab title text */
220.tab-title {
221	font-size: 0.75em;
222	color: var(--text);
223	font-weight: 500;
224}
225`
226}
227
228// generateOmniboxCSS creates omnibox styles.
229// Uses em units for scalable UI.
230func generateOmniboxCSS(p Palette) string {
231	return `/* ===== Omnibox Styling ===== */
232
233window.standalone-omnibox-window {
234	background-color: transparent;
235}
236
237/* Omnibox outer container - for positioning in overlay */
238.omnibox-outer {
239	/* Positioning is handled via SetHalign/SetValign/SetMarginTop in Go */
240}
241
242/* Omnibox main container - the visible popup */
243.omnibox-container {
244	background-color: var(--surface);
245	background-image: none;
246	border: 0.09375em solid alpha(var(--border), 0.9);
247	border-radius: 0.1875em;
248	box-shadow: 0 0.875em 2.5em alpha(black, 0.24), 0 0.0625em 0 alpha(white, 0.05) inset;
249	padding: 0;
250}
251
252/* Header with History/Favorites toggle */
253.omnibox-header {
254	background-color: var(--surface);
255	background-image: none;
256	border-bottom: 0.0625em solid alpha(var(--border), 0.75);
257	padding: 0.375em 0.75em;
258}
259
260.omnibox-header-btn {
261	background-color: alpha(var(--surface), 0.12);
262	background-image: none;
263	border: 0.0625em solid transparent;
264	border-radius: 0.125em;
265	padding: 0.25em 0.75em;
266	margin-right: 0.5em;
267	font-size: 0.8125em;
268	font-weight: 500;
269	color: var(--muted);
270	transition: all 100ms ease-in-out;
271}
272
273.omnibox-header-btn:hover {
274	background-color: alpha(var(--accent), 0.14);
275	border-color: alpha(var(--accent), 0.18);
276	color: var(--text);
277}
278
279.omnibox-header-btn.omnibox-header-active {
280	background-color: alpha(var(--accent), 0.18);
281	border-color: alpha(var(--accent), 0.28);
282	color: var(--accent);
283	font-weight: 600;
284}
285
286/* Search entry field */
287entry.omnibox-entry,
288entry.omnibox-entry > text {
289	background-color: var(--surface-variant);
290	background-image: none;
291	color: var(--control-text);
292	caret-color: var(--accent);
293}
294
295entry.omnibox-entry {
296	border: 0.0625em solid alpha(var(--border), 0.82);
297	border-radius: 0.125em;
298	padding: 0.625em 0.75em;
299	margin: 0.5em 0.75em;
300	font-size: 1em;
301	box-shadow: 0 0.0625em 0 alpha(white, 0.04) inset;
302}
303
304entry.omnibox-entry:focus,
305entry.omnibox-entry:focus-within,
306entry.omnibox-entry:focus-visible,
307entry.omnibox-entry:focus > text,
308entry.omnibox-entry:focus-within > text,
309entry.omnibox-entry:focus-visible > text {
310	border-color: var(--accent);
311	background-color: shade(var(--surface-variant), 1.08);
312	background-image: none;
313	outline-style: none;
314	outline-width: 0px;
315	outline-color: transparent;
316}
317
318/* Override any internal focus styling */
319entry.omnibox-entry > text:focus,
320entry.omnibox-entry > text:focus-visible {
321	outline-style: none;
322	outline-color: transparent;
323}
324
325/* Scrolled window for suggestions */
326.omnibox-scrolled {
327	background-color: var(--surface);
328	border-top: 0.0625em solid alpha(var(--border), 0.65);
329}
330
331/* List box */
332.omnibox-listbox {
333	background-color: transparent;
334}
335
336/* Suggestion rows */
337.omnibox-row {
338	padding: 0.5em 0.75em;
339	margin: 0;
340	border-radius: 0;
341	border-left: 0.1875em solid transparent;
342	border-bottom: 0.0625em solid alpha(var(--border), 0.35);
343	background-color: var(--surface);
344	background-image: none;
345	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
346	min-height: 2.75em;
347}
348
349.omnibox-row:last-child {
350	border-bottom: none;
351}
352
353.omnibox-row:hover {
354	background-color: alpha(var(--accent), 0.12);
355	border-left: 0.1875em solid var(--accent);
356}
357
358.omnibox-row:selected {
359	background-color: alpha(var(--accent), 0.2);
360	border-left: 0.1875em solid var(--accent);
361}
362
363/* Favorite indicator in history mode - keep the row neutral and use the left selection stripe */
364.omnibox-row.omnibox-row-favorite {
365	background-color: transparent;
366	border-left: 0.1875em solid var(--warning);
367}
368
369.omnibox-row.omnibox-row-favorite:hover {
370	background-color: alpha(var(--accent), 0.12);
371	border-left: 0.1875em solid var(--warning);
372}
373
374.omnibox-row.omnibox-row-favorite:selected {
375	background-color: alpha(var(--accent), 0.2);
376	border-left: 0.1875em solid var(--warning);
377}
378
379/* Favicon in omnibox rows */
380.omnibox-favicon {
381	min-width: 1em;
382	min-height: 1em;
383	margin-right: 0.5em;
384}
385
386/* Suggestion title/URL */
387.omnibox-suggestion-title {
388	font-size: 0.875em;
389	color: var(--text);
390	font-weight: 500;
391}
392
393/* Also style labels inside omnibox rows directly */
394.omnibox-row label {
395	color: var(--text);
396}
397
398.omnibox-row:selected .omnibox-suggestion-title {
399	color: var(--text);
400	font-weight: 500;
401}
402
403/* URL text below title - use .omnibox-row prefix for specificity over .omnibox-row label */
404.omnibox-row .omnibox-suggestion-url {
405	font-size: 0.75em;
406	color: alpha(var(--muted), 0.72);
407	font-weight: 400;
408}
409
410.omnibox-row .omnibox-favorite-star {
411	color: mix(var(--warning), var(--muted), 0.45);
412	opacity: 0.75;
413	margin-left: 0.625em;
414	margin-right: 0.125em;
415	font-family: var(--font-mono);
416	font-size: 1em;
417	font-weight: 700;
418}
419
420.omnibox-row:selected .omnibox-favorite-star {
421	color: mix(var(--warning), var(--text), 0.35);
422	opacity: 0.88;
423}
424
425.omnibox-row:selected .omnibox-suggestion-url {
426	color: alpha(var(--muted), 0.75);
427}
428
429/* Keyboard shortcut badge */
430.omnibox-shortcut-badge {
431	background-color: alpha(var(--muted), 0.3);
432	color: var(--muted);
433	border-radius: 0.25em;
434	padding: 0.125em 0.375em;
435	font-size: 0.625em;
436	font-weight: 500;
437	font-family: var(--font-mono);
438	margin-left: 0.5em;
439}
440
441.omnibox-row:hover .omnibox-shortcut-badge {
442	background-color: alpha(var(--accent), 0.2);
443	color: var(--accent);
444}
445
446.omnibox-row:selected .omnibox-shortcut-badge {
447	background-color: alpha(var(--accent), 0.3);
448	color: var(--accent);
449}
450
451/* Bang indicator badge in omnibox header */
452.omnibox-bang-badge {
453	background-color: alpha(var(--accent), 0.18);
454	color: var(--accent);
455	border-radius: 0.25em;
456	padding: 0.125em 0.5em;
457	font-size: 0.6875em;
458	font-weight: 600;
459}
460
461/* Header badge used for omnibox state */
462.omnibox-header-badge {
463	background-color: alpha(var(--surface-variant), 0.65);
464	color: var(--muted);
465	border: 0.0625em solid alpha(var(--border), 0.85);
466	border-radius: 0.1875em;
467	padding: 0.0625em 0.375em;
468	font-size: 0.6875em;
469	font-weight: 500;
470}
471
472/* Zoom indicator in omnibox header */
473.omnibox-zoom-indicator {
474	background-color: alpha(var(--accent), 0.2);
475	color: var(--accent);
476	border-radius: 0.25em;
477	padding: 0.125em 0.5em;
478	font-size: 0.6875em;
479	font-weight: 600;
480	/* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
481}
482
483entry.omnibox-entry.omnibox-entry-bang-active,
484entry.omnibox-entry.omnibox-entry-bang-active:focus,
485entry.omnibox-entry.omnibox-entry-bang-active:focus-within,
486entry.omnibox-entry.omnibox-entry-bang-active:focus-visible,
487entry.omnibox-entry.omnibox-entry-bang-active > text,
488entry.omnibox-entry.omnibox-entry-bang-active:focus > text,
489entry.omnibox-entry.omnibox-entry-bang-active:focus-within > text,
490entry.omnibox-entry.omnibox-entry-bang-active:focus-visible > text {
491	border-color: var(--accent);
492	background-color: shade(var(--surface-variant), 1.08);
493	background-image: none;
494}
495
496.omnibox-row.omnibox-row-bang .omnibox-suggestion-title {
497	color: var(--accent);
498	font-weight: 600;
499}
500
501/* Ghost text autocomplete overlay - positioned to align with entry text */
502.omnibox-ghost {
503	font-size: 1em;
504	color: var(--control-text);
505	margin-top: -0.0625em; /* Nudge up 1px to align with entry text baseline */
506	background-color: transparent;
507}
508
509/* Multiplexer: segmented status line, square prompt, and strong active rail. */
510.omnibox-container.omnibox-style-multiplexer {
511	border-radius: 0;
512	border-color: var(--border);
513	box-shadow: 0 1.125em 3em alpha(black, 0.34);
514}
515
516.omnibox-style-multiplexer .omnibox-header {
517	padding: 0;
518	min-height: 2em;
519	background-color: shade(var(--surface), 0.94);
520}
521
522.omnibox-style-multiplexer .omnibox-header-btn {
523	margin: 0;
524	padding: 0.375em 1em;
525	border: none;
526	border-right: 0.0625em solid var(--border);
527	border-radius: 0;
528	font-family: var(--font-mono);
529	font-size: 0.75em;
530	font-weight: 600;
531}
532
533.omnibox-style-multiplexer .omnibox-header-btn.omnibox-header-active {
534	background-color: var(--accent);
535	color: var(--accent-text);
536}
537
538.omnibox-style-multiplexer .omnibox-header-badge {
539	padding: 0.1875em 0.5em;
540	background-color: transparent;
541	border: none;
542	color: var(--muted);
543	font-family: var(--font-mono);
544	font-size: 0.6875em;
545	font-weight: 600;
546}
547
548.omnibox-style-multiplexer .omnibox-bang-badge,
549.omnibox-style-multiplexer .omnibox-zoom-indicator,
550.omnibox-style-multiplexer .omnibox-shortcut-badge {
551	border-radius: 0;
552	font-family: var(--font-mono);
553}
554
555.omnibox-style-multiplexer entry.omnibox-entry {
556	margin: 0.75em;
557	border-radius: 0;
558	border-left: 0.25em solid var(--accent);
559	font-family: var(--font-mono);
560}
561
562.omnibox-style-multiplexer .omnibox-row {
563	border-left-width: 0.25em;
564}
565
566.omnibox-style-multiplexer .omnibox-row:selected {
567	background-color: alpha(var(--accent), 0.14);
568	box-shadow: 0.1875em 0 0 var(--accent) inset;
569}
570
571.omnibox-style-multiplexer .omnibox-row.omnibox-row-favorite:selected {
572	border-left-color: var(--warning);
573	box-shadow: 0.1875em 0 0 var(--warning) inset;
574}
575
576.omnibox-style-multiplexer .omnibox-favorite-star {
577	min-width: 1.25em;
578	margin: 0 0.375em 0 0.125em;
579	padding: 0;
580	color: var(--warning);
581	opacity: 0.85;
582}
583
584/* Command: a calmer command palette with a floating input and compact tabs. */
585.omnibox-container.omnibox-style-command {
586	border: 0.125em solid alpha(var(--border), 0.95);
587	border-radius: 0.625em;
588	box-shadow: 0 1.25em 3.5em alpha(black, 0.38), 0 0 0 0.0625em alpha(white, 0.04) inset;
589	padding: 0.375em;
590}
591
592.omnibox-style-command .omnibox-header {
593	border-bottom: none;
594	padding: 0.25em 0.375em 0.125em;
595}
596
597.omnibox-style-command .omnibox-header-btn {
598	border-radius: 1em;
599	padding: 0.25em 0.75em;
600	font-size: 0.75em;
601}
602
603.omnibox-style-command .omnibox-header-btn.omnibox-header-active {
604	background-color: alpha(var(--accent), 0.13);
605	border-color: transparent;
606}
607
608.omnibox-style-command .omnibox-header-badge {
609	padding: 0.1875em 0.5em;
610	background-color: transparent;
611	border: none;
612	color: var(--muted);
613	font-family: var(--font-mono);
614	font-size: 0.6875em;
615	font-weight: 600;
616}
617
618.omnibox-style-command entry.omnibox-entry {
619	margin: 0.5em 0.375em 0.75em;
620	padding: 0.75em 0.875em;
621	border-radius: 0.375em;
622	font-family: var(--font-mono);
623	box-shadow: 0 0 0 0.0625em alpha(var(--accent), 0.12), 0 0.5em 1.5em alpha(black, 0.12);
624}
625
626.omnibox-style-command .omnibox-scrolled {
627	border-top-color: alpha(var(--border), 0.35);
628}
629
630.omnibox-style-command .omnibox-row {
631	margin: 0.125em 0.25em;
632	border: none;
633	border-radius: 0.25em;
634}
635
636.omnibox-style-command .omnibox-row:selected {
637	background-color: alpha(var(--accent), 0.16);
638	border-left-color: transparent;
639}
640
641.omnibox-style-command .omnibox-row.omnibox-row-favorite:selected {
642	border-left: 0.1875em solid var(--warning);
643}
644
645.omnibox-style-command .omnibox-favorite-star {
646	min-width: 1.25em;
647	margin: 0 0.375em 0 0.125em;
648	padding: 0;
649	color: var(--warning);
650	opacity: 0.85;
651}
652
653/* Minimal: terminal typography and hairline structure with almost no chrome. */
654.omnibox-container.omnibox-style-minimal {
655	border-radius: 0;
656	box-shadow: 0 0.75em 2em alpha(black, 0.2);
657}
658
659.omnibox-style-minimal .omnibox-header {
660	padding: 0.25em 0.75em;
661	background-color: var(--surface);
662}
663
664.omnibox-style-minimal .omnibox-header-btn {
665	margin-right: 1em;
666	padding: 0.25em 0;
667	border: none;
668	border-bottom: 0.125em solid transparent;
669	border-radius: 0;
670	background-color: transparent;
671	font-family: var(--font-mono);
672	font-size: 0.75em;
673}
674
675.omnibox-style-minimal .omnibox-header-btn.omnibox-header-active {
676	background-color: transparent;
677	border-bottom-color: var(--accent);
678}
679
680.omnibox-style-minimal entry.omnibox-entry {
681	margin: 0;
682	padding: 0.75em;
683	border: none;
684	border-bottom: 0.0625em solid var(--border);
685	border-radius: 0;
686	background-color: var(--surface);
687	font-family: var(--font-mono);
688	box-shadow: none;
689}
690
691.omnibox-style-minimal .omnibox-scrolled {
692	border-top: none;
693}
694
695.omnibox-style-minimal .omnibox-row {
696	padding-top: 0.4375em;
697	padding-bottom: 0.4375em;
698	border-left-width: 0.125em;
699	font-family: var(--font-mono);
700}
701
702.omnibox-style-minimal .omnibox-row.omnibox-row-favorite:selected {
703	border-left: 0.125em solid var(--warning);
704}
705
706.omnibox-style-minimal .omnibox-favorite-star {
707	color: var(--warning);
708	opacity: 0.85;
709}
710
711.omnibox-style-minimal .omnibox-header-badge {
712	background-color: transparent;
713	border: none;
714	color: var(--muted);
715	font-family: var(--font-mono);
716}
717
718.omnibox-style-minimal .omnibox-shortcut-badge {
719	background-color: transparent;
720	border: 0.0625em solid alpha(var(--border), 0.7);
721	border-radius: 0;
722}
723`
724}
725
726// generateFindBarCSS creates find bar styles.
727// Uses em units for scalable UI.
728func generateFindBarCSS(p Palette) string {
729	_ = p
730	return `/* ===== Find Bar Styling ===== */
731.find-bar-outer {
732	margin: 0.5em;
733}
734
735.find-bar-container {
736	background-color: var(--surface-variant);
737	border: 0.0625em solid var(--border);
738	border-radius: 0.25em;
739	box-shadow: 0 0.25em 0.75em alpha(black, 0.2);
740	padding: 0.5em;
741	min-width: 20em;
742}
743
744.find-bar-input-row {
745	margin-bottom: 0.25em;
746}
747
748.find-bar-entry {
749	background-color: var(--bg);
750	color: var(--text);
751	border: 0.0625em solid var(--border);
752	border-radius: 0.1875em;
753	padding: 0.4em 0.5em;
754	caret-color: var(--accent);
755}
756
757entry.find-bar-entry:focus,
758entry.find-bar-entry:focus-within,
759entry.find-bar-entry:focus-visible {
760	border-color: var(--accent);
761	outline-style: none;
762	outline-width: 0px;
763	outline-color: transparent;
764}
765
766.find-bar-entry.not-found {
767	background-color: alpha(#ef4444, 0.15);
768	border-color: #ef4444;
769}
770
771.find-bar-count {
772	font-size: 0.8em;
773	color: var(--muted);
774	min-width: 3.5em;
775}
776
777.find-bar-count.find-bar-count-has {
778	color: var(--accent);
779}
780
781.find-bar-nav,
782.find-bar-close,
783.find-bar-toggle {
784	background-image: none;
785	background-color: alpha(var(--bg), 0.6);
786	border: 0.0625em solid var(--border);
787	border-radius: 0.1875em;
788	padding: 0.25em 0.5em;
789	transition: background-color 120ms ease-in-out, border-color 120ms ease-in-out;
790}
791
792.find-bar-nav:hover,
793.find-bar-close:hover,
794.find-bar-toggle:hover {
795	background-color: alpha(var(--accent), 0.15);
796}
797
798.find-bar-toggle:checked {
799	background-color: alpha(var(--accent), 0.2);
800	border-color: var(--accent);
801	color: var(--accent);
802}
803`
804}
805
806// generatePaneCSS creates pane border styles.
807// Uses em units for scalable UI.
808// generateVimModeCSS creates the pane-local Vim Mode accent and pulse styling.
809// It uses --vim-mode-color (from workspace.styling.vim_mode_color).
810func generateVimModeCSS(transitionDurationMs int) string {
811	if transitionDurationMs <= 0 {
812		transitionDurationMs = defaultTransitionDurationMs
813	}
814	normalPulseMs := transitionDurationMs * 3
815	fastPulseMs := transitionDurationMs * 6
816	return fmt.Sprintf(`/* ===== Vim Mode Styling ===== */
817
818/* Vim mode frame on the active pane; pulses use this same widget. */
819.vim-mode-active {
820	box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35);
821	border-radius: 0;
822}
823
824/* Frame pulse keyframes — start/end at the active accent state so the
825   frame keeps its Vim mode border while briefly flaring brighter. The separate
826   -anim-a and -anim-b pairs are required to restart repeated pulse animations. */
827@keyframes vim-mode-overlay-pulse-anim-a {
828	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
829	35%%  { box-shadow: inset 0 0 0 0.2em alpha(var(--vim-mode-color), 0.78); }
830	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
831}
832
833@keyframes vim-mode-overlay-pulse-anim-b {
834	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
835	35%%  { box-shadow: inset 0 0 0 0.2em alpha(var(--vim-mode-color), 0.78); }
836	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
837}
838
839@keyframes vim-mode-overlay-pulse-fast-anim-a {
840	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
841	25%%  { box-shadow: inset 0 0 0 0.26em alpha(var(--vim-mode-color), 0.95); }
842	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
843}
844
845@keyframes vim-mode-overlay-pulse-fast-anim-b {
846	0%%   { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
847	25%%  { box-shadow: inset 0 0 0 0.26em alpha(var(--vim-mode-color), 0.95); }
848	100%% { box-shadow: inset 0 0 0 0.125em alpha(var(--vim-mode-color), 0.35); }
849}
850
851/* Pane overlay scroll pulse — derived from transition_duration. */
852.vim-mode-pulse.vim-mode-pulse-cycle-a {
853	animation: vim-mode-overlay-pulse-anim-a %dms ease-in-out;
854}
855
856.vim-mode-pulse.vim-mode-pulse-cycle-b {
857	animation: vim-mode-overlay-pulse-anim-b %dms ease-in-out;
858}
859
860.vim-mode-pulse-fast.vim-mode-pulse-cycle-a {
861	animation: vim-mode-overlay-pulse-fast-anim-a %dms ease-in-out;
862}
863
864.vim-mode-pulse-fast.vim-mode-pulse-cycle-b {
865	animation: vim-mode-overlay-pulse-fast-anim-b %dms ease-in-out;
866}
867`, normalPulseMs, normalPulseMs, fastPulseMs, fastPulseMs)
868}
869
870func generatePaneCSS(p Palette) string {
871	return `/* ===== Pane Styling ===== */
872
873/* Pane overlay container - theme background prevents white flash */
874.pane-overlay {
875	background-color: var(--bg);
876}
877
878/* WebView widget - theme background prevents white flash during page load */
879.webview-themed {
880	background-color: var(--bg);
881}
882
883/* Loading skeleton - shown until WebView paints */
884.loading-skeleton {
885	background-color: var(--bg);
886}
887
888.loading-skeleton-spinner {
889	-gtk-icon-size: 2.1818em;
890	min-width: 2.1818em;
891	min-height: 2.1818em;
892	color: var(--muted);
893	opacity: 0.4;
894}
895
896.loading-skeleton-logo {
897	opacity: 0.08;
898	min-width: 17.4545em;
899	min-height: 17.4545em;
900}
901
902.loading-skeleton-version {
903	opacity: 0.25;
904	font-size: 0.8864em;
905	color: var(--muted);
906}
907
908/* Pane border - default transparent */
909.pane-border {
910	border: 0.0625em solid transparent;
911}
912
913/* Active pane - accent border */
914.pane-active {
915	border: 0.0625em solid var(--accent);
916}
917
918/* Hide active border when only one pane exists */
919.single-pane .pane-active {
920	border-color: transparent;
921}
922
923/* Mode frame: a click-through overlay drawn above web content. */
924.mode-frame-border {
925	background-color: transparent;
926	border-radius: 0;
927}
928
929.mode-frame-border.pane-mode-active {
930	box-shadow: inset 0 0 0 0.25em var(--pane-mode-color);
931}
932
933.mode-frame-border.tab-mode-active {
934	box-shadow: inset 0 0 0 0.25em var(--tab-mode-color);
935}
936
937.mode-frame-border.resize-mode-active {
938	box-shadow: inset 0 0 0 0.25em var(--resize-mode-color);
939}
940`
941}
942
943// generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
944// Text uses scalable units while vertical chrome stays compact in device pixels.
945func generateStackedPaneCSS(p Palette) string {
946	return `/* ===== Stacked Pane Styling ===== */
947
948/* Title bar for inactive panes in a stack */
949/* The title bar is now a Box with GestureClick, not wrapped in a button */
950.stacked-pane-titlebar {
951	background-color: var(--surface-variant);
952	color: var(--control-text);
953	border-bottom: 0.0625em solid var(--border);
954	padding: 4px 0.5em;
955	min-height: 24px;
956}
957
958/* Keep the background stable so --control-text retains its contrast.
959 * Indicate hover with an underline and the active pane with an accent border. */
960.stacked-pane-titlebar.active {
961	border-left: 0.1875em solid var(--accent);
962}
963
964/* Favicon image in title bar */
965.stacked-pane-titlebar image {
966	color: var(--control-text);
967	min-width: 1em;
968	min-height: 1em;
969	margin-right: 0.375em;
970}
971
972/* Title text in title bar */
973.stacked-pane-titlebar label {
974	color: var(--control-text);
975	font-size: 0.75em;
976	font-weight: 400;
977}
978
979/* Hover effect on title bar label */
980.stacked-pane-titlebar.stacked-pane-title-clickable:hover label {
981	text-decoration-line: underline;
982}
983
984/* Close button in stacked pane title bar.
985 * Its vertical footprint is intentionally smaller than the original scale-1 baseline. */
986.stacked-pane-close-button {
987	background: transparent;
988	border: none;
989	border-radius: 0.2727em;
990	padding: 3px;
991	margin: 0 2px;
992	min-width: 1.3636em;
993	min-height: 15px;
994	opacity: 0.4;
995	transition: opacity 100ms ease-in-out, background-color 100ms ease-in-out;
996}
997
998.stacked-pane-close-button > image {
999	margin: 0;
1000	padding: 0;
1001}
1002
1003.stacked-pane-close-button:hover {
1004	opacity: 1.0;
1005	background: alpha(var(--destructive), 0.2);
1006}
1007
1008.stacked-pane-close-button:hover > image {
1009	color: var(--destructive);
1010}
1011
1012/* Show close button more prominently when hovering title bar */
1013.stacked-pane-titlebar.stacked-pane-title-clickable:hover .stacked-pane-close-button {
1014	opacity: 0.7;
1015}
1016
1017.stacked-pane-close-button:focus {
1018	outline: none;
1019	box-shadow: none;
1020}
1021`
1022}
1023
1024// generateProgressBarCSS creates progress bar styles for page loading indication.
1025func generateProgressBarCSS(p Palette) string {
1026	return `/* ===== Progress Bar Styling ===== */
1027
1028/* Native GtkProgressBar with osd class */
1029progressbar.osd {
1030	min-height: 0.2727em;
1031}
1032
1033progressbar.osd trough {
1034	min-height: 0.2727em;
1035	min-width: 0.1364em;
1036	margin: 0;
1037	padding: 0;
1038	background-color: alpha(var(--bg), 0.3);
1039}
1040
1041progressbar.osd progress {
1042	min-height: 0.2727em;
1043	min-width: 0.1364em;
1044	margin: 0;
1045	padding: 0;
1046	background-color: var(--accent);
1047}
1048`
1049}
1050
1051func generateTouchpadNavigationIndicatorCSS(p Palette) string {
1052	_ = p
1053	return `/* ===== Touchpad Navigation Indicator Styling ===== */
1054
1055.touchpad-navigation-indicator {
1056	background-color: alpha(var(--surface), 0.90);
1057	color: var(--text);
1058	border: 0.0625em solid alpha(var(--border), 0.72);
1059	border-radius: 1.125em;
1060	padding: 0.75em 0.875em 0.8125em;
1061	opacity: 1;
1062	filter: blur(0px);
1063	backdrop-filter: blur(0.75em);
1064	box-shadow: 0 1em 2.25em alpha(black, 0.30),
1065	            0 0.0625em 0 alpha(white, 0.06) inset;
1066	transition: opacity 220ms ease-out,
1067	            filter 220ms ease-out,
1068	            backdrop-filter 220ms ease-out,
1069	            background-color 140ms ease-out,
1070	            border-color 140ms ease-out,
1071	            box-shadow 220ms ease-out;
1072}
1073
1074.touchpad-navigation-indicator.back {
1075	border-left: 0.1875em solid alpha(var(--accent), 0.82);
1076}
1077
1078.touchpad-navigation-indicator.forward {
1079	border-right: 0.1875em solid alpha(var(--accent), 0.82);
1080}
1081
1082.touchpad-navigation-indicator.threshold-reached {
1083	border-color: alpha(var(--accent), 0.95);
1084	background-color: alpha(var(--accent), 0.18);
1085	box-shadow: 0 1.125em 2.5em alpha(black, 0.34),
1086	            0 0 0 0.0625em alpha(var(--accent), 0.24) inset;
1087}
1088
1089.touchpad-navigation-indicator.hiding {
1090	opacity: 0;
1091	filter: blur(0.0625em);
1092	box-shadow: 0 0.375em 0.875em alpha(black, 0.10),
1093	            0 0 0 0.0625em alpha(var(--accent), 0.04) inset;
1094}
1095
1096.touchpad-navigation-header {
1097	min-width: 9.75em;
1098}
1099
1100.touchpad-navigation-icon {
1101	background-color: alpha(var(--accent), 0.16);
1102	color: var(--accent);
1103	border-radius: 999px;
1104	font-size: 0.95em;
1105	font-weight: 700;
1106	padding: 0.125em 0.375em;
1107}
1108
1109.touchpad-navigation-indicator.threshold-reached .touchpad-navigation-icon {
1110	background-color: var(--accent);
1111	color: var(--bg);
1112}
1113
1114.touchpad-navigation-label {
1115	color: var(--text);
1116	font-weight: 700;
1117	font-size: 0.92em;
1118	letter-spacing: 0.01em;
1119}
1120
1121progressbar.touchpad-navigation-progress {
1122	min-height: 0.3125em;
1123}
1124
1125progressbar.touchpad-navigation-progress trough {
1126	min-height: 0.3125em;
1127	min-width: 9.75em;
1128	border-radius: 999px;
1129	background-color: alpha(var(--muted), 0.24);
1130	box-shadow: 0 0.0625em 0 alpha(white, 0.06) inset;
1131}
1132
1133progressbar.touchpad-navigation-progress progress {
1134	min-height: 0.3125em;
1135	border-radius: 999px;
1136	background-color: var(--accent);
1137	box-shadow: 0 0 0.35em alpha(var(--accent), 0.35);
1138}
1139`
1140}
1141
1142// generateSessionManagerCSS creates session manager modal styles.
1143// Uses em units for scalable UI, matches omnibox styling patterns.
1144func generateSessionManagerCSS(p Palette) string {
1145	_ = p
1146	return `/* ===== Session Manager Styling ===== */
1147
1148/* Session manager outer container - for positioning in overlay */
1149.session-manager-outer {
1150	/* Positioning is handled via SetHalign/SetValign in Go */
1151}
1152
1153/* Session manager main container - the visible popup */
1154.session-manager-container {
1155	background-color: var(--surface-variant);
1156	border: 0.0625em solid var(--border);
1157	border-radius: 0.1875em;
1158	padding: 0;
1159	/* Width is owned by the Go sizing request so CSS must not force
1160	   growth beyond the percentage viewport limit. */
1161	/* Note: max-width not supported in GTK4 CSS, rely on container constraints */
1162}
1163
1164/* Header with title */
1165.session-manager-header {
1166	background-color: shade(var(--surface-variant), 1.1);
1167	border-bottom: 0.0625em solid var(--border);
1168	padding: 0.5em 0.75em;
1169}
1170
1171.session-manager-title {
1172	font-size: 0.9375em;
1173	font-weight: 600;
1174	color: var(--text);
1175}
1176
1177/* Scrolled window for session list */
1178.session-manager-scrolled {
1179	background-color: shade(var(--surface-variant), 0.95);
1180}
1181
1182/* List box */
1183.session-manager-list {
1184	background-color: transparent;
1185}
1186
1187/* Session rows */
1188.session-manager-row {
1189	padding: 0.5em 0.75em;
1190	margin: 0;
1191	border-radius: 0;
1192	border-left: 0.1875em solid transparent;
1193	border-bottom: 0.0625em solid alpha(var(--border), 0.5);
1194	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
1195	min-height: 2.75em;
1196}
1197
1198.session-manager-row:last-child {
1199	border-bottom: none;
1200}
1201
1202.session-manager-row:hover {
1203	background-color: alpha(var(--accent), 0.12);
1204	border-left: 0.1875em solid var(--accent);
1205}
1206
1207.session-manager-row:selected,
1208row:selected .session-manager-row {
1209	background-color: alpha(var(--accent), 0.2);
1210	border-left: 0.1875em solid var(--accent);
1211}
1212
1213/* Status indicator dot */
1214.session-status {
1215	font-size: 0.875em;
1216	min-width: 1em;
1217	margin-right: 0.5em;
1218}
1219
1220/* Current session - accent color dot */
1221.session-current .session-status {
1222	color: var(--accent);
1223}
1224
1225/* Active session (other instance) - muted dot */
1226.session-active .session-status {
1227	color: var(--muted);
1228}
1229
1230/* Exited session - dimmed appearance with subtle stop icon */
1231.session-exited {
1232	opacity: 0.75;
1233}
1234
1235.session-exited .session-status {
1236	color: var(--error);
1237	opacity: 0.5;
1238}
1239
1240/* Session ID labels */
1241.session-id-short {
1242	font-size: 0.875em;
1243	font-weight: 600;
1244	color: var(--accent);
1245	font-family: var(--font-mono);
1246}
1247
1248.session-id-prefix {
1249	font-size: 0.875em;
1250	font-weight: 400;
1251	color: var(--muted);
1252	font-family: var(--font-mono);
1253}
1254
1255.session-id-badge {
1256	font-size: 0.75em;
1257	font-weight: 500;
1258	color: var(--accent);
1259	margin-left: 0.5em;
1260}
1261
1262/* Tab/pane count label */
1263.session-count {
1264	font-size: 0.75em;
1265	color: var(--muted);
1266}
1267
1268/* Relative time label */
1269.session-time {
1270	font-size: 0.75em;
1271	color: var(--muted);
1272	/* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
1273}
1274
1275/* Section divider for EXITED sessions */
1276.session-divider {
1277	font-size: 0.6875em;
1278	font-weight: 600;
1279	color: var(--muted);
1280	text-transform: uppercase;
1281	letter-spacing: 0.05em;
1282	padding: 0.5em 0.75em 0.25em 0.75em;
1283	background-color: alpha(var(--border), 0.3);
1284	border-bottom: 0.0625em solid var(--border);
1285}
1286
1287/* Footer with keyboard shortcuts */
1288.session-manager-footer {
1289	background-color: shade(var(--surface-variant), 0.9);
1290	border-top: 0.0625em solid var(--border);
1291	padding: 0.375em 0.75em;
1292	font-size: 0.6875em;
1293	color: var(--muted);
1294	font-family: var(--font-mono);
1295}
1296
1297/* Session mode border */
1298.mode-frame-border.session-mode-active {
1299	box-shadow: inset 0 0 0 0.25em var(--session-mode-color);
1300}
1301`
1302}
1303
1304// generateContextMenuCSS creates shared context menu styles.
1305func generateContextMenuCSS() string {
1306	return `/* ===== Context Menu Styling ===== */
1307
1308popover.context-menu-popover {
1309	background-color: transparent;
1310	background-image: none;
1311}
1312
1313popover.context-menu-popover > contents {
1314	background-color: var(--surface);
1315	background-image: none;
1316	border: 0.0625em solid alpha(var(--border), 0.85);
1317	border-radius: 0.25em;
1318	box-shadow: 0 0.75em 1.75em alpha(var(--bg), 0.28), 0 0.0625em 0 alpha(var(--border), 0.18) inset;
1319	padding: 0;
1320}
1321
1322.context-menu {
1323	padding: 0.125em 0;
1324	min-width: 10.5em;
1325}
1326
1327.context-menu button.context-menu-item {
1328	background-color: transparent;
1329	background-image: none;
1330	border: none;
1331	border-radius: 0;
1332	color: var(--text);
1333	padding: 0.25em 0.75em;
1334	margin: 0;
1335	min-height: 1.75em;
1336	transition: background-color 120ms ease-in-out, color 120ms ease-in-out;
1337}
1338
1339.context-menu button.context-menu-item:hover,
1340.context-menu button.context-menu-item:focus,
1341.context-menu button.context-menu-item:active {
1342	background-color: alpha(var(--surface-variant), 0.9);
1343	color: var(--text);
1344}
1345
1346.context-menu button.context-menu-item:active {
1347	background-color: alpha(var(--accent), 0.16);
1348}
1349
1350.context-menu button.context-menu-item:disabled {
1351	color: var(--muted);
1352	opacity: 0.7;
1353}
1354
1355.context-menu-separator {
1356	background-color: alpha(var(--border), 0.55);
1357	margin: 0.25em 0.5em;
1358	min-height: 0.0625em;
1359}
1360`
1361}
1362
1363// generateToasterCSS creates toast notification styles.
1364// Uses em units for scalable UI.
1365func generateToasterCSS(p Palette) string {
1366	return `/* ===== Toaster Styling ===== */
1367
1368/* Toast notification container */
1369.toast {
1370	background-color: var(--surface-variant);
1371	border-radius: 0.25em;
1372	padding: 0.375em 0.625em;
1373	margin: 0.5em;
1374	font-size: 0.875em;
1375	font-weight: 600;
1376	box-shadow: 0 2px 8px alpha(black, 0.3);
1377	min-width: 2.5em;
1378}
1379
1380/* Toast level: info (default, accent color) */
1381.toast-info {
1382	background-color: alpha(var(--accent), 0.9);
1383	color: var(--accent-text);
1384}
1385
1386/* Toast level: success */
1387.toast-success {
1388	background-color: alpha(var(--success), 0.9);
1389	color: var(--success-text);
1390}
1391
1392/* Toast level: warning */
1393.toast-warning {
1394	background-color: alpha(var(--warning), 0.9);
1395	color: var(--warning-text);
1396}
1397
1398/* Toast level: error */
1399.toast-error {
1400	background-color: alpha(var(--destructive), 0.9);
1401	color: var(--destructive-text);
1402}
1403
1404/* Toast with custom styling (mode indicator toasters) */
1405.toast-custom {
1406	color: #ffffff;
1407}
1408
1409/* Mode-specific toast colors */
1410.toast-pane-mode {
1411	background-color: var(--pane-mode-color);
1412	color: var(--pane-mode-text);
1413}
1414
1415.toast-vim-mode {
1416	background-color: var(--vim-mode-color);
1417	color: var(--vim-mode-text);
1418}
1419
1420.toast-tab-mode {
1421	background-color: var(--tab-mode-color);
1422	color: var(--tab-mode-text);
1423}
1424
1425.toast-session-mode {
1426	background-color: var(--session-mode-color);
1427	color: var(--session-mode-text);
1428}
1429
1430.toast-resize-mode {
1431	background-color: var(--resize-mode-color);
1432	color: var(--resize-mode-text);
1433}
1434`
1435}
1436
1437// generateLinkStatusCSS creates link status overlay styles.
1438// Uses CSS transitions for fade-in/fade-out effect.
1439func generateLinkStatusCSS(p Palette) string {
1440	return `/* ===== Link Status Overlay Styling ===== */
1441
1442/* Link status container - bottom left positioning */
1443.link-status {
1444	background-color: alpha(var(--surface-variant), 0.95);
1445	border-radius: 0.25em 0.25em 0 0;
1446	padding: 0.25em 0.5em;
1447	margin: 0;
1448	font-size: 0.75em;
1449	/* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */
1450	box-shadow: 0 -1px 4px alpha(black, 0.15);
1451
1452	/* Fade transition - hidden by default */
1453	opacity: 0;
1454	transition: opacity 150ms ease-in-out;
1455}
1456
1457/* Visible state - triggered by adding .visible class */
1458.link-status.visible {
1459	opacity: 1;
1460}
1461
1462/* Link status label text */
1463.link-status label {
1464	color: var(--control-muted);
1465}
1466`
1467}
1468
1469func generateFloatingPaneCSS(p Palette) string {
1470	_ = p
1471	return `/* ===== Floating Pane Styling ===== */
1472.floating-pane-outer {
1473	/* Positioning handled in Go via overlay alignment and margins. */
1474}
1475
1476.floating-pane-container {
1477	background-color: var(--surface-variant);
1478	border: 0.0625em solid var(--border);
1479	border-radius: 0.25em;
1480	box-shadow: 0 0.5em 1.5em alpha(black, 0.25);
1481	opacity: 0;
1482	transition: opacity 110ms ease-out;
1483}
1484
1485.floating-pane-container.floating-pane-visible {
1486	opacity: 1;
1487}
1488
1489.floating-pane-container.pane-active {
1490	border-color: var(--accent);
1491}
1492
1493.floating-pane-omnibox {
1494	background-color: shade(var(--surface-variant), 1.08);
1495	border-bottom: 0.0625em solid var(--border);
1496	padding: 0.375em 0.5em;
1497}
1498
1499.floating-pane-content {
1500	background-color: var(--bg);
1501	min-height: 12em;
1502}
1503`
1504}

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