v0.20.1 d9a391f

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 overlay container - theme background prevents white flash */
438.pane-overlay {
439	background-color: var(--bg);
440}
441
442/* Pane border - default transparent */
443.pane-border {
444	border: 0.0625em solid transparent;
445}
446
447/* Active pane - accent border */
448.pane-active {
449	border: 0.0625em solid var(--accent);
450}
451
452/* Hide active border when only one pane exists */
453.single-pane .pane-active {
454	border-color: transparent;
455}
456
457/* Pane mode active - thick blue inset border (for overlay) */
458.pane-mode-active {
459	background-color: transparent;
460	box-shadow: inset 0 0 0 0.25em #4A90E2;
461	border-radius: 0;
462}
463
464/* Tab mode active - thick orange inset border (for overlay) */
465.tab-mode-active {
466	background-color: transparent;
467	box-shadow: inset 0 0 0 0.25em #FFA500;
468	border-radius: 0;
469}
470`
471}
472
473// generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
474// Uses em units for scalable UI.
475func generateStackedPaneCSS(p Palette) string {
476	return `/* ===== Stacked Pane Styling ===== */
477
478/* Title bar for inactive panes in a stack */
479.stacked-pane-titlebar {
480	background-color: var(--surface-variant);
481	border-bottom: 0.0625em solid var(--border);
482	padding: 0.25em 0.5em;
483	min-height: 1.5em;
484}
485
486/* Title bar button wrapper - clickable area */
487button.stacked-pane-title-button {
488	background-color: var(--surface-variant);
489	background-image: none;
490	border: none;
491	border-bottom: 0.0625em solid var(--border);
492	border-radius: 0;
493	padding: 0;
494	margin: 0;
495	transition: background-color 150ms ease-in-out;
496}
497
498button.stacked-pane-title-button:hover {
499	background-color: shade(var(--surface-variant), 1.15);
500}
501
502button.stacked-pane-title-button:focus {
503	outline: none;
504	box-shadow: none;
505}
506
507/* Title bar content box */
508.stacked-pane-titlebar.active {
509	background-color: shade(var(--surface-variant), 1.2);
510	border-left: 0.1875em solid var(--accent);
511}
512
513/* Favicon image in title bar */
514.stacked-pane-titlebar image {
515	min-width: 1em;
516	min-height: 1em;
517	margin-right: 0.375em;
518}
519
520/* Title text in title bar */
521.stacked-pane-titlebar label {
522	color: var(--text);
523	font-size: 0.75em;
524	font-weight: 400;
525}
526
527button.stacked-pane-title-button:hover .stacked-pane-titlebar label {
528	color: var(--accent);
529}
530`
531}
532
533// generateProgressBarCSS creates progress bar styles for page loading indication.
534func generateProgressBarCSS(p Palette) string {
535	return `/* ===== Progress Bar Styling ===== */
536
537/* Native GtkProgressBar with osd class */
538progressbar.osd {
539	min-height: 4px;
540}
541
542progressbar.osd trough {
543	min-height: 4px;
544	min-width: 0px;
545	background-color: alpha(var(--bg), 0.3);
546}
547
548progressbar.osd progress {
549	min-height: 4px;
550	min-width: 0px;
551	background-color: var(--accent);
552}
553`
554}
555
556// generateToasterCSS creates toast notification styles.
557// Uses em units for scalable UI.
558func generateToasterCSS(p Palette) string {
559	return `/* ===== Toaster Styling ===== */
560
561/* Toast notification container */
562.toast {
563	background-color: var(--surface-variant);
564	border-radius: 0.25em;
565	padding: 0.375em 0.625em;
566	margin: 0.5em;
567	font-size: 0.875em;
568	font-weight: 600;
569	box-shadow: 0 2px 8px alpha(black, 0.3);
570	min-width: 2.5em;
571}
572
573/* Toast level: info (default, accent color) */
574.toast-info {
575	background-color: alpha(var(--accent), 0.9);
576	color: var(--bg);
577}
578
579/* Toast level: success */
580.toast-success {
581	background-color: alpha(var(--success), 0.9);
582	color: var(--bg);
583}
584
585/* Toast level: warning */
586.toast-warning {
587	background-color: alpha(var(--warning), 0.9);
588	color: var(--bg);
589}
590
591/* Toast level: error */
592.toast-error {
593	background-color: alpha(var(--destructive), 0.9);
594	color: var(--bg);
595}
596`
597}

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