detached 2b84462

refactor: address code review feedback and simplify architecture

- Move color palette injection from inline Go to main-world.ts for better maintainability
- Extract magic emoji value to named constant
- Remove shortcuts fallback - fail cleanly if backend unavailable (KISS)
commit
2b84462dc9dff4ac456e76b33011cce7f933eaaf
author
bamen · · 2025-10-06 21:10 UTC
committer
bamen · · 2025-10-06 17:18 UTC
parent
37da1ff
signature
unsigned
5 files · +113 -176
D gui/src/constants/shortcuts.ts +0 -91
@@ -1,91 +0,0 @@
1-/**
2- * Search shortcuts configuration
3- *
4- * @deprecated This file is only used as a fallback. The real source of truth
5- * is the backend Go config (internal/config/defaults.go). Shortcuts should be
6- * fetched from the backend via /api/config or the messaging bridge.
7- *
8- * Only kept for development fallback when backend is unavailable.
9- */
10-
11-export interface SearchShortcutDefinition {
12- shortcut: string;
13- url_template: string;
14- description: string;
15-}
16-
17-/**
18- * Default search shortcuts
19- * Alphabetically ordered by shortcut key
20- */
21-export const DEFAULT_SHORTCUTS: Record<string, SearchShortcutDefinition> = {
22- ddg: {
23- shortcut: "ddg",
24- url_template: "https://duckduckgo.com/?q={query}",
25- description: "DuckDuckGo search",
26- },
27- g: {
28- shortcut: "g",
29- url_template: "https://www.google.com/search?q={query}",
30- description: "Google search",
31- },
32- gh: {
33- shortcut: "gh",
34- url_template: "https://github.com/search?q={query}",
35- description: "GitHub search",
36- },
37- go: {
38- shortcut: "go",
39- url_template: "https://pkg.go.dev/search?q={query}",
40- description: "Go package search",
41- },
42- mdn: {
43- shortcut: "mdn",
44- url_template: "https://developer.mozilla.org/en-US/search?q={query}",
45- description: "MDN Web Docs search",
46- },
47- npm: {
48- shortcut: "npm",
49- url_template: "https://www.npmjs.com/search?q={query}",
50- description: "npm package search",
51- },
52- r: {
53- shortcut: "r",
54- url_template: "https://www.reddit.com/search?q={query}",
55- description: "Reddit search",
56- },
57- so: {
58- shortcut: "so",
59- url_template: "https://stackoverflow.com/search?q={query}",
60- description: "Stack Overflow search",
61- },
62- w: {
63- shortcut: "w",
64- url_template: "https://en.wikipedia.org/wiki/{query}",
65- description: "Wikipedia search",
66- },
67- yt: {
68- shortcut: "yt",
69- url_template: "https://www.youtube.com/results?search_query={query}",
70- description: "YouTube search",
71- },
72-};
73-
74-/**
75- * Get array of shortcut keys for validation
76- */
77-export const SHORTCUT_KEYS = Object.keys(DEFAULT_SHORTCUTS);
78-
79-/**
80- * Check if a string is a valid shortcut
81- */
82-export function isValidShortcut(key: string): boolean {
83- return key in DEFAULT_SHORTCUTS;
84-}
85-
86-/**
87- * Get shortcut description by key
88- */
89-export function getShortcutDescription(key: string): string | null {
90- return DEFAULT_SHORTCUTS[key]?.description ?? null;
91-}
@@ -1,91 +0,0 @@
1-/**
2- * Search shortcuts configuration
3- *
4- * @deprecated This file is only used as a fallback. The real source of truth
5- * is the backend Go config (internal/config/defaults.go). Shortcuts should be
6- * fetched from the backend via /api/config or the messaging bridge.
7- *
8- * Only kept for development fallback when backend is unavailable.
9- */
10-
11-export interface SearchShortcutDefinition {
12- shortcut: string;
13- url_template: string;
14- description: string;
15-}
16-
17-/**
18- * Default search shortcuts
19- * Alphabetically ordered by shortcut key
20- */
21-export const DEFAULT_SHORTCUTS: Record<string, SearchShortcutDefinition> = {
22- ddg: {
23- shortcut: "ddg",
24- url_template: "https://duckduckgo.com/?q={query}",
25- description: "DuckDuckGo search",
26- },
27- g: {
28- shortcut: "g",
29- url_template: "https://www.google.com/search?q={query}",
30- description: "Google search",
31- },
32- gh: {
33- shortcut: "gh",
34- url_template: "https://github.com/search?q={query}",
35- description: "GitHub search",
36- },
37- go: {
38- shortcut: "go",
39- url_template: "https://pkg.go.dev/search?q={query}",
40- description: "Go package search",
41- },
42- mdn: {
43- shortcut: "mdn",
44- url_template: "https://developer.mozilla.org/en-US/search?q={query}",
45- description: "MDN Web Docs search",
46- },
47- npm: {
48- shortcut: "npm",
49- url_template: "https://www.npmjs.com/search?q={query}",
50- description: "npm package search",
51- },
52- r: {
53- shortcut: "r",
54- url_template: "https://www.reddit.com/search?q={query}",
55- description: "Reddit search",
56- },
57- so: {
58- shortcut: "so",
59- url_template: "https://stackoverflow.com/search?q={query}",
60- description: "Stack Overflow search",
61- },
62- w: {
63- shortcut: "w",
64- url_template: "https://en.wikipedia.org/wiki/{query}",
65- description: "Wikipedia search",
66- },
67- yt: {
68- shortcut: "yt",
69- url_template: "https://www.youtube.com/results?search_query={query}",
70- description: "YouTube search",
71- },
72-};
73-
74-/**
75- * Get array of shortcut keys for validation
76- */
77-export const SHORTCUT_KEYS = Object.keys(DEFAULT_SHORTCUTS);
78-
79-/**
80- * Check if a string is a valid shortcut
81- */
82-export function isValidShortcut(key: string): boolean {
83- return key in DEFAULT_SHORTCUTS;
84-}
85-
86-/**
87- * Get shortcut description by key
88- */
89-export function getShortcutDescription(key: string): string | null {
90- return DEFAULT_SHORTCUTS[key]?.description ?? null;
91-}
M gui/src/injected/main-world.ts +78 -0
@@ -20,6 +20,7 @@ declare global {
2020 __dumber_initial_theme?: string;
2121 __dumber_setTheme?: (theme: "light" | "dark") => void;
2222 __dumber_applyPalette?: (theme: "light" | "dark") => void;
23+ __dumber_palette?: { light?: Record<string, string>; dark?: Record<string, string> };
2324 __dumber_applyDomZoom?: (level: number) => void;
2425 __dumber_showToast?: (
2526 message: string,
@@ -119,6 +120,83 @@ function detectWindowType(features?: string | null): string {
119120 window.__dumber_webview_id = "__WEBVIEW_ID__";
120121 window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean;
121122
123+ // Initialize color palette (will be replaced by Go with actual palette JSON)
124+ // Placeholder: __PALETTE_JSON__
125+ try {
126+ const paletteData = "__PALETTE_JSON__";
127+ if (paletteData && paletteData !== "__PALETTE_JSON__") {
128+ window.__dumber_palette = JSON.parse(paletteData);
129+
130+ // Normalize palette keys to lowercase
131+ const normalized: typeof window.__dumber_palette = {};
132+ if (window.__dumber_palette) {
133+ for (const key in window.__dumber_palette) {
134+ if (Object.prototype.hasOwnProperty.call(window.__dumber_palette, key)) {
135+ normalized[key.toLowerCase()] = window.__dumber_palette[key];
136+ }
137+ }
138+ window.__dumber_palette = normalized;
139+ }
140+ }
141+ } catch (err) {
142+ console.error("[dumber] Failed to parse palette JSON", err);
143+ }
144+
145+ // Palette application function
146+ window.__dumber_applyPalette = (theme: "light" | "dark") => {
147+ try {
148+ const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.light;
149+ if (!palette) {
150+ return;
151+ }
152+
153+ const tokenMap: Record<string, string> = {
154+ Background: "--color-browser-bg",
155+ Surface: "--color-browser-surface",
156+ SurfaceVariant: "--color-browser-surface-variant",
157+ Text: "--color-browser-text",
158+ Muted: "--color-browser-muted",
159+ Accent: "--color-browser-accent",
160+ Border: "--color-browser-border",
161+ };
162+
163+ const dynamicMap: Record<string, string> = {
164+ Background: "--dynamic-bg",
165+ Surface: "--dynamic-surface",
166+ SurfaceVariant: "--dynamic-surface-variant",
167+ Text: "--dynamic-text",
168+ Muted: "--dynamic-muted",
169+ Accent: "--dynamic-accent",
170+ Border: "--dynamic-border",
171+ };
172+
173+ const root = document.documentElement;
174+ for (const key in tokenMap) {
175+ if (Object.prototype.hasOwnProperty.call(tokenMap, key) && palette[key]) {
176+ root.style.setProperty(tokenMap[key], palette[key]);
177+ if (Object.prototype.hasOwnProperty.call(dynamicMap, key)) {
178+ root.style.setProperty(dynamicMap[key], palette[key]);
179+ }
180+ }
181+ }
182+
183+ document.dispatchEvent(
184+ new CustomEvent("dumber:palette-updated", { detail: { theme } })
185+ );
186+ } catch (err) {
187+ console.error("[dumber] Failed to apply palette", err);
188+ }
189+ };
190+
191+ // Apply initial palette based on theme
192+ try {
193+ const initialTheme = document.documentElement.classList.contains("dark") ? "dark" : "light";
194+ window.__dumber_initial_theme = window.__dumber_initial_theme || initialTheme;
195+ window.__dumber_applyPalette(window.__dumber_initial_theme);
196+ } catch (err) {
197+ console.error("[dumber] Failed to apply initial palette", err);
198+ }
199+
122200 // Theme setter function for GTK theme integration
123201 window.__dumber_setTheme = (theme: "light" | "dark") => {
124202 window.__dumber_initial_theme = theme;
@@ -20,6 +20,7 @@ declare global {
20 __dumber_initial_theme?: string;20 __dumber_initial_theme?: string;
21 __dumber_setTheme?: (theme: "light" | "dark") => void;21 __dumber_setTheme?: (theme: "light" | "dark") => void;
22 __dumber_applyPalette?: (theme: "light" | "dark") => void;22 __dumber_applyPalette?: (theme: "light" | "dark") => void;
23+ __dumber_palette?: { light?: Record<string, string>; dark?: Record<string, string> };
23 __dumber_applyDomZoom?: (level: number) => void;24 __dumber_applyDomZoom?: (level: number) => void;
24 __dumber_showToast?: (25 __dumber_showToast?: (
25 message: string,26 message: string,
@@ -119,6 +120,83 @@ function detectWindowType(features?: string | null): string {
119 window.__dumber_webview_id = "__WEBVIEW_ID__";120 window.__dumber_webview_id = "__WEBVIEW_ID__";
120 window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean;121 window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean;
121 122
123+ // Initialize color palette (will be replaced by Go with actual palette JSON)
124+ // Placeholder: __PALETTE_JSON__
125+ try {
126+ const paletteData = "__PALETTE_JSON__";
127+ if (paletteData && paletteData !== "__PALETTE_JSON__") {
128+ window.__dumber_palette = JSON.parse(paletteData);
129+
130+ // Normalize palette keys to lowercase
131+ const normalized: typeof window.__dumber_palette = {};
132+ if (window.__dumber_palette) {
133+ for (const key in window.__dumber_palette) {
134+ if (Object.prototype.hasOwnProperty.call(window.__dumber_palette, key)) {
135+ normalized[key.toLowerCase()] = window.__dumber_palette[key];
136+ }
137+ }
138+ window.__dumber_palette = normalized;
139+ }
140+ }
141+ } catch (err) {
142+ console.error("[dumber] Failed to parse palette JSON", err);
143+ }
144+
145+ // Palette application function
146+ window.__dumber_applyPalette = (theme: "light" | "dark") => {
147+ try {
148+ const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.light;
149+ if (!palette) {
150+ return;
151+ }
152+
153+ const tokenMap: Record<string, string> = {
154+ Background: "--color-browser-bg",
155+ Surface: "--color-browser-surface",
156+ SurfaceVariant: "--color-browser-surface-variant",
157+ Text: "--color-browser-text",
158+ Muted: "--color-browser-muted",
159+ Accent: "--color-browser-accent",
160+ Border: "--color-browser-border",
161+ };
162+
163+ const dynamicMap: Record<string, string> = {
164+ Background: "--dynamic-bg",
165+ Surface: "--dynamic-surface",
166+ SurfaceVariant: "--dynamic-surface-variant",
167+ Text: "--dynamic-text",
168+ Muted: "--dynamic-muted",
169+ Accent: "--dynamic-accent",
170+ Border: "--dynamic-border",
171+ };
172+
173+ const root = document.documentElement;
174+ for (const key in tokenMap) {
175+ if (Object.prototype.hasOwnProperty.call(tokenMap, key) && palette[key]) {
176+ root.style.setProperty(tokenMap[key], palette[key]);
177+ if (Object.prototype.hasOwnProperty.call(dynamicMap, key)) {
178+ root.style.setProperty(dynamicMap[key], palette[key]);
179+ }
180+ }
181+ }
182+
183+ document.dispatchEvent(
184+ new CustomEvent("dumber:palette-updated", { detail: { theme } })
185+ );
186+ } catch (err) {
187+ console.error("[dumber] Failed to apply palette", err);
188+ }
189+ };
190+
191+ // Apply initial palette based on theme
192+ try {
193+ const initialTheme = document.documentElement.classList.contains("dark") ? "dark" : "light";
194+ window.__dumber_initial_theme = window.__dumber_initial_theme || initialTheme;
195+ window.__dumber_applyPalette(window.__dumber_initial_theme);
196+ } catch (err) {
197+ console.error("[dumber] Failed to apply initial palette", err);
198+ }
199+
122 // Theme setter function for GTK theme integration200 // Theme setter function for GTK theme integration
123 window.__dumber_setTheme = (theme: "light" | "dark") => {201 window.__dumber_setTheme = (theme: "light" | "dark") => {
124 window.__dumber_initial_theme = theme;202 window.__dumber_initial_theme = theme;
M gui/src/services/app.ts +14 -32
@@ -1,5 +1,4 @@
11 import type { HistoryEntry, SearchShortcut } from "../types/generated.js";
2-import { DEFAULT_SHORTCUTS } from "../constants/shortcuts.js";
32
43 export class AppService {
54 private history: HistoryEntry[] = [];
@@ -80,38 +79,21 @@ export class AppService {
8079 }
8180
8281 async loadShortcuts(): Promise<void> {
83- try {
84- const cfg = await fetch("/api/config").then((r) => r.json());
85- const raw = cfg?.search_shortcuts || {};
86- // Normalize field casing from backend (supports URL/Description and url/description)
87- const normalized: Record<string, SearchShortcut> = {};
88- for (const [key, value] of Object.entries(raw)) {
89- const v = value as Record<string, unknown>;
90- normalized[key] = {
91- id: 0,
92- shortcut: key,
93- url_template: (v.url ?? v.URL ?? "") as string,
94- description: (v.description ?? v.Description ?? "") as string,
95- created_at: null,
96- };
97- }
98- this.shortcuts = normalized;
99- } catch (error) {
100- console.error("Failed to load shortcuts:", error);
101- // Mock shortcuts for development/fallback using shared constants
102- this.shortcuts = Object.fromEntries(
103- Object.entries(DEFAULT_SHORTCUTS).map(([key, def]) => [
104- key,
105- {
106- id: 0,
107- shortcut: def.shortcut,
108- url_template: def.url_template,
109- description: def.description,
110- created_at: null,
111- },
112- ])
113- );
82+ const cfg = await fetch("/api/config").then((r) => r.json());
83+ const raw = cfg?.search_shortcuts || {};
84+ // Normalize field casing from backend (supports URL/Description and url/description)
85+ const normalized: Record<string, SearchShortcut> = {};
86+ for (const [key, value] of Object.entries(raw)) {
87+ const v = value as Record<string, unknown>;
88+ normalized[key] = {
89+ id: 0,
90+ shortcut: key,
91+ url_template: (v.url ?? v.URL ?? "") as string,
92+ description: (v.description ?? v.Description ?? "") as string,
93+ created_at: null,
94+ };
11495 }
96+ this.shortcuts = normalized;
11597 }
11698
11799 getHistory(): HistoryEntry[] {
@@ -1,5 +1,4 @@
1 import type { HistoryEntry, SearchShortcut } from "../types/generated.js";1 import type { HistoryEntry, SearchShortcut } from "../types/generated.js";
2-import { DEFAULT_SHORTCUTS } from "../constants/shortcuts.js";
3 2
4 export class AppService {3 export class AppService {
5 private history: HistoryEntry[] = [];4 private history: HistoryEntry[] = [];
@@ -80,38 +79,21 @@ export class AppService {
80 }79 }
81 80
82 async loadShortcuts(): Promise<void> {81 async loadShortcuts(): Promise<void> {
83- try {82+ const cfg = await fetch("/api/config").then((r) => r.json());
84- const cfg = await fetch("/api/config").then((r) => r.json());83+ const raw = cfg?.search_shortcuts || {};
85- const raw = cfg?.search_shortcuts || {};84+ // Normalize field casing from backend (supports URL/Description and url/description)
86- // Normalize field casing from backend (supports URL/Description and url/description)85+ const normalized: Record<string, SearchShortcut> = {};
87- const normalized: Record<string, SearchShortcut> = {};86+ for (const [key, value] of Object.entries(raw)) {
88- for (const [key, value] of Object.entries(raw)) {87+ const v = value as Record<string, unknown>;
89- const v = value as Record<string, unknown>;88+ normalized[key] = {
90- normalized[key] = {89+ id: 0,
91- id: 0,90+ shortcut: key,
92- shortcut: key,91+ url_template: (v.url ?? v.URL ?? "") as string,
93- url_template: (v.url ?? v.URL ?? "") as string,92+ description: (v.description ?? v.Description ?? "") as string,
94- description: (v.description ?? v.Description ?? "") as string,93+ created_at: null,
95- created_at: null,94+ };
96- };
97- }
98- this.shortcuts = normalized;
99- } catch (error) {
100- console.error("Failed to load shortcuts:", error);
101- // Mock shortcuts for development/fallback using shared constants
102- this.shortcuts = Object.fromEntries(
103- Object.entries(DEFAULT_SHORTCUTS).map(([key, def]) => [
104- key,
105- {
106- id: 0,
107- shortcut: def.shortcut,
108- url_template: def.url_template,
109- description: def.description,
110- created_at: null,
111- },
112- ])
113- );
114 }95 }
96+ this.shortcuts = normalized;
115 }97 }
116 98
117 getHistory(): HistoryEntry[] {99 getHistory(): HistoryEntry[] {
M internal/app/browser/stacked_panes.go +6 -1
@@ -12,6 +12,11 @@ import (
1212 "github.com/bnema/dumber/pkg/webkit"
1313 )
1414
15+const (
16+ // faviconPlaceholder is the emoji displayed when no favicon is available
17+ faviconPlaceholder = "🌐"
18+)
19+
1520 // StackedPaneManager handles all stacked pane operations
1621 type StackedPaneManager struct {
1722 wm *WorkspaceManager
@@ -711,7 +716,7 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL str
711716 webkit.WidgetShow(faviconImg)
712717 } else {
713718 // DEBUG: Add visible placeholder label to show code is running
714- placeholderLabel := webkit.NewLabel("🌐")
719+ placeholderLabel := webkit.NewLabel(faviconPlaceholder)
715720 if placeholderLabel != 0 {
716721 webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder")
717722 webkit.BoxAppend(titleBox, placeholderLabel)
@@ -12,6 +12,11 @@ import (
12 "github.com/bnema/dumber/pkg/webkit"12 "github.com/bnema/dumber/pkg/webkit"
13 )13 )
14 14
15+const (
16+ // faviconPlaceholder is the emoji displayed when no favicon is available
17+ faviconPlaceholder = "🌐"
18+)
19+
15 // StackedPaneManager handles all stacked pane operations20 // StackedPaneManager handles all stacked pane operations
16 type StackedPaneManager struct {21 type StackedPaneManager struct {
17 wm *WorkspaceManager22 wm *WorkspaceManager
@@ -711,7 +716,7 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL str
711 webkit.WidgetShow(faviconImg)716 webkit.WidgetShow(faviconImg)
712 } else {717 } else {
713 // DEBUG: Add visible placeholder label to show code is running718 // DEBUG: Add visible placeholder label to show code is running
714- placeholderLabel := webkit.NewLabel("🌐")719+ placeholderLabel := webkit.NewLabel(faviconPlaceholder)
715 if placeholderLabel != 0 {720 if placeholderLabel != 0 {
716 webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder")721 webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder")
717 webkit.BoxAppend(titleBox, placeholderLabel)722 webkit.BoxAppend(titleBox, placeholderLabel)
M pkg/webkit/webview_cgo.go +15 -52
@@ -2061,58 +2061,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
20612061 C.webkit_user_script_unref(schemeScript)
20622062 }
20632063
2064- // Inject color palette tokens sourced from Go configuration
2065- if cfg != nil {
2066- if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 {
2067- paletteScript := fmt.Sprintf(`(function(){try{
2068- window.__dumber_palette=%s;
2069- var normalized={};
2070- if(window.__dumber_palette){
2071- for (var key in window.__dumber_palette){
2072- if(Object.prototype.hasOwnProperty.call(window.__dumber_palette,key)){
2073- normalized[key.toLowerCase()] = window.__dumber_palette[key];
2074- }
2075- }
2076- window.__dumber_palette = normalized;
2077- }
2078- var tokenMap={Background:'--color-browser-bg',Surface:'--color-browser-surface',SurfaceVariant:'--color-browser-surface-variant',Text:'--color-browser-text',Muted:'--color-browser-muted',Accent:'--color-browser-accent',Border:'--color-browser-border'};
2079- var dynamicMap={Background:'--dynamic-bg',Surface:'--dynamic-surface',SurfaceVariant:'--dynamic-surface-variant',Text:'--dynamic-text',Muted:'--dynamic-muted',Accent:'--dynamic-accent',Border:'--dynamic-border'};
2080- var apply=function(theme){
2081- var palette=(window.__dumber_palette && (window.__dumber_palette[theme] || window.__dumber_palette.light)) || null;
2082- if(!palette){return;}
2083- var root=document.documentElement;
2084- for(var key in tokenMap){
2085- if(Object.prototype.hasOwnProperty.call(tokenMap,key) && palette[key]){
2086- root.style.setProperty(tokenMap[key], palette[key]);
2087- if(Object.prototype.hasOwnProperty.call(dynamicMap,key)){
2088- root.style.setProperty(dynamicMap[key], palette[key]);
2089- }
2090- }
2091- }
2092- document.dispatchEvent(new CustomEvent('dumber:palette-updated',{detail:{theme:theme}}));
2093- };
2094- window.__dumber_applyPalette=function(theme){
2095- apply(theme==='dark'?'dark':'light');
2096- };
2097- var initial=document.documentElement.classList.contains('dark')?'dark':'light';
2098- if(window.__dumber_initial_theme){
2099- initial=window.__dumber_initial_theme;
2100- }
2101- apply(initial);
2102-}catch(err){console.error('[dumber] Failed to apply palette',err);}})();`, string(paletteJSON))
2103- cPalette := C.CString(paletteScript)
2104- defer C.free(unsafe.Pointer(cPalette))
2105- paletteUserScript := C.webkit_user_script_new((*C.gchar)(cPalette), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
2106- if paletteUserScript != nil {
2107- C.webkit_user_content_manager_add_script(w.native.ucm, paletteUserScript)
2108- C.webkit_user_script_unref(paletteUserScript)
2109- } else {
2110- log.Printf("[webkit] Failed to create palette user script")
2111- }
2112- } else if err != nil {
2113- log.Printf("[webkit] Failed to marshal color palettes: %v", err)
2114- }
2115- }
2064+ // Color palette injection now handled in main-world.ts via __PALETTE_JSON__ placeholder
21162065
21172066 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
21182067 if cfg != nil && cfg.Assets != nil {
@@ -2263,6 +2212,20 @@ func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w
22632212 log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState)
22642213 script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)
22652214
2215+ // Inject color palette JSON
2216+ if cfg != nil {
2217+ if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 {
2218+ script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", string(paletteJSON))
2219+ } else {
2220+ if err != nil {
2221+ log.Printf("[webkit] Failed to marshal color palettes: %v", err)
2222+ }
2223+ script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null")
2224+ }
2225+ } else {
2226+ script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null")
2227+ }
2228+
22662229 log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
22672230 return script
22682231 }
@@ -2061,58 +2061,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
2061 C.webkit_user_script_unref(schemeScript)2061 C.webkit_user_script_unref(schemeScript)
2062 }2062 }
2063 2063
2064- // Inject color palette tokens sourced from Go configuration2064+ // Color palette injection now handled in main-world.ts via __PALETTE_JSON__ placeholder
2065- if cfg != nil {
2066- if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 {
2067- paletteScript := fmt.Sprintf(`(function(){try{
2068- window.__dumber_palette=%s;
2069- var normalized={};
2070- if(window.__dumber_palette){
2071- for (var key in window.__dumber_palette){
2072- if(Object.prototype.hasOwnProperty.call(window.__dumber_palette,key)){
2073- normalized[key.toLowerCase()] = window.__dumber_palette[key];
2074- }
2075- }
2076- window.__dumber_palette = normalized;
2077- }
2078- var tokenMap={Background:'--color-browser-bg',Surface:'--color-browser-surface',SurfaceVariant:'--color-browser-surface-variant',Text:'--color-browser-text',Muted:'--color-browser-muted',Accent:'--color-browser-accent',Border:'--color-browser-border'};
2079- var dynamicMap={Background:'--dynamic-bg',Surface:'--dynamic-surface',SurfaceVariant:'--dynamic-surface-variant',Text:'--dynamic-text',Muted:'--dynamic-muted',Accent:'--dynamic-accent',Border:'--dynamic-border'};
2080- var apply=function(theme){
2081- var palette=(window.__dumber_palette && (window.__dumber_palette[theme] || window.__dumber_palette.light)) || null;
2082- if(!palette){return;}
2083- var root=document.documentElement;
2084- for(var key in tokenMap){
2085- if(Object.prototype.hasOwnProperty.call(tokenMap,key) && palette[key]){
2086- root.style.setProperty(tokenMap[key], palette[key]);
2087- if(Object.prototype.hasOwnProperty.call(dynamicMap,key)){
2088- root.style.setProperty(dynamicMap[key], palette[key]);
2089- }
2090- }
2091- }
2092- document.dispatchEvent(new CustomEvent('dumber:palette-updated',{detail:{theme:theme}}));
2093- };
2094- window.__dumber_applyPalette=function(theme){
2095- apply(theme==='dark'?'dark':'light');
2096- };
2097- var initial=document.documentElement.classList.contains('dark')?'dark':'light';
2098- if(window.__dumber_initial_theme){
2099- initial=window.__dumber_initial_theme;
2100- }
2101- apply(initial);
2102-}catch(err){console.error('[dumber] Failed to apply palette',err);}})();`, string(paletteJSON))
2103- cPalette := C.CString(paletteScript)
2104- defer C.free(unsafe.Pointer(cPalette))
2105- paletteUserScript := C.webkit_user_script_new((*C.gchar)(cPalette), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil)
2106- if paletteUserScript != nil {
2107- C.webkit_user_content_manager_add_script(w.native.ucm, paletteUserScript)
2108- C.webkit_user_script_unref(paletteUserScript)
2109- } else {
2110- log.Printf("[webkit] Failed to create palette user script")
2111- }
2112- } else if err != nil {
2113- log.Printf("[webkit] Failed to marshal color palettes: %v", err)
2114- }
2115- }
2116 2065
2117 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)2066 // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls)
2118 if cfg != nil && cfg.Assets != nil {2067 if cfg != nil && cfg.Assets != nil {
@@ -2263,6 +2212,20 @@ func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w
2263 log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState)2212 log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState)
2264 script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)2213 script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)
2265 2214
2215+ // Inject color palette JSON
2216+ if cfg != nil {
2217+ if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 {
2218+ script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", string(paletteJSON))
2219+ } else {
2220+ if err != nil {
2221+ log.Printf("[webkit] Failed to marshal color palettes: %v", err)
2222+ }
2223+ script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null")
2224+ }
2225+ } else {
2226+ script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null")
2227+ }
2228+
2266 log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))2229 log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
2267 return script2230 return script
2268 }2231 }

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