v0.20.0 c741a39

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.
53func GenerateCSSWithScaleAndFonts(p Palette, scale float64, fonts FontConfig) string {
54	if scale <= 0 {
55		scale = 1.0
56	}
57
58	defaults := DefaultFontConfig()
59	fonts = FontConfig{
60		SansFont:      Coalesce(fonts.SansFont, defaults.SansFont),
61		MonospaceFont: Coalesce(fonts.MonospaceFont, defaults.MonospaceFont),
62	}
63
64	var sb strings.Builder
65
66	// CSS custom properties (variables) - GTK4 uses :root selector
67	sb.WriteString("/* Theme variables */\n")
68	sb.WriteString(":root {\n")
69	sb.WriteString(p.ToCSSVars())
70	sb.WriteString(FontCSSVars(fonts))
71	sb.WriteString("}\n\n")
72
73	// Global font styling
74	sb.WriteString(generateFontCSS())
75	sb.WriteString("\n")
76
77	// Global UI scaling via font-size on all widgets
78	if scale != 1.0 {
79		sb.WriteString(generateScalingCSS(scale))
80		sb.WriteString("\n")
81	}
82
83	// Tab bar styling
84	sb.WriteString(generateTabBarCSS(p))
85	sb.WriteString("\n")
86
87	// Omnibox styling
88	sb.WriteString(generateOmniboxCSS(p))
89	sb.WriteString("\n")
90
91	// Find bar styling
92	sb.WriteString(generateFindBarCSS(p))
93	sb.WriteString("\n")
94
95	// Pane styling
96	sb.WriteString(generatePaneCSS(p))
97	sb.WriteString("\n")
98
99	// Stacked pane styling
100	sb.WriteString(generateStackedPaneCSS(p))
101	sb.WriteString("\n")
102
103	// Progress bar styling
104	sb.WriteString(generateProgressBarCSS(p))
105	sb.WriteString("\n")
106
107	// Toaster styling
108	sb.WriteString(generateToasterCSS(p))
109
110	return sb.String()
111}
112
113// generateScalingCSS creates CSS rules that scale the UI.
114// GTK4 CSS doesn't support transform:scale() well, so we scale font-size
115// and use relative units (em) for padding/margins in other rules.
116func generateScalingCSS(scale float64) string {
117	// Use 16px base so em conversions are correct:
118	// 1px = 0.0625em (1/16), 4px = 0.25em, etc.
119	basePx := int(16 * scale)
120	return fmt.Sprintf(`/* UI Scaling (%.0f%%) */
121window {
122	font-size: %dpx;
123}
124`, scale*100, basePx)
125}
126
127// generateTabBarCSS creates tab bar styles.
128// Uses em units for scalable UI.
129func generateTabBarCSS(p Palette) string {
130	return `/* Tab bar styling */
131.tab-bar {
132	background-color: var(--surface);
133	border-top: 0.125em solid var(--border);
134	padding: 0;
135	min-height: 2em;
136}
137
138/* Tab button styling */
139button.tab-button {
140	background-color: var(--surface-variant);
141	background-image: none;
142	border: none;
143	border-right: 0.0625em solid var(--border);
144	border-radius: 0;
145	padding: 0.25em 0.5em;
146	transition: background-color 200ms ease-in-out;
147}
148
149button.tab-button:hover {
150	background-color: shade(var(--surface-variant), 1.2);
151}
152
153button.tab-button.tab-button-active {
154	background-color: shade(var(--surface-variant), 1.2);
155	border-bottom: 0.125em solid var(--accent);
156	font-weight: 600;
157}
158
159/* Tab title text */
160.tab-title {
161	font-size: 0.75em;
162	color: var(--text);
163	font-weight: 500;
164}
165`
166}
167
168// generateOmniboxCSS creates omnibox styles.
169// Uses em units for scalable UI.
170func generateOmniboxCSS(p Palette) string {
171	return `/* ===== Omnibox Styling ===== */
172
173/* Omnibox outer container - for positioning in overlay */
174.omnibox-outer {
175	/* Positioning is handled via SetHalign/SetValign/SetMarginTop in Go */
176}
177
178/* Omnibox main container - the visible popup */
179.omnibox-container {
180	background-color: var(--surface-variant);
181	border: 0.0625em solid var(--border);
182	border-radius: 0.1875em;
183	padding: 0;
184}
185
186/* Header with History/Favorites toggle */
187.omnibox-header {
188	background-color: shade(var(--surface-variant), 1.1);
189	border-bottom: 0.0625em solid var(--border);
190	padding: 0.375em 0.75em;
191}
192
193.omnibox-header-btn {
194	background-color: transparent;
195	background-image: none;
196	border: none;
197	border-radius: 0.125em;
198	padding: 0.25em 0.75em;
199	margin-right: 0.5em;
200	font-size: 0.8125em;
201	font-weight: 500;
202	color: var(--muted);
203	transition: all 100ms ease-in-out;
204}
205
206.omnibox-header-btn:hover {
207	background-color: alpha(var(--accent), 0.15);
208	color: var(--text);
209}
210
211.omnibox-header-btn.omnibox-header-active {
212	background-color: alpha(var(--accent), 0.2);
213	color: var(--accent);
214	font-weight: 600;
215}
216
217/* Search entry field */
218.omnibox-entry {
219	background-color: var(--bg);
220	color: var(--text);
221	border: 0.0625em solid var(--border);
222	border-radius: 0.125em;
223	padding: 0.625em 0.75em;
224	margin: 0.5em 0.75em;
225	font-size: 1em;
226	caret-color: var(--accent);
227}
228
229entry.omnibox-entry:focus,
230entry.omnibox-entry:focus-within,
231entry.omnibox-entry:focus-visible {
232	border-color: var(--accent);
233	background-color: shade(var(--bg), 1.05);
234	outline-style: none;
235	outline-width: 0;
236	outline-color: transparent;
237}
238
239/* Override any internal focus styling */
240entry.omnibox-entry > *:focus,
241entry.omnibox-entry > *:focus-visible {
242	outline-style: none;
243	outline-color: transparent;
244}
245
246/* Scrolled window for suggestions */
247.omnibox-scrolled {
248	background-color: shade(var(--surface-variant), 0.95);
249	border-top: 0.0625em solid var(--border);
250}
251
252/* List box */
253.omnibox-listbox {
254	background-color: transparent;
255}
256
257/* Suggestion rows */
258.omnibox-row {
259	padding: 0.5em 0.75em;
260	margin: 0;
261	border-radius: 0;
262	border-left: 0.1875em solid transparent;
263	border-bottom: 0.0625em solid alpha(var(--border), 0.5);
264	transition: background-color 100ms ease-in-out, border-left 100ms ease-in-out;
265	min-height: 2.75em;
266}
267
268.omnibox-row:last-child {
269	border-bottom: none;
270}
271
272.omnibox-row:hover {
273	background-color: alpha(var(--accent), 0.12);
274	border-left: 0.1875em solid var(--accent);
275}
276
277.omnibox-row:selected {
278	background-color: alpha(var(--accent), 0.2);
279	border-left: 0.1875em solid var(--accent);
280}
281
282/* Favicon in omnibox rows */
283.omnibox-favicon {
284	min-width: 1em;
285	min-height: 1em;
286	margin-right: 0.5em;
287}
288
289/* Suggestion title/URL */
290.omnibox-suggestion-title {
291	font-size: 0.875em;
292	color: var(--text);
293	font-weight: 400;
294}
295
296/* Also style labels inside omnibox rows directly */
297.omnibox-row label {
298	color: var(--text);
299}
300
301.omnibox-row:selected .omnibox-suggestion-title {
302	color: var(--text);
303	font-weight: 500;
304}
305
306/* URL text below title */
307.omnibox-suggestion-url {
308	font-size: 0.75em;
309	color: var(--muted);
310	font-weight: 400;
311}
312
313.omnibox-row:selected .omnibox-suggestion-url {
314	color: var(--muted);
315}
316
317/* Keyboard shortcut badge */
318.omnibox-shortcut-badge {
319	background-color: alpha(var(--muted), 0.3);
320	color: var(--muted);
321	border-radius: 0.25em;
322	padding: 0.125em 0.375em;
323	font-size: 0.625em;
324	font-weight: 500;
325	font-family: var(--font-mono);
326	margin-left: 0.5em;
327}
328
329.omnibox-row:hover .omnibox-shortcut-badge {
330	background-color: alpha(var(--accent), 0.2);
331	color: var(--accent);
332}
333
334.omnibox-row:selected .omnibox-shortcut-badge {
335	background-color: alpha(var(--accent), 0.3);
336	color: var(--accent);
337}
338
339/* Zoom indicator in omnibox header */
340.omnibox-zoom-indicator {
341	background-color: alpha(var(--accent), 0.2);
342	color: var(--accent);
343	border-radius: 0.25em;
344	padding: 0.125em 0.5em;
345	font-size: 0.6875em;
346	font-weight: 600;
347	margin-left: auto;
348}
349`
350}
351
352// generateFindBarCSS creates find bar styles.
353// Uses em units for scalable UI.
354func generateFindBarCSS(p Palette) string {
355	_ = p
356	return `/* ===== Find Bar Styling ===== */
357.find-bar-outer {
358	margin: 0.5em;
359}
360
361.find-bar-container {
362	background-color: var(--surface-variant);
363	border: 0.0625em solid var(--border);
364	border-radius: 0.25em;
365	box-shadow: 0 0.25em 0.75em alpha(black, 0.2);
366	padding: 0.5em;
367	min-width: 20em;
368}
369
370.find-bar-input-row {
371	margin-bottom: 0.25em;
372}
373
374.find-bar-entry {
375	background-color: var(--bg);
376	color: var(--text);
377	border: 0.0625em solid var(--border);
378	border-radius: 0.1875em;
379	padding: 0.4em 0.5em;
380	caret-color: var(--accent);
381}
382
383entry.find-bar-entry:focus,
384entry.find-bar-entry:focus-within,
385entry.find-bar-entry:focus-visible {
386	border-color: var(--accent);
387	outline-style: none;
388	outline-width: 0;
389	outline-color: transparent;
390}
391
392.find-bar-entry.not-found {
393	background-color: alpha(#ef4444, 0.15);
394	border-color: #ef4444;
395}
396
397.find-bar-count {
398	font-size: 0.8em;
399	color: var(--muted);
400	min-width: 3.5em;
401}
402
403.find-bar-count.find-bar-count-has {
404	color: var(--accent);
405}
406
407.find-bar-nav,
408.find-bar-close,
409.find-bar-toggle {
410	background-image: none;
411	background-color: alpha(var(--bg), 0.6);
412	border: 0.0625em solid var(--border);
413	border-radius: 0.1875em;
414	padding: 0.25em 0.5em;
415	transition: background-color 120ms ease-in-out, border-color 120ms ease-in-out;
416}
417
418.find-bar-nav:hover,
419.find-bar-close:hover,
420.find-bar-toggle:hover {
421	background-color: alpha(var(--accent), 0.15);
422}
423
424.find-bar-toggle:checked {
425	background-color: alpha(var(--accent), 0.2);
426	border-color: var(--accent);
427	color: var(--accent);
428}
429`
430}
431
432// generatePaneCSS creates pane border styles.
433// Uses em units for scalable UI.
434func generatePaneCSS(p Palette) string {
435	return `/* ===== Pane Styling ===== */
436
437/* Pane border - default transparent */
438.pane-border {
439	border: 0.0625em solid transparent;
440}
441
442/* Active pane - accent border */
443.pane-active {
444	border: 0.0625em solid var(--accent);
445}
446
447/* Hide active border when only one pane exists */
448.single-pane .pane-active {
449	border-color: transparent;
450}
451
452/* Pane mode active - thick blue inset border (for overlay) */
453.pane-mode-active {
454	background-color: transparent;
455	box-shadow: inset 0 0 0 0.25em #4A90E2;
456	border-radius: 0;
457}
458
459/* Tab mode active - thick orange inset border (for overlay) */
460.tab-mode-active {
461	background-color: transparent;
462	box-shadow: inset 0 0 0 0.25em #FFA500;
463	border-radius: 0;
464}
465`
466}
467
468// generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
469// Uses em units for scalable UI.
470func generateStackedPaneCSS(p Palette) string {
471	return `/* ===== Stacked Pane Styling ===== */
472
473/* Title bar for inactive panes in a stack */
474.stacked-pane-titlebar {
475	background-color: var(--surface-variant);
476	border-bottom: 0.0625em solid var(--border);
477	padding: 0.25em 0.5em;
478	min-height: 1.5em;
479}
480
481/* Title bar button wrapper - clickable area */
482button.stacked-pane-title-button {
483	background-color: var(--surface-variant);
484	background-image: none;
485	border: none;
486	border-bottom: 0.0625em solid var(--border);
487	border-radius: 0;
488	padding: 0;
489	margin: 0;
490	transition: background-color 150ms ease-in-out;
491}
492
493button.stacked-pane-title-button:hover {
494	background-color: shade(var(--surface-variant), 1.15);
495}
496
497button.stacked-pane-title-button:focus {
498	outline: none;
499	box-shadow: none;
500}
501
502/* Title bar content box */
503.stacked-pane-titlebar.active {
504	background-color: shade(var(--surface-variant), 1.2);
505	border-left: 0.1875em solid var(--accent);
506}
507
508/* Favicon image in title bar */
509.stacked-pane-titlebar image {
510	min-width: 1em;
511	min-height: 1em;
512	margin-right: 0.375em;
513}
514
515/* Title text in title bar */
516.stacked-pane-titlebar label {
517	color: var(--text);
518	font-size: 0.75em;
519	font-weight: 400;
520}
521
522button.stacked-pane-title-button:hover .stacked-pane-titlebar label {
523	color: var(--accent);
524}
525`
526}
527
528// generateProgressBarCSS creates progress bar styles for page loading indication.
529func generateProgressBarCSS(p Palette) string {
530	return `/* ===== Progress Bar Styling ===== */
531
532/* Native GtkProgressBar with osd class */
533progressbar.osd {
534	min-height: 4px;
535}
536
537progressbar.osd trough {
538	min-height: 4px;
539	min-width: 0px;
540	background-color: alpha(var(--bg), 0.3);
541}
542
543progressbar.osd progress {
544	min-height: 4px;
545	min-width: 0px;
546	background-color: var(--accent);
547}
548`
549}
550
551// generateToasterCSS creates toast notification styles.
552// Uses em units for scalable UI.
553func generateToasterCSS(p Palette) string {
554	return `/* ===== Toaster Styling ===== */
555
556/* Toast notification container */
557.toast {
558	background-color: var(--surface-variant);
559	border-radius: 0.25em;
560	padding: 0.375em 0.625em;
561	margin: 0.5em;
562	font-size: 0.875em;
563	font-weight: 600;
564	box-shadow: 0 2px 8px alpha(black, 0.3);
565	min-width: 2.5em;
566}
567
568/* Toast level: info (default, accent color) */
569.toast-info {
570	background-color: alpha(var(--accent), 0.9);
571	color: var(--bg);
572}
573
574/* Toast level: success */
575.toast-success {
576	background-color: alpha(var(--success), 0.9);
577	color: var(--bg);
578}
579
580/* Toast level: warning */
581.toast-warning {
582	background-color: alpha(var(--warning), 0.9);
583	color: var(--bg);
584}
585
586/* Toast level: error */
587.toast-error {
588	background-color: alpha(var(--destructive), 0.9);
589	color: var(--bg);
590}
591`
592}

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