v0.25.0 2e6c485

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

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