v0.22.0 a85da87

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 font sizes and widget padding/margins proportionally.
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}
25
26// DefaultFontConfig returns safe font fallbacks.
27func DefaultFontConfig() FontConfig {
28	return FontConfig{
29		SansFont:      "sans-serif",
30		MonospaceFont: "monospace",
31	}
32}
33
34// FontCSSVars generates CSS custom properties for fonts.
35func FontCSSVars(fonts FontConfig) string {
36	var sb strings.Builder
37	// Quote the configured family; include generic fallback.
38	sb.WriteString(fmt.Sprintf("  --font-sans: %q, sans-serif;\n", fonts.SansFont))
39	sb.WriteString(fmt.Sprintf("  --font-mono: %q, monospace;\n", fonts.MonospaceFont))
40	return sb.String()
41}
42
43func generateFontCSS() string {
44	return `/* Font styling */
45window, tooltip, popover {
46	font-family: var(--font-sans);
47}
48`
49}
50
51// GenerateCSSWithScaleAndFonts creates GTK4 CSS using the provided palette, UI scale factor and fonts.
52// Scale affects font sizes and widget padding/margins proportionally.
53// Uses default mode colors.
54func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string {
55	return GenerateCSSFull(p, scale, fonts, DefaultModeColors())
56}
57
58// GenerateCSSFull creates GTK4 CSS using all provided configuration.
59// Scale affects font sizes and widget padding/margins proportionally.
60func GenerateCSSFull(p Palette, scale float64, fonts FontConfig, modeColors ModeColors) string {
61	if scale <= 0 {
62		scale = 1.0
63	}
64
65	defaults := DefaultFontConfig()
66	fonts = FontConfig{
67		SansFont:      Coalesce(fonts.SansFont, defaults.SansFont),
68		MonospaceFont: Coalesce(fonts.MonospaceFont, defaults.MonospaceFont),
69	}
70
71	var sb strings.Builder
72
73	// CSS custom properties (variables) - GTK4 uses :root selector
74	sb.WriteString("/* Theme variables */\n")
75	sb.WriteString(":root {\n")
76	sb.WriteString(p.ToCSSVars())
77	sb.WriteString(FontCSSVars(fonts))
78	sb.WriteString(modeColors.ToCSSVars())
79	sb.WriteString("}\n\n")
80
81	// Global font styling
82	sb.WriteString(generateFontCSS())
83	sb.WriteString("\n")
84
85	// Global UI scaling via font-size on all widgets
86	if scale != 1.0 {
87		sb.WriteString(generateScalingCSS(scale))
88		sb.WriteString("\n")
89	}
90
91	// Tab bar styling
92	sb.WriteString(generateTabBarCSS(p))
93	sb.WriteString("\n")
94
95	// Omnibox styling
96	sb.WriteString(generateOmniboxCSS(p))
97	sb.WriteString("\n")
98
99	// Find bar styling
100	sb.WriteString(generateFindBarCSS(p))
101	sb.WriteString("\n")
102
103	// Pane styling
104	sb.WriteString(generatePaneCSS(p))
105	sb.WriteString("\n")
106
107	// Stacked pane styling
108	sb.WriteString(generateStackedPaneCSS(p))
109	sb.WriteString("\n")
110
111	// Progress bar styling
112	sb.WriteString(generateProgressBarCSS(p))
113	sb.WriteString("\n")
114
115	// Toaster styling
116	sb.WriteString(generateToasterCSS(p))
117	sb.WriteString("\n")
118
119	// Link status overlay styling
120	sb.WriteString(generateLinkStatusCSS(p))
121	sb.WriteString("\n")
122
123	// Session manager styling
124	sb.WriteString(generateSessionManagerCSS(p))
125	sb.WriteString("\n")
126
127	// Tab picker styling
128	sb.WriteString(generateTabPickerCSS(p))
129
130	return sb.String()
131}
132
133// generateScalingCSS creates CSS rules that scale the UI.
134// GTK4 CSS doesn't support transform:scale() well, so we scale font-size
135// and use relative units (em) for padding/margins in other rules.
136func generateScalingCSS(scale float64) string {
137	// Use 16px base so em conversions are correct:
138	// 1px = 0.0625em (1/16), 4px = 0.25em, etc.
139	basePx := int(16 * scale)
140	return fmt.Sprintf(`/* UI Scaling (%.0f%%) */
141window {
142	font-size: %dpx;
143}
144`, scale*100, basePx)
145}
146
147// generateTabBarCSS creates tab bar styles.
148// Uses em units for scalable UI.
149func generateTabBarCSS(p Palette) string {
150	return `/* Tab bar styling */
151.tab-bar {
152	background-color: var(--surface);
153	border-top: 0.125em solid var(--border);
154	padding: 0;
155	min-height: 2em;
156}
157
158/* Tab button styling */
159button.tab-button {
160	background-color: var(--surface-variant);
161	background-image: none;
162	border: none;
163	border-right: 0.0625em solid var(--border);
164	border-radius: 0;
165	padding: 0.25em 0.5em;
166	transition: background-color 200ms ease-in-out;
167}
168
169button.tab-button:hover {
170	background-color: shade(var(--surface-variant), 1.2);
171}
172
173button.tab-button.tab-button-active {
174	background-color: shade(var(--surface-variant), 1.2);
175	border-bottom: 0.125em solid var(--accent);
176	font-weight: 600;
177}
178
179/* Tab title text */
180.tab-title {
181	font-size: 0.75em;
182	color: var(--text);
183	font-weight: 500;
184}
185`
186}
187
188// generateOmniboxCSS creates omnibox styles.
189// Uses em units for scalable UI.
190func generateOmniboxCSS(p Palette) string {
191	return `/* ===== Omnibox Styling ===== */
192
193/* Omnibox outer container - for positioning in overlay */
194.omnibox-outer {
195	/* Positioning is handled via SetHalign/SetValign/SetMarginTop in Go */
196}
197
198/* Omnibox main container - the visible popup */
199.omnibox-container {
200	background-color: var(--surface-variant);
201	border: 0.0625em solid var(--border);
202	border-radius: 0.1875em;
203	padding: 0;
204}
205
206/* Header with History/Favorites toggle */
207.omnibox-header {
208	background-color: shade(var(--surface-variant), 1.1);
209	border-bottom: 0.0625em solid var(--border);
210	padding: 0.375em 0.75em;
211}
212
213.omnibox-header-btn {
214	background-color: transparent;
215	background-image: none;
216	border: none;
217	border-radius: 0.125em;
218	padding: 0.25em 0.75em;
219	margin-right: 0.5em;
220	font-size: 0.8125em;
221	font-weight: 500;
222	color: var(--muted);
223	transition: all 100ms ease-in-out;
224}
225
226.omnibox-header-btn:hover {
227	background-color: alpha(var(--accent), 0.15);
228	color: var(--text);
229}
230
231.omnibox-header-btn.omnibox-header-active {
232	background-color: alpha(var(--accent), 0.2);
233	color: var(--accent);
234	font-weight: 600;
235}
236
237/* Search entry field */
238.omnibox-entry {
239	background-color: var(--bg);
240	color: var(--text);
241	border: 0.0625em solid var(--border);
242	border-radius: 0.125em;
243	padding: 0.625em 0.75em;
244	margin: 0.5em 0.75em;
245	font-size: 1em;
246	caret-color: var(--accent);
247}
248
249entry.omnibox-entry:focus,
250entry.omnibox-entry:focus-within,
251entry.omnibox-entry:focus-visible {
252	border-color: var(--accent);
253	background-color: shade(var(--bg), 1.05);
254	outline-style: none;
255	outline-width: 0px;
256	outline-color: transparent;
257}
258
259/* Override any internal focus styling */
260entry.omnibox-entry > *:focus,
261entry.omnibox-entry > *:focus-visible {
262	outline-style: none;
263	outline-color: transparent;
264}
265
266/* Scrolled window for suggestions */
267.omnibox-scrolled {
268	background-color: shade(var(--surface-variant), 0.95);
269	border-top: 0.0625em solid var(--border);
270}
271
272/* List box */
273.omnibox-listbox {
274	background-color: transparent;
275}
276
277/* Suggestion rows */
278.omnibox-row {
279	padding: 0.5em 0.75em;
280	margin: 0;
281	border-radius: 0;
282	border-left: 0.1875em solid transparent;
283	border-bottom: 0.0625em solid alpha(var(--border), 0.5);
284	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
285	min-height: 2.75em;
286}
287
288.omnibox-row:last-child {
289	border-bottom: none;
290}
291
292.omnibox-row:hover {
293	background-color: alpha(var(--accent), 0.12);
294	border-left: 0.1875em solid var(--accent);
295}
296
297.omnibox-row:selected {
298	background-color: alpha(var(--accent), 0.2);
299	border-left: 0.1875em solid var(--accent);
300}
301
302/* Favorite indicator in history mode - yellowish accent */
303.omnibox-row.omnibox-row-favorite {
304	background-color: alpha(var(--warning), 0.08);
305	border-left: 0.1875em solid var(--warning);
306}
307
308.omnibox-row.omnibox-row-favorite:hover {
309	background-color: alpha(var(--warning), 0.15);
310	border-left: 0.1875em solid var(--warning);
311}
312
313.omnibox-row.omnibox-row-favorite:selected {
314	background-color: alpha(var(--warning), 0.2);
315	border-left: 0.1875em solid var(--warning);
316}
317
318/* Favicon in omnibox rows */
319.omnibox-favicon {
320	min-width: 1em;
321	min-height: 1em;
322	margin-right: 0.5em;
323}
324
325/* Suggestion title/URL */
326.omnibox-suggestion-title {
327	font-size: 0.875em;
328	color: var(--text);
329	font-weight: 400;
330}
331
332/* Also style labels inside omnibox rows directly */
333.omnibox-row label {
334	color: var(--text);
335}
336
337.omnibox-row:selected .omnibox-suggestion-title {
338	color: var(--text);
339	font-weight: 500;
340}
341
342/* URL text below title - use .omnibox-row prefix for specificity over .omnibox-row label */
343.omnibox-row .omnibox-suggestion-url {
344	font-size: 0.75em;
345	color: alpha(var(--muted), 0.65);
346	font-weight: 400;
347}
348
349.omnibox-row:selected .omnibox-suggestion-url {
350	color: alpha(var(--muted), 0.75);
351}
352
353/* Keyboard shortcut badge */
354.omnibox-shortcut-badge {
355	background-color: alpha(var(--muted), 0.3);
356	color: var(--muted);
357	border-radius: 0.25em;
358	padding: 0.125em 0.375em;
359	font-size: 0.625em;
360	font-weight: 500;
361	font-family: var(--font-mono);
362	margin-left: 0.5em;
363}
364
365.omnibox-row:hover .omnibox-shortcut-badge {
366	background-color: alpha(var(--accent), 0.2);
367	color: var(--accent);
368}
369
370.omnibox-row:selected .omnibox-shortcut-badge {
371	background-color: alpha(var(--accent), 0.3);
372	color: var(--accent);
373}
374
375/* Bang indicator badge in omnibox header */
376.omnibox-bang-badge {
377	background-color: alpha(var(--accent), 0.18);
378	color: var(--accent);
379	border-radius: 0.25em;
380	padding: 0.125em 0.5em;
381	font-size: 0.6875em;
382	font-weight: 600;
383}
384
385/* Zoom indicator in omnibox header */
386.omnibox-zoom-indicator {
387	background-color: alpha(var(--accent), 0.2);
388	color: var(--accent);
389	border-radius: 0.25em;
390	padding: 0.125em 0.5em;
391	font-size: 0.6875em;
392	font-weight: 600;
393	/* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
394}
395
396entry.omnibox-entry.omnibox-entry-bang-active,
397entry.omnibox-entry.omnibox-entry-bang-active:focus,
398entry.omnibox-entry.omnibox-entry-bang-active:focus-within,
399entry.omnibox-entry.omnibox-entry-bang-active:focus-visible {
400	border-color: var(--accent);
401	background-color: shade(var(--bg), 1.05);
402}
403
404.omnibox-row.omnibox-row-bang .omnibox-suggestion-title {
405	color: var(--accent);
406	font-weight: 600;
407}
408`
409}
410
411// generateFindBarCSS creates find bar styles.
412// Uses em units for scalable UI.
413func generateFindBarCSS(p Palette) string {
414	_ = p
415	return `/* ===== Find Bar Styling ===== */
416.find-bar-outer {
417	margin: 0.5em;
418}
419
420.find-bar-container {
421	background-color: var(--surface-variant);
422	border: 0.0625em solid var(--border);
423	border-radius: 0.25em;
424	box-shadow: 0 0.25em 0.75em alpha(black, 0.2);
425	padding: 0.5em;
426	min-width: 20em;
427}
428
429.find-bar-input-row {
430	margin-bottom: 0.25em;
431}
432
433.find-bar-entry {
434	background-color: var(--bg);
435	color: var(--text);
436	border: 0.0625em solid var(--border);
437	border-radius: 0.1875em;
438	padding: 0.4em 0.5em;
439	caret-color: var(--accent);
440}
441
442entry.find-bar-entry:focus,
443entry.find-bar-entry:focus-within,
444entry.find-bar-entry:focus-visible {
445	border-color: var(--accent);
446	outline-style: none;
447	outline-width: 0px;
448	outline-color: transparent;
449}
450
451.find-bar-entry.not-found {
452	background-color: alpha(#ef4444, 0.15);
453	border-color: #ef4444;
454}
455
456.find-bar-count {
457	font-size: 0.8em;
458	color: var(--muted);
459	min-width: 3.5em;
460}
461
462.find-bar-count.find-bar-count-has {
463	color: var(--accent);
464}
465
466.find-bar-nav,
467.find-bar-close,
468.find-bar-toggle {
469	background-image: none;
470	background-color: alpha(var(--bg), 0.6);
471	border: 0.0625em solid var(--border);
472	border-radius: 0.1875em;
473	padding: 0.25em 0.5em;
474	transition: background-color 120ms ease-in-out, border-color 120ms ease-in-out;
475}
476
477.find-bar-nav:hover,
478.find-bar-close:hover,
479.find-bar-toggle:hover {
480	background-color: alpha(var(--accent), 0.15);
481}
482
483.find-bar-toggle:checked {
484	background-color: alpha(var(--accent), 0.2);
485	border-color: var(--accent);
486	color: var(--accent);
487}
488`
489}
490
491// generatePaneCSS creates pane border styles.
492// Uses em units for scalable UI.
493func generatePaneCSS(p Palette) string {
494	return `/* ===== Pane Styling ===== */
495
496/* Pane overlay container - theme background prevents white flash */
497.pane-overlay {
498	background-color: var(--bg);
499}
500
501/* WebView widget - theme background prevents white flash during page load */
502.webview-themed {
503	background-color: var(--bg);
504}
505
506/* Loading skeleton - shown until WebView paints */
507.loading-skeleton {
508	background-color: var(--bg);
509}
510
511.loading-skeleton-spinner {
512	-gtk-icon-size: 32px;
513	min-width: 32px;
514	min-height: 32px;
515	color: var(--muted);
516}
517
518.loading-skeleton-logo {
519	opacity: 0.08;
520	min-width: 512px;
521	min-height: 512px;
522}
523
524/* Pane border - default transparent */
525.pane-border {
526	border: 0.0625em solid transparent;
527}
528
529/* Active pane - accent border */
530.pane-active {
531	border: 0.0625em solid var(--accent);
532}
533
534/* Hide active border when only one pane exists */
535.single-pane .pane-active {
536	border-color: transparent;
537}
538
539/* Pane mode active - thick inset border (for overlay) */
540.pane-mode-active {
541	background-color: transparent;
542	box-shadow: inset 0 0 0 0.25em var(--pane-mode-color);
543	border-radius: 0;
544}
545
546/* Tab mode active - thick inset border (for overlay) */
547.tab-mode-active {
548	background-color: transparent;
549	box-shadow: inset 0 0 0 0.25em var(--tab-mode-color);
550	border-radius: 0;
551}
552
553/* Resize mode active - thick inset border */
554.resize-mode-active {
555	background-color: transparent;
556	/* Prefer inset shadow, but also set outline for widgets that don't paint shadows */
557	box-shadow: inset 0 0 0 0.25em var(--resize-mode-color);
558	outline: 0.25em solid var(--resize-mode-color);
559	outline-offset: -0.25em;
560	border-radius: 0;
561}
562`
563}
564
565// generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
566// Uses em units for scalable UI.
567func generateStackedPaneCSS(p Palette) string {
568	return `/* ===== Stacked Pane Styling ===== */
569
570/* Title bar for inactive panes in a stack */
571.stacked-pane-titlebar {
572	background-color: var(--surface-variant);
573	border-bottom: 0.0625em solid var(--border);
574	padding: 0.25em 0.5em;
575	min-height: 1.5em;
576}
577
578/* Title bar button wrapper - clickable area */
579button.stacked-pane-title-button {
580	background-color: var(--surface-variant);
581	background-image: none;
582	border: none;
583	border-bottom: 0.0625em solid var(--border);
584	border-radius: 0;
585	padding: 0;
586	margin: 0;
587	transition: background-color 150ms ease-in-out;
588}
589
590button.stacked-pane-title-button:hover {
591	background-color: shade(var(--surface-variant), 1.15);
592}
593
594button.stacked-pane-title-button:focus {
595	outline: none;
596	box-shadow: none;
597}
598
599/* Title bar content box */
600.stacked-pane-titlebar.active {
601	background-color: shade(var(--surface-variant), 1.2);
602	border-left: 0.1875em solid var(--accent);
603}
604
605/* Favicon image in title bar */
606.stacked-pane-titlebar image {
607	min-width: 1em;
608	min-height: 1em;
609	margin-right: 0.375em;
610}
611
612/* Title text in title bar */
613.stacked-pane-titlebar label {
614	color: var(--text);
615	font-size: 0.75em;
616	font-weight: 400;
617}
618
619button.stacked-pane-title-button:hover .stacked-pane-titlebar label {
620	color: var(--accent);
621}
622`
623}
624
625// generateProgressBarCSS creates progress bar styles for page loading indication.
626func generateProgressBarCSS(p Palette) string {
627	return `/* ===== Progress Bar Styling ===== */
628
629/* Native GtkProgressBar with osd class */
630progressbar.osd {
631	min-height: 4px;
632}
633
634progressbar.osd trough {
635	min-height: 4px;
636	min-width: 2px;
637	margin: 0;
638	padding: 0;
639	background-color: alpha(var(--bg), 0.3);
640}
641
642progressbar.osd progress {
643	min-height: 4px;
644	min-width: 2px;
645	margin: 0;
646	padding: 0;
647	background-color: var(--accent);
648}
649`
650}
651
652// generateSessionManagerCSS creates session manager modal styles.
653// Uses em units for scalable UI, matches omnibox styling patterns.
654func generateSessionManagerCSS(p Palette) string {
655	_ = p
656	return `/* ===== Session Manager Styling ===== */
657
658/* Session manager outer container - for positioning in overlay */
659.session-manager-outer {
660	/* Positioning is handled via SetHalign/SetValign in Go */
661}
662
663/* Session manager main container - the visible popup */
664.session-manager-container {
665	background-color: var(--surface-variant);
666	border: 0.0625em solid var(--border);
667	border-radius: 0.1875em;
668	padding: 0;
669	min-width: 28em;
670	/* Note: max-width not supported in GTK4 CSS, rely on container constraints */
671}
672
673/* Header with title */
674.session-manager-header {
675	background-color: shade(var(--surface-variant), 1.1);
676	border-bottom: 0.0625em solid var(--border);
677	padding: 0.5em 0.75em;
678}
679
680.session-manager-title {
681	font-size: 0.9375em;
682	font-weight: 600;
683	color: var(--text);
684}
685
686/* Scrolled window for session list */
687.session-manager-scrolled {
688	background-color: shade(var(--surface-variant), 0.95);
689}
690
691/* List box */
692.session-manager-list {
693	background-color: transparent;
694}
695
696/* Session rows */
697.session-manager-row {
698	padding: 0.5em 0.75em;
699	margin: 0;
700	border-radius: 0;
701	border-left: 0.1875em solid transparent;
702	border-bottom: 0.0625em solid alpha(var(--border), 0.5);
703	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
704	min-height: 2.75em;
705}
706
707.session-manager-row:last-child {
708	border-bottom: none;
709}
710
711.session-manager-row:hover {
712	background-color: alpha(var(--accent), 0.12);
713	border-left: 0.1875em solid var(--accent);
714}
715
716.session-manager-row:selected,
717row:selected .session-manager-row {
718	background-color: alpha(var(--accent), 0.2);
719	border-left: 0.1875em solid var(--accent);
720}
721
722/* Status indicator dot */
723.session-status {
724	font-size: 0.875em;
725	min-width: 1em;
726	margin-right: 0.5em;
727}
728
729/* Current session - accent color dot */
730.session-current .session-status {
731	color: var(--accent);
732}
733
734/* Active session (other instance) - muted dot */
735.session-active .session-status {
736	color: var(--muted);
737}
738
739/* Exited session - dimmed appearance with subtle stop icon */
740.session-exited {
741	opacity: 0.75;
742}
743
744.session-exited .session-status {
745	color: var(--error);
746	opacity: 0.5;
747}
748
749/* Session ID labels */
750.session-id-short {
751	font-size: 0.875em;
752	font-weight: 600;
753	color: var(--accent);
754	font-family: var(--font-mono);
755}
756
757.session-id-prefix {
758	font-size: 0.875em;
759	font-weight: 400;
760	color: var(--muted);
761	font-family: var(--font-mono);
762}
763
764.session-id-badge {
765	font-size: 0.75em;
766	font-weight: 500;
767	color: var(--accent);
768	margin-left: 0.5em;
769}
770
771/* Tab/pane count label */
772.session-count {
773	font-size: 0.75em;
774	color: var(--muted);
775}
776
777/* Relative time label */
778.session-time {
779	font-size: 0.75em;
780	color: var(--muted);
781	/* Note: margin-left: auto not supported in GTK4 CSS, use SetHalign(End) in code */
782}
783
784/* Section divider for EXITED sessions */
785.session-divider {
786	font-size: 0.6875em;
787	font-weight: 600;
788	color: var(--muted);
789	text-transform: uppercase;
790	letter-spacing: 0.05em;
791	padding: 0.5em 0.75em 0.25em 0.75em;
792	background-color: alpha(var(--border), 0.3);
793	border-bottom: 0.0625em solid var(--border);
794}
795
796/* Footer with keyboard shortcuts */
797.session-manager-footer {
798	background-color: shade(var(--surface-variant), 0.9);
799	border-top: 0.0625em solid var(--border);
800	padding: 0.375em 0.75em;
801	font-size: 0.6875em;
802	color: var(--muted);
803	font-family: var(--font-mono);
804}
805
806/* Session mode border */
807.session-mode-active {
808	background-color: transparent;
809	box-shadow: inset 0 0 0 0.25em var(--session-mode-color);
810	border-radius: 0;
811}
812`
813}
814
815// generateToasterCSS creates toast notification styles.
816// Uses em units for scalable UI.
817func generateToasterCSS(p Palette) string {
818	return `/* ===== Toaster Styling ===== */
819
820/* Toast notification container */
821.toast {
822	background-color: var(--surface-variant);
823	border-radius: 0.25em;
824	padding: 0.375em 0.625em;
825	margin: 0.5em;
826	font-size: 0.875em;
827	font-weight: 600;
828	box-shadow: 0 2px 8px alpha(black, 0.3);
829	min-width: 2.5em;
830}
831
832/* Toast level: info (default, accent color) */
833.toast-info {
834	background-color: alpha(var(--accent), 0.9);
835	color: var(--bg);
836}
837
838/* Toast level: success */
839.toast-success {
840	background-color: alpha(var(--success), 0.9);
841	color: var(--bg);
842}
843
844/* Toast level: warning */
845.toast-warning {
846	background-color: alpha(var(--warning), 0.9);
847	color: var(--bg);
848}
849
850/* Toast level: error */
851.toast-error {
852	background-color: alpha(var(--destructive), 0.9);
853	color: var(--bg);
854}
855
856/* Toast with custom styling (mode indicator toasters) */
857.toast-custom {
858	color: #ffffff;
859}
860
861/* Mode-specific toast colors */
862.toast-pane-mode {
863	background-color: var(--pane-mode-color);
864	color: #ffffff;
865}
866
867.toast-tab-mode {
868	background-color: var(--tab-mode-color);
869	color: #ffffff;
870}
871
872.toast-session-mode {
873	background-color: var(--session-mode-color);
874	color: #ffffff;
875}
876
877.toast-resize-mode {
878	background-color: var(--resize-mode-color);
879	color: #ffffff;
880}
881`
882}
883
884// generateLinkStatusCSS creates link status overlay styles.
885// Uses CSS transitions for fade-in/fade-out effect.
886func generateLinkStatusCSS(p Palette) string {
887	return `/* ===== Link Status Overlay Styling ===== */
888
889/* Link status container - bottom left positioning */
890.link-status {
891	background-color: alpha(var(--surface-variant), 0.95);
892	border-radius: 0.25em 0.25em 0 0;
893	padding: 0.25em 0.5em;
894	margin: 0;
895	font-size: 0.75em;
896	/* Note: max-width not supported in GTK4 CSS, use label SetMaxWidthChars instead */
897	box-shadow: 0 -1px 4px alpha(black, 0.15);
898
899	/* Fade transition - hidden by default */
900	opacity: 0;
901	transition: opacity 150ms ease-in-out;
902}
903
904/* Visible state - triggered by adding .visible class */
905.link-status.visible {
906	opacity: 1;
907}
908
909/* Link status label text */
910.link-status label {
911	color: var(--muted);
912}
913`
914}

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