detached 453b589

feat(theme): implement UI scaling via CSS with em-based units

commit
453b589b6ed885681dac22998e4a1f9c39bef239
author
brice · · 2025-12-15 19:29 UTC
committer
brice · · 2025-12-15 22:00 UTC
parent
ac0f90d
signature
unsigned
2 files · +105 -43
M internal/ui/theme/css.go +94 -41
@@ -2,11 +2,22 @@
22 package theme
33
44 import (
5+ "fmt"
56 "strings"
67 )
78
8-// GenerateCSS creates GTK4 CSS using the provided palette.
9+// GenerateCSS creates GTK4 CSS using the provided palette with default scale.
910 func 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.
16+func GenerateCSSWithScale(p Palette, scale float64) string {
17+ if scale <= 0 {
18+ scale = 1.0
19+ }
20+
1021 var sb strings.Builder
1122
1223 // CSS custom properties (variables) - GTK4 uses :root selector
@@ -15,6 +26,12 @@ func GenerateCSS(p Palette) string {
1526 sb.WriteString(p.ToCSSVars())
1627 sb.WriteString("}\n\n")
1728
29+ // Global UI scaling via font-size on all widgets
30+ if scale != 1.0 {
31+ sb.WriteString(generateScalingCSS(scale))
32+ sb.WriteString("\n")
33+ }
34+
1835 // Tab bar styling
1936 sb.WriteString(generateTabBarCSS(p))
2037 sb.WriteString("\n")
@@ -33,14 +50,29 @@ func GenerateCSS(p Palette) string {
3350 return sb.String()
3451 }
3552
53+// generateScalingCSS creates CSS rules that scale the UI.
54+// GTK4 CSS doesn't support transform:scale() well, so we scale font-size
55+// and use relative units (em) for padding/margins in other rules.
56+func generateScalingCSS(scale float64) string {
57+ // Use 16px base so em conversions are correct:
58+ // 1px = 0.0625em (1/16), 4px = 0.25em, etc.
59+ basePx := int(16 * scale)
60+ return fmt.Sprintf(`/* UI Scaling (%.0f%%) */
61+window {
62+ font-size: %dpx;
63+}
64+`, scale*100, basePx)
65+}
66+
3667 // generateTabBarCSS creates tab bar styles.
68+// Uses em units for scalable UI.
3769 func generateTabBarCSS(p Palette) string {
3870 return `/* Tab bar styling */
3971 .tab-bar {
4072 background-color: var(--surface);
41- border-top: 2px solid var(--border);
73+ border-top: 0.125em solid var(--border);
4274 padding: 0;
43- min-height: 32px;
75+ min-height: 2em;
4476 }
4577
4678 /* Tab button styling */
@@ -48,9 +80,9 @@ button.tab-button {
4880 background-color: var(--surface-variant);
4981 background-image: none;
5082 border: none;
51- border-right: 1px solid var(--border);
83+ border-right: 0.0625em solid var(--border);
5284 border-radius: 0;
53- padding: 4px 8px;
85+ padding: 0.25em 0.5em;
5486 transition: background-color 200ms ease-in-out;
5587 }
5688
@@ -65,7 +97,7 @@ button.tab-button.tab-button-active {
6597
6698 /* Tab title text */
6799 .tab-title {
68- font-size: 12px;
100+ font-size: 0.75em;
69101 color: var(--text);
70102 font-weight: 500;
71103 }
@@ -73,14 +105,15 @@ button.tab-button.tab-button-active {
73105 }
74106
75107 // generateOmniboxCSS creates omnibox styles.
108+// Uses em units for scalable UI.
76109 func generateOmniboxCSS(p Palette) string {
77110 return `/* ===== Omnibox Styling ===== */
78111
79112 /* Omnibox window - floating popup */
80113 window.omnibox-window {
81114 background-color: var(--surface-variant);
82- border: 1px solid var(--border);
83- border-radius: 3px;
115+ border: 0.0625em solid var(--border);
116+ border-radius: 0.1875em;
84117 }
85118
86119 /* Main container */
@@ -92,18 +125,18 @@ window.omnibox-window {
92125 /* Header with History/Favorites toggle */
93126 .omnibox-header {
94127 background-color: shade(var(--surface-variant), 1.1);
95- border-bottom: 1px solid var(--border);
96- padding: 6px 12px;
128+ border-bottom: 0.0625em solid var(--border);
129+ padding: 0.375em 0.75em;
97130 }
98131
99132 .omnibox-header-btn {
100133 background-color: transparent;
101134 background-image: none;
102135 border: none;
103- border-radius: 2px;
104- padding: 4px 12px;
105- margin-right: 8px;
106- font-size: 13px;
136+ border-radius: 0.125em;
137+ padding: 0.25em 0.75em;
138+ margin-right: 0.5em;
139+ font-size: 0.8125em;
107140 font-weight: 500;
108141 color: var(--muted);
109142 transition: all 100ms ease-in-out;
@@ -124,11 +157,11 @@ window.omnibox-window {
124157 .omnibox-entry {
125158 background-color: var(--bg);
126159 color: var(--text);
127- border: 1px solid var(--border);
128- border-radius: 2px;
129- padding: 10px 12px;
130- margin: 8px 12px;
131- font-size: 16px;
160+ border: 0.0625em solid var(--border);
161+ border-radius: 0.125em;
162+ padding: 0.625em 0.75em;
163+ margin: 0.5em 0.75em;
164+ font-size: 1em;
132165 caret-color: var(--accent);
133166 }
134167
@@ -140,7 +173,7 @@ window.omnibox-window {
140173 /* Scrolled window for suggestions */
141174 .omnibox-scrolled {
142175 background-color: shade(var(--surface-variant), 0.95);
143- border-top: 1px solid var(--border);
176+ border-top: 0.0625em solid var(--border);
144177 }
145178
146179 /* List box */
@@ -150,11 +183,11 @@ window.omnibox-window {
150183
151184 /* Suggestion rows */
152185 .omnibox-row {
153- padding: 8px 12px;
186+ padding: 0.5em 0.75em;
154187 margin: 0;
155188 border-radius: 0;
156- border-left: 3px solid transparent;
157- border-bottom: 1px solid alpha(var(--border), 0.5);
189+ border-left: 0.1875em solid transparent;
190+ border-bottom: 0.0625em solid alpha(var(--border), 0.5);
158191 transition: background-color 100ms ease-in-out, border-left-color 100ms ease-in-out;
159192 }
160193
@@ -172,9 +205,16 @@ window.omnibox-window {
172205 border-left-color: var(--accent);
173206 }
174207
208+/* Favicon in omnibox rows */
209+.omnibox-favicon {
210+ min-width: 1em;
211+ min-height: 1em;
212+ margin-right: 0.5em;
213+}
214+
175215 /* Suggestion title/URL */
176216 .omnibox-suggestion-title {
177- font-size: 14px;
217+ font-size: 0.875em;
178218 color: var(--text, #ffffff);
179219 font-weight: 400;
180220 }
@@ -193,12 +233,12 @@ window.omnibox-window {
193233 .omnibox-shortcut-badge {
194234 background-color: alpha(var(--muted, #909090), 0.3);
195235 color: var(--muted, #909090);
196- border-radius: 4px;
197- padding: 2px 6px;
198- font-size: 10px;
236+ border-radius: 0.25em;
237+ padding: 0.125em 0.375em;
238+ font-size: 0.625em;
199239 font-weight: 500;
200240 font-family: monospace;
201- margin-left: 8px;
241+ margin-left: 0.5em;
202242 }
203243
204244 .omnibox-row:hover .omnibox-shortcut-badge {
@@ -210,49 +250,62 @@ window.omnibox-window {
210250 background-color: alpha(var(--accent, #4ade80), 0.3);
211251 color: var(--accent, #4ade80);
212252 }
253+
254+/* Zoom indicator in omnibox header */
255+.omnibox-zoom-indicator {
256+ background-color: alpha(var(--accent, #4ade80), 0.2);
257+ color: var(--accent, #4ade80);
258+ border-radius: 0.25em;
259+ padding: 0.125em 0.5em;
260+ font-size: 0.6875em;
261+ font-weight: 600;
262+ margin-left: auto;
263+}
213264 `
214265 }
215266
216267 // generatePaneCSS creates pane border styles.
268+// Uses em units for scalable UI.
217269 func generatePaneCSS(p Palette) string {
218270 return `/* ===== Pane Styling ===== */
219271
220272 /* Pane border - default transparent */
221273 .pane-border {
222- border: 1px solid transparent;
274+ border: 0.0625em solid transparent;
223275 }
224276
225277 /* Active pane - accent border */
226278 .pane-active {
227- border: 1px solid var(--accent);
279+ border: 0.0625em solid var(--accent);
228280 }
229281
230282 /* Pane mode active - thick blue inset border (for overlay) */
231283 .pane-mode-active {
232284 background-color: transparent;
233- box-shadow: inset 0 0 0 4px #4A90E2;
285+ box-shadow: inset 0 0 0 0.25em #4A90E2;
234286 border-radius: 0;
235287 }
236288
237289 /* Tab mode active - thick orange inset border (for overlay) */
238290 .tab-mode-active {
239291 background-color: transparent;
240- box-shadow: inset 0 0 0 4px #FFA500;
292+ box-shadow: inset 0 0 0 0.25em #FFA500;
241293 border-radius: 0;
242294 }
243295 `
244296 }
245297
246298 // generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
299+// Uses em units for scalable UI.
247300 func generateStackedPaneCSS(p Palette) string {
248301 return `/* ===== Stacked Pane Styling ===== */
249302
250303 /* Title bar for inactive panes in a stack */
251304 .stacked-pane-titlebar {
252305 background-color: var(--surface-variant);
253- border-bottom: 1px solid var(--border);
254- padding: 4px 8px;
255- min-height: 24px;
306+ border-bottom: 0.0625em solid var(--border);
307+ padding: 0.25em 0.5em;
308+ min-height: 1.5em;
256309 }
257310
258311 /* Title bar button wrapper - clickable area */
@@ -260,7 +313,7 @@ button.stacked-pane-title-button {
260313 background-color: var(--surface-variant);
261314 background-image: none;
262315 border: none;
263- border-bottom: 1px solid var(--border);
316+ border-bottom: 0.0625em solid var(--border);
264317 border-radius: 0;
265318 padding: 0;
266319 margin: 0;
@@ -279,20 +332,20 @@ button.stacked-pane-title-button:focus {
279332 /* Title bar content box */
280333 .stacked-pane-titlebar.active {
281334 background-color: alpha(var(--accent), 0.1);
282- border-left: 3px solid var(--accent);
335+ border-left: 0.1875em solid var(--accent);
283336 }
284337
285338 /* Favicon image in title bar */
286339 .stacked-pane-titlebar image {
287- min-width: 16px;
288- min-height: 16px;
289- margin-right: 6px;
340+ min-width: 1em;
341+ min-height: 1em;
342+ margin-right: 0.375em;
290343 }
291344
292345 /* Title text in title bar */
293346 .stacked-pane-titlebar label {
294347 color: var(--text);
295- font-size: 12px;
348+ font-size: 0.75em;
296349 font-weight: 400;
297350 }
298351
@@ -2,11 +2,22 @@
2 package theme2 package theme
3 3
4 import (4 import (
5+ "fmt"
5 "strings"6 "strings"
6 )7 )
7 8
8-// GenerateCSS creates GTK4 CSS using the provided palette.9+// GenerateCSS creates GTK4 CSS using the provided palette with default scale.
9 func GenerateCSS(p Palette) string {10 func 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.
16+func GenerateCSSWithScale(p Palette, scale float64) string {
17+ if scale <= 0 {
18+ scale = 1.0
19+ }
20+
10 var sb strings.Builder21 var sb strings.Builder
11 22
12 // CSS custom properties (variables) - GTK4 uses :root selector23 // CSS custom properties (variables) - GTK4 uses :root selector
@@ -15,6 +26,12 @@ func GenerateCSS(p Palette) string {
15 sb.WriteString(p.ToCSSVars())26 sb.WriteString(p.ToCSSVars())
16 sb.WriteString("}\n\n")27 sb.WriteString("}\n\n")
17 28
29+ // Global UI scaling via font-size on all widgets
30+ if scale != 1.0 {
31+ sb.WriteString(generateScalingCSS(scale))
32+ sb.WriteString("\n")
33+ }
34+
18 // Tab bar styling35 // Tab bar styling
19 sb.WriteString(generateTabBarCSS(p))36 sb.WriteString(generateTabBarCSS(p))
20 sb.WriteString("\n")37 sb.WriteString("\n")
@@ -33,14 +50,29 @@ func GenerateCSS(p Palette) string {
33 return sb.String()50 return sb.String()
34 }51 }
35 52
53+// generateScalingCSS creates CSS rules that scale the UI.
54+// GTK4 CSS doesn't support transform:scale() well, so we scale font-size
55+// and use relative units (em) for padding/margins in other rules.
56+func generateScalingCSS(scale float64) string {
57+ // Use 16px base so em conversions are correct:
58+ // 1px = 0.0625em (1/16), 4px = 0.25em, etc.
59+ basePx := int(16 * scale)
60+ return fmt.Sprintf(`/* UI Scaling (%.0f%%) */
61+window {
62+ font-size: %dpx;
63+}
64+`, scale*100, basePx)
65+}
66+
36 // generateTabBarCSS creates tab bar styles.67 // generateTabBarCSS creates tab bar styles.
68+// Uses em units for scalable UI.
37 func generateTabBarCSS(p Palette) string {69 func generateTabBarCSS(p Palette) string {
38 return `/* Tab bar styling */70 return `/* Tab bar styling */
39 .tab-bar {71 .tab-bar {
40 background-color: var(--surface);72 background-color: var(--surface);
41- border-top: 2px solid var(--border);73+ border-top: 0.125em solid var(--border);
42 padding: 0;74 padding: 0;
43- min-height: 32px;75+ min-height: 2em;
44 }76 }
45 77
46 /* Tab button styling */78 /* Tab button styling */
@@ -48,9 +80,9 @@ button.tab-button {
48 background-color: var(--surface-variant);80 background-color: var(--surface-variant);
49 background-image: none;81 background-image: none;
50 border: none;82 border: none;
51- border-right: 1px solid var(--border);83+ border-right: 0.0625em solid var(--border);
52 border-radius: 0;84 border-radius: 0;
53- padding: 4px 8px;85+ padding: 0.25em 0.5em;
54 transition: background-color 200ms ease-in-out;86 transition: background-color 200ms ease-in-out;
55 }87 }
56 88
@@ -65,7 +97,7 @@ button.tab-button.tab-button-active {
65 97
66 /* Tab title text */98 /* Tab title text */
67 .tab-title {99 .tab-title {
68- font-size: 12px;100+ font-size: 0.75em;
69 color: var(--text);101 color: var(--text);
70 font-weight: 500;102 font-weight: 500;
71 }103 }
@@ -73,14 +105,15 @@ button.tab-button.tab-button-active {
73 }105 }
74 106
75 // generateOmniboxCSS creates omnibox styles.107 // generateOmniboxCSS creates omnibox styles.
108+// Uses em units for scalable UI.
76 func generateOmniboxCSS(p Palette) string {109 func generateOmniboxCSS(p Palette) string {
77 return `/* ===== Omnibox Styling ===== */110 return `/* ===== Omnibox Styling ===== */
78 111
79 /* Omnibox window - floating popup */112 /* Omnibox window - floating popup */
80 window.omnibox-window {113 window.omnibox-window {
81 background-color: var(--surface-variant);114 background-color: var(--surface-variant);
82- border: 1px solid var(--border);115+ border: 0.0625em solid var(--border);
83- border-radius: 3px;116+ border-radius: 0.1875em;
84 }117 }
85 118
86 /* Main container */119 /* Main container */
@@ -92,18 +125,18 @@ window.omnibox-window {
92 /* Header with History/Favorites toggle */125 /* Header with History/Favorites toggle */
93 .omnibox-header {126 .omnibox-header {
94 background-color: shade(var(--surface-variant), 1.1);127 background-color: shade(var(--surface-variant), 1.1);
95- border-bottom: 1px solid var(--border);128+ border-bottom: 0.0625em solid var(--border);
96- padding: 6px 12px;129+ padding: 0.375em 0.75em;
97 }130 }
98 131
99 .omnibox-header-btn {132 .omnibox-header-btn {
100 background-color: transparent;133 background-color: transparent;
101 background-image: none;134 background-image: none;
102 border: none;135 border: none;
103- border-radius: 2px;136+ border-radius: 0.125em;
104- padding: 4px 12px;137+ padding: 0.25em 0.75em;
105- margin-right: 8px;138+ margin-right: 0.5em;
106- font-size: 13px;139+ font-size: 0.8125em;
107 font-weight: 500;140 font-weight: 500;
108 color: var(--muted);141 color: var(--muted);
109 transition: all 100ms ease-in-out;142 transition: all 100ms ease-in-out;
@@ -124,11 +157,11 @@ window.omnibox-window {
124 .omnibox-entry {157 .omnibox-entry {
125 background-color: var(--bg);158 background-color: var(--bg);
126 color: var(--text);159 color: var(--text);
127- border: 1px solid var(--border);160+ border: 0.0625em solid var(--border);
128- border-radius: 2px;161+ border-radius: 0.125em;
129- padding: 10px 12px;162+ padding: 0.625em 0.75em;
130- margin: 8px 12px;163+ margin: 0.5em 0.75em;
131- font-size: 16px;164+ font-size: 1em;
132 caret-color: var(--accent);165 caret-color: var(--accent);
133 }166 }
134 167
@@ -140,7 +173,7 @@ window.omnibox-window {
140 /* Scrolled window for suggestions */173 /* Scrolled window for suggestions */
141 .omnibox-scrolled {174 .omnibox-scrolled {
142 background-color: shade(var(--surface-variant), 0.95);175 background-color: shade(var(--surface-variant), 0.95);
143- border-top: 1px solid var(--border);176+ border-top: 0.0625em solid var(--border);
144 }177 }
145 178
146 /* List box */179 /* List box */
@@ -150,11 +183,11 @@ window.omnibox-window {
150 183
151 /* Suggestion rows */184 /* Suggestion rows */
152 .omnibox-row {185 .omnibox-row {
153- padding: 8px 12px;186+ padding: 0.5em 0.75em;
154 margin: 0;187 margin: 0;
155 border-radius: 0;188 border-radius: 0;
156- border-left: 3px solid transparent;189+ border-left: 0.1875em solid transparent;
157- border-bottom: 1px solid alpha(var(--border), 0.5);190+ border-bottom: 0.0625em solid alpha(var(--border), 0.5);
158 transition: background-color 100ms ease-in-out, border-left-color 100ms ease-in-out;191 transition: background-color 100ms ease-in-out, border-left-color 100ms ease-in-out;
159 }192 }
160 193
@@ -172,9 +205,16 @@ window.omnibox-window {
172 border-left-color: var(--accent);205 border-left-color: var(--accent);
173 }206 }
174 207
208+/* Favicon in omnibox rows */
209+.omnibox-favicon {
210+ min-width: 1em;
211+ min-height: 1em;
212+ margin-right: 0.5em;
213+}
214+
175 /* Suggestion title/URL */215 /* Suggestion title/URL */
176 .omnibox-suggestion-title {216 .omnibox-suggestion-title {
177- font-size: 14px;217+ font-size: 0.875em;
178 color: var(--text, #ffffff);218 color: var(--text, #ffffff);
179 font-weight: 400;219 font-weight: 400;
180 }220 }
@@ -193,12 +233,12 @@ window.omnibox-window {
193 .omnibox-shortcut-badge {233 .omnibox-shortcut-badge {
194 background-color: alpha(var(--muted, #909090), 0.3);234 background-color: alpha(var(--muted, #909090), 0.3);
195 color: var(--muted, #909090);235 color: var(--muted, #909090);
196- border-radius: 4px;236+ border-radius: 0.25em;
197- padding: 2px 6px;237+ padding: 0.125em 0.375em;
198- font-size: 10px;238+ font-size: 0.625em;
199 font-weight: 500;239 font-weight: 500;
200 font-family: monospace;240 font-family: monospace;
201- margin-left: 8px;241+ margin-left: 0.5em;
202 }242 }
203 243
204 .omnibox-row:hover .omnibox-shortcut-badge {244 .omnibox-row:hover .omnibox-shortcut-badge {
@@ -210,49 +250,62 @@ window.omnibox-window {
210 background-color: alpha(var(--accent, #4ade80), 0.3);250 background-color: alpha(var(--accent, #4ade80), 0.3);
211 color: var(--accent, #4ade80);251 color: var(--accent, #4ade80);
212 }252 }
253+
254+/* Zoom indicator in omnibox header */
255+.omnibox-zoom-indicator {
256+ background-color: alpha(var(--accent, #4ade80), 0.2);
257+ color: var(--accent, #4ade80);
258+ border-radius: 0.25em;
259+ padding: 0.125em 0.5em;
260+ font-size: 0.6875em;
261+ font-weight: 600;
262+ margin-left: auto;
263+}
213 `264 `
214 }265 }
215 266
216 // generatePaneCSS creates pane border styles.267 // generatePaneCSS creates pane border styles.
268+// Uses em units for scalable UI.
217 func generatePaneCSS(p Palette) string {269 func generatePaneCSS(p Palette) string {
218 return `/* ===== Pane Styling ===== */270 return `/* ===== Pane Styling ===== */
219 271
220 /* Pane border - default transparent */272 /* Pane border - default transparent */
221 .pane-border {273 .pane-border {
222- border: 1px solid transparent;274+ border: 0.0625em solid transparent;
223 }275 }
224 276
225 /* Active pane - accent border */277 /* Active pane - accent border */
226 .pane-active {278 .pane-active {
227- border: 1px solid var(--accent);279+ border: 0.0625em solid var(--accent);
228 }280 }
229 281
230 /* Pane mode active - thick blue inset border (for overlay) */282 /* Pane mode active - thick blue inset border (for overlay) */
231 .pane-mode-active {283 .pane-mode-active {
232 background-color: transparent;284 background-color: transparent;
233- box-shadow: inset 0 0 0 4px #4A90E2;285+ box-shadow: inset 0 0 0 0.25em #4A90E2;
234 border-radius: 0;286 border-radius: 0;
235 }287 }
236 288
237 /* Tab mode active - thick orange inset border (for overlay) */289 /* Tab mode active - thick orange inset border (for overlay) */
238 .tab-mode-active {290 .tab-mode-active {
239 background-color: transparent;291 background-color: transparent;
240- box-shadow: inset 0 0 0 4px #FFA500;292+ box-shadow: inset 0 0 0 0.25em #FFA500;
241 border-radius: 0;293 border-radius: 0;
242 }294 }
243 `295 `
244 }296 }
245 297
246 // generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.298 // generateStackedPaneCSS creates stacked pane (Zellij-style tabs within panes) styles.
299+// Uses em units for scalable UI.
247 func generateStackedPaneCSS(p Palette) string {300 func generateStackedPaneCSS(p Palette) string {
248 return `/* ===== Stacked Pane Styling ===== */301 return `/* ===== Stacked Pane Styling ===== */
249 302
250 /* Title bar for inactive panes in a stack */303 /* Title bar for inactive panes in a stack */
251 .stacked-pane-titlebar {304 .stacked-pane-titlebar {
252 background-color: var(--surface-variant);305 background-color: var(--surface-variant);
253- border-bottom: 1px solid var(--border);306+ border-bottom: 0.0625em solid var(--border);
254- padding: 4px 8px;307+ padding: 0.25em 0.5em;
255- min-height: 24px;308+ min-height: 1.5em;
256 }309 }
257 310
258 /* Title bar button wrapper - clickable area */311 /* Title bar button wrapper - clickable area */
@@ -260,7 +313,7 @@ button.stacked-pane-title-button {
260 background-color: var(--surface-variant);313 background-color: var(--surface-variant);
261 background-image: none;314 background-image: none;
262 border: none;315 border: none;
263- border-bottom: 1px solid var(--border);316+ border-bottom: 0.0625em solid var(--border);
264 border-radius: 0;317 border-radius: 0;
265 padding: 0;318 padding: 0;
266 margin: 0;319 margin: 0;
@@ -279,20 +332,20 @@ button.stacked-pane-title-button:focus {
279 /* Title bar content box */332 /* Title bar content box */
280 .stacked-pane-titlebar.active {333 .stacked-pane-titlebar.active {
281 background-color: alpha(var(--accent), 0.1);334 background-color: alpha(var(--accent), 0.1);
282- border-left: 3px solid var(--accent);335+ border-left: 0.1875em solid var(--accent);
283 }336 }
284 337
285 /* Favicon image in title bar */338 /* Favicon image in title bar */
286 .stacked-pane-titlebar image {339 .stacked-pane-titlebar image {
287- min-width: 16px;340+ min-width: 1em;
288- min-height: 16px;341+ min-height: 1em;
289- margin-right: 6px;342+ margin-right: 0.375em;
290 }343 }
291 344
292 /* Title text in title bar */345 /* Title text in title bar */
293 .stacked-pane-titlebar label {346 .stacked-pane-titlebar label {
294 color: var(--text);347 color: var(--text);
295- font-size: 12px;348+ font-size: 0.75em;
296 font-weight: 400;349 font-weight: 400;
297 }350 }
298 351
M internal/ui/theme/manager.go +11 -2
@@ -15,6 +15,7 @@ type Manager struct {
1515 prefersDark bool // Resolved dark mode preference
1616 lightPalette Palette // Light theme colors
1717 darkPalette Palette // Dark theme colors
18+ uiScale float64 // UI scaling factor (1.0 = 100%)
1819 cssProvider *gtk.CssProvider
1920 }
2021
@@ -41,9 +42,16 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager {
4142 darkPalette = DefaultDarkPalette()
4243 }
4344
45+ // Get UI scale factor (default to 1.0 if not set)
46+ uiScale := 1.0
47+ if cfg != nil && cfg.DefaultUIScale > 0 {
48+ uiScale = cfg.DefaultUIScale
49+ }
50+
4451 log.Debug().
4552 Str("scheme", scheme).
4653 Bool("prefers_dark", prefersDark).
54+ Float64("ui_scale", uiScale).
4755 Msg("theme manager initialized")
4856
4957 return &Manager{
@@ -51,6 +59,7 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager {
5159 prefersDark: prefersDark,
5260 lightPalette: lightPalette,
5361 darkPalette: darkPalette,
62+ uiScale: uiScale,
5463 }
5564 }
5665
@@ -86,9 +95,9 @@ func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) {
8695 return
8796 }
8897
89- // Generate CSS with current palette
98+ // Generate CSS with current palette and UI scale
9099 palette := m.GetCurrentPalette()
91- css := GenerateCSS(palette)
100+ css := GenerateCSSWithScale(palette, m.uiScale)
92101
93102 // Create CSS provider if needed
94103 if m.cssProvider == nil {
@@ -15,6 +15,7 @@ type Manager struct {
15 prefersDark bool // Resolved dark mode preference15 prefersDark bool // Resolved dark mode preference
16 lightPalette Palette // Light theme colors16 lightPalette Palette // Light theme colors
17 darkPalette Palette // Dark theme colors17 darkPalette Palette // Dark theme colors
18+ uiScale float64 // UI scaling factor (1.0 = 100%)
18 cssProvider *gtk.CssProvider19 cssProvider *gtk.CssProvider
19 }20 }
20 21
@@ -41,9 +42,16 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager {
41 darkPalette = DefaultDarkPalette()42 darkPalette = DefaultDarkPalette()
42 }43 }
43 44
45+ // Get UI scale factor (default to 1.0 if not set)
46+ uiScale := 1.0
47+ if cfg != nil && cfg.DefaultUIScale > 0 {
48+ uiScale = cfg.DefaultUIScale
49+ }
50+
44 log.Debug().51 log.Debug().
45 Str("scheme", scheme).52 Str("scheme", scheme).
46 Bool("prefers_dark", prefersDark).53 Bool("prefers_dark", prefersDark).
54+ Float64("ui_scale", uiScale).
47 Msg("theme manager initialized")55 Msg("theme manager initialized")
48 56
49 return &Manager{57 return &Manager{
@@ -51,6 +59,7 @@ func NewManager(ctx context.Context, cfg *config.Config) *Manager {
51 prefersDark: prefersDark,59 prefersDark: prefersDark,
52 lightPalette: lightPalette,60 lightPalette: lightPalette,
53 darkPalette: darkPalette,61 darkPalette: darkPalette,
62+ uiScale: uiScale,
54 }63 }
55 }64 }
56 65
@@ -86,9 +95,9 @@ func (m *Manager) ApplyToDisplay(ctx context.Context, display *gdk.Display) {
86 return95 return
87 }96 }
88 97
89- // Generate CSS with current palette98+ // Generate CSS with current palette and UI scale
90 palette := m.GetCurrentPalette()99 palette := m.GetCurrentPalette()
91- css := GenerateCSS(palette)100+ css := GenerateCSSWithScale(palette, m.uiScale)
92 101
93 // Create CSS provider if needed102 // Create CSS provider if needed
94 if m.cssProvider == nil {103 if m.cssProvider == nil {

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