detached 2cbda56

feat(config): use messaging bridge for config instead of fetch/CORS

- Add get_search_shortcuts and get_color_palettes message handlers
- Request config via webkit.messageHandlers.dumber bridge
- Remove /api/config endpoint (avoids CORS issues with custom schemes)
- Remove __PALETTE_JSON__ placeholder injection
- Frontend requests config on-demand via messaging callbacks
commit
2cbda566a2676f98cbd4dbecdf81d764a5694db0
author
bamen · · 2025-10-06 17:04 UTC
committer
bamen · · 2025-10-06 18:48 UTC
parent
a01c57b
signature
unsigned
7 files · +179 -99
M gui/src/injected/main-world.ts +54 -30
@@ -20,7 +20,11 @@ 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> };
23+ __dumber_palette?: Record<string, Record<string, string>>;
24+ __dumber_color_palettes?: (palettes: unknown) => void;
25+ __dumber_color_palettes_error?: (error: string) => void;
26+ __dumber_search_shortcuts?: (data: unknown) => void;
27+ __dumber_search_shortcuts_error?: (error: string) => void;
2428 __dumber_applyDomZoom?: (level: number) => void;
2529 __dumber_showToast?: (
2630 message: string,
@@ -120,32 +124,58 @@ function detectWindowType(features?: string | null): string {
120124 window.__dumber_webview_id = "__WEBVIEW_ID__";
121125 window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean;
122126
123- // Initialize color palette (will be replaced by Go with actual palette JSON)
124- // Placeholder: __PALETTE_JSON__
127+ // Request color palettes from Go backend
125128 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];
129+ const bridge = window.webkit?.messageHandlers?.dumber;
130+ if (bridge && typeof bridge.postMessage === "function") {
131+ // Set up callback handler before requesting
132+ window.__dumber_color_palettes = (palettes: unknown) => {
133+ try {
134+ // Type guard to ensure palettes is a valid object
135+ if (typeof palettes !== "object" || palettes === null) {
136+ console.error("[dumber] Invalid palettes received");
137+ return;
138+ }
139+
140+ window.__dumber_palette = palettes as Record<string, Record<string, string>>;
141+
142+ // Normalize palette keys to lowercase
143+ const normalized: Record<string, Record<string, string>> = {};
144+ if (window.__dumber_palette) {
145+ for (const key in window.__dumber_palette) {
146+ if (Object.prototype.hasOwnProperty.call(window.__dumber_palette, key)) {
147+ normalized[key.toLowerCase()] = window.__dumber_palette[key];
148+ }
149+ }
150+ window.__dumber_palette = normalized;
151+ }
152+
153+ // Apply palette now that we have it
154+ const initialTheme: "light" | "dark" = document.documentElement.classList.contains("dark") ? "dark" : "light";
155+ window.__dumber_initial_theme = initialTheme;
156+ if (window.__dumber_applyPalette) {
157+ window.__dumber_applyPalette(initialTheme);
136158 }
159+ } catch (err) {
160+ console.error("[dumber] Failed to apply received palette", err);
137161 }
138- window.__dumber_palette = normalized;
139- }
162+ };
163+
164+ window.__dumber_color_palettes_error = (error: string) => {
165+ console.error("[dumber] Failed to get color palettes:", error);
166+ };
167+
168+ // Request color palettes
169+ bridge.postMessage(JSON.stringify({ type: "get_color_palettes" }));
140170 }
141171 } catch (err) {
142- console.error("[dumber] Failed to parse palette JSON", err);
172+ console.error("[dumber] Failed to request color palettes", err);
143173 }
144174
145175 // Palette application function
146176 window.__dumber_applyPalette = (theme: "light" | "dark") => {
147177 try {
148- const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.light;
178+ const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.["light"];
149179 if (!palette) {
150180 return;
151181 }
@@ -172,10 +202,13 @@ function detectWindowType(features?: string | null): string {
172202
173203 const root = document.documentElement;
174204 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]);
205+ if (Object.prototype.hasOwnProperty.call(tokenMap, key)) {
206+ const colorValue = palette[key];
207+ if (colorValue) {
208+ root.style.setProperty(tokenMap[key], colorValue);
209+ if (Object.prototype.hasOwnProperty.call(dynamicMap, key)) {
210+ root.style.setProperty(dynamicMap[key], colorValue);
211+ }
179212 }
180213 }
181214 }
@@ -188,15 +221,6 @@ function detectWindowType(features?: string | null): string {
188221 }
189222 };
190223
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-
200224 // Theme setter function for GTK theme integration
201225 window.__dumber_setTheme = (theme: "light" | "dark") => {
202226 window.__dumber_initial_theme = theme;
@@ -20,7 +20,11 @@ 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_palette?: Record<string, Record<string, string>>;
24+ __dumber_color_palettes?: (palettes: unknown) => void;
25+ __dumber_color_palettes_error?: (error: string) => void;
26+ __dumber_search_shortcuts?: (data: unknown) => void;
27+ __dumber_search_shortcuts_error?: (error: string) => void;
24 __dumber_applyDomZoom?: (level: number) => void;28 __dumber_applyDomZoom?: (level: number) => void;
25 __dumber_showToast?: (29 __dumber_showToast?: (
26 message: string,30 message: string,
@@ -120,32 +124,58 @@ function detectWindowType(features?: string | null): string {
120 window.__dumber_webview_id = "__WEBVIEW_ID__";124 window.__dumber_webview_id = "__WEBVIEW_ID__";
121 window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean;125 window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean;
122 126
123- // Initialize color palette (will be replaced by Go with actual palette JSON)127+ // Request color palettes from Go backend
124- // Placeholder: __PALETTE_JSON__
125 try {128 try {
126- const paletteData = "__PALETTE_JSON__";129+ const bridge = window.webkit?.messageHandlers?.dumber;
127- if (paletteData && paletteData !== "__PALETTE_JSON__") {130+ if (bridge && typeof bridge.postMessage === "function") {
128- window.__dumber_palette = JSON.parse(paletteData);131+ // Set up callback handler before requesting
129-132+ window.__dumber_color_palettes = (palettes: unknown) => {
130- // Normalize palette keys to lowercase133+ try {
131- const normalized: typeof window.__dumber_palette = {};134+ // Type guard to ensure palettes is a valid object
132- if (window.__dumber_palette) {135+ if (typeof palettes !== "object" || palettes === null) {
133- for (const key in window.__dumber_palette) {136+ console.error("[dumber] Invalid palettes received");
134- if (Object.prototype.hasOwnProperty.call(window.__dumber_palette, key)) {137+ return;
135- normalized[key.toLowerCase()] = window.__dumber_palette[key];138+ }
139+
140+ window.__dumber_palette = palettes as Record<string, Record<string, string>>;
141+
142+ // Normalize palette keys to lowercase
143+ const normalized: Record<string, Record<string, string>> = {};
144+ if (window.__dumber_palette) {
145+ for (const key in window.__dumber_palette) {
146+ if (Object.prototype.hasOwnProperty.call(window.__dumber_palette, key)) {
147+ normalized[key.toLowerCase()] = window.__dumber_palette[key];
148+ }
149+ }
150+ window.__dumber_palette = normalized;
151+ }
152+
153+ // Apply palette now that we have it
154+ const initialTheme: "light" | "dark" = document.documentElement.classList.contains("dark") ? "dark" : "light";
155+ window.__dumber_initial_theme = initialTheme;
156+ if (window.__dumber_applyPalette) {
157+ window.__dumber_applyPalette(initialTheme);
136 }158 }
159+ } catch (err) {
160+ console.error("[dumber] Failed to apply received palette", err);
137 }161 }
138- window.__dumber_palette = normalized;162+ };
139- }163+
164+ window.__dumber_color_palettes_error = (error: string) => {
165+ console.error("[dumber] Failed to get color palettes:", error);
166+ };
167+
168+ // Request color palettes
169+ bridge.postMessage(JSON.stringify({ type: "get_color_palettes" }));
140 }170 }
141 } catch (err) {171 } catch (err) {
142- console.error("[dumber] Failed to parse palette JSON", err);172+ console.error("[dumber] Failed to request color palettes", err);
143 }173 }
144 174
145 // Palette application function175 // Palette application function
146 window.__dumber_applyPalette = (theme: "light" | "dark") => {176 window.__dumber_applyPalette = (theme: "light" | "dark") => {
147 try {177 try {
148- const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.light;178+ const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.["light"];
149 if (!palette) {179 if (!palette) {
150 return;180 return;
151 }181 }
@@ -172,10 +202,13 @@ function detectWindowType(features?: string | null): string {
172 202
173 const root = document.documentElement;203 const root = document.documentElement;
174 for (const key in tokenMap) {204 for (const key in tokenMap) {
175- if (Object.prototype.hasOwnProperty.call(tokenMap, key) && palette[key]) {205+ if (Object.prototype.hasOwnProperty.call(tokenMap, key)) {
176- root.style.setProperty(tokenMap[key], palette[key]);206+ const colorValue = palette[key];
177- if (Object.prototype.hasOwnProperty.call(dynamicMap, key)) {207+ if (colorValue) {
178- root.style.setProperty(dynamicMap[key], palette[key]);208+ root.style.setProperty(tokenMap[key], colorValue);
209+ if (Object.prototype.hasOwnProperty.call(dynamicMap, key)) {
210+ root.style.setProperty(dynamicMap[key], colorValue);
211+ }
179 }212 }
180 }213 }
181 }214 }
@@ -188,15 +221,6 @@ function detectWindowType(features?: string | null): string {
188 }221 }
189 };222 };
190 223
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-
200 // Theme setter function for GTK theme integration224 // Theme setter function for GTK theme integration
201 window.__dumber_setTheme = (theme: "light" | "dark") => {225 window.__dumber_setTheme = (theme: "light" | "dark") => {
202 window.__dumber_initial_theme = theme;226 window.__dumber_initial_theme = theme;
M gui/src/services/app.ts +41 -14
@@ -79,21 +79,48 @@ export class AppService {
7979 }
8080
8181 async loadShortcuts(): Promise<void> {
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,
82+ return new Promise((resolve, reject) => {
83+ // Set up response handler
84+ window.__dumber_search_shortcuts = (data: unknown) => {
85+ // Type guard
86+ if (typeof data !== "object" || data === null) {
87+ reject(new Error("Invalid shortcuts data"));
88+ return;
89+ }
90+ const dataObj = data as Record<string, unknown>;
91+ // Normalize field casing from backend (supports URL/Description and url/description)
92+ const normalized: Record<string, SearchShortcut> = {};
93+ for (const [key, value] of Object.entries(dataObj)) {
94+ const v = value as Record<string, unknown>;
95+ normalized[key] = {
96+ id: 0,
97+ shortcut: key,
98+ url_template: (v.url ?? v.URL ?? "") as string,
99+ description: (v.description ?? v.Description ?? "") as string,
100+ created_at: null,
101+ };
102+ }
103+ this.shortcuts = normalized;
104+ resolve();
94105 };
95- }
96- this.shortcuts = normalized;
106+
107+ window.__dumber_search_shortcuts_error = (error: string) => {
108+ console.error("Failed to load shortcuts:", error);
109+ reject(new Error(error));
110+ };
111+
112+ // Send message to Go backend
113+ const bridge = window.webkit?.messageHandlers?.dumber;
114+ if (bridge && typeof bridge.postMessage === "function") {
115+ bridge.postMessage(
116+ JSON.stringify({
117+ type: "get_search_shortcuts",
118+ }),
119+ );
120+ } else {
121+ reject(new Error("WebKit message handler not available"));
122+ }
123+ });
97124 }
98125
99126 getHistory(): HistoryEntry[] {
@@ -79,21 +79,48 @@ export class AppService {
79 }79 }
80 80
81 async loadShortcuts(): Promise<void> {81 async loadShortcuts(): Promise<void> {
82- const cfg = await fetch("/api/config").then((r) => r.json());82+ return new Promise((resolve, reject) => {
83- const raw = cfg?.search_shortcuts || {};83+ // Set up response handler
84- // Normalize field casing from backend (supports URL/Description and url/description)84+ window.__dumber_search_shortcuts = (data: unknown) => {
85- const normalized: Record<string, SearchShortcut> = {};85+ // Type guard
86- for (const [key, value] of Object.entries(raw)) {86+ if (typeof data !== "object" || data === null) {
87- const v = value as Record<string, unknown>;87+ reject(new Error("Invalid shortcuts data"));
88- normalized[key] = {88+ return;
89- id: 0,89+ }
90- shortcut: key,90+ const dataObj = data as Record<string, unknown>;
91- url_template: (v.url ?? v.URL ?? "") as string,91+ // Normalize field casing from backend (supports URL/Description and url/description)
92- description: (v.description ?? v.Description ?? "") as string,92+ const normalized: Record<string, SearchShortcut> = {};
93- created_at: null,93+ for (const [key, value] of Object.entries(dataObj)) {
94+ const v = value as Record<string, unknown>;
95+ normalized[key] = {
96+ id: 0,
97+ shortcut: key,
98+ url_template: (v.url ?? v.URL ?? "") as string,
99+ description: (v.description ?? v.Description ?? "") as string,
100+ created_at: null,
101+ };
102+ }
103+ this.shortcuts = normalized;
104+ resolve();
94 };105 };
95- }106+
96- this.shortcuts = normalized;107+ window.__dumber_search_shortcuts_error = (error: string) => {
108+ console.error("Failed to load shortcuts:", error);
109+ reject(new Error(error));
110+ };
111+
112+ // Send message to Go backend
113+ const bridge = window.webkit?.messageHandlers?.dumber;
114+ if (bridge && typeof bridge.postMessage === "function") {
115+ bridge.postMessage(
116+ JSON.stringify({
117+ type: "get_search_shortcuts",
118+ }),
119+ );
120+ } else {
121+ reject(new Error("WebKit message handler not available"));
122+ }
123+ });
97 }124 }
98 125
99 getHistory(): HistoryEntry[] {126 getHistory(): HistoryEntry[] {
M internal/app/api/scheme.go +0 -41
@@ -2,7 +2,6 @@ package api
22
33 import (
44 "embed"
5- "encoding/json"
65 "log"
76 "mime"
87 neturl "net/url"
@@ -19,7 +18,6 @@ type SchemeHandler struct {
1918 assets embed.FS
2019 parserService *services.ParserService
2120 browserService *services.BrowserService
22- config *config.Config
2321 }
2422
2523 // NewSchemeHandler creates a new scheme handler
@@ -27,13 +25,11 @@ func NewSchemeHandler(
2725 assets embed.FS,
2826 parserService *services.ParserService,
2927 browserService *services.BrowserService,
30- cfg *config.Config,
3128 ) *SchemeHandler {
3229 return &SchemeHandler{
3330 assets: assets,
3431 parserService: parserService,
3532 browserService: browserService,
36- config: cfg,
3733 }
3834 }
3935
@@ -56,11 +52,6 @@ func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte,
5652
5753 // handleAsset serves static assets from embedded filesystem
5854 func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
59- // Check if this is an API route
60- if strings.HasPrefix(u.Path, "/api/") {
61- return s.handleAPI(u)
62- }
63-
6455 // Resolve target path inside embed FS
6556 var rel string
6657 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
@@ -110,38 +101,6 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
110101 return mt, data, true
111102 }
112103
113-// handleAPI handles API routes
114-func (s *SchemeHandler) handleAPI(u *neturl.URL) (string, []byte, bool) {
115- switch u.Path {
116- case "/api/config":
117- return s.handleConfigAPI()
118- default:
119- log.Printf("[scheme] API route not found: %s", u.Path)
120- return "", nil, false
121- }
122-}
123-
124-// handleConfigAPI returns the application configuration as JSON
125-func (s *SchemeHandler) handleConfigAPI() (string, []byte, bool) {
126- if s.config == nil {
127- log.Printf("[scheme] config is nil, cannot serve /api/config")
128- return "", nil, false
129- }
130-
131- // Create a response with search shortcuts
132- response := map[string]interface{}{
133- "search_shortcuts": s.config.SearchShortcuts,
134- }
135-
136- data, err := json.Marshal(response)
137- if err != nil {
138- log.Printf("[scheme] failed to marshal config: %v", err)
139- return "", nil, false
140- }
141-
142- return "application/json", data, true
143-}
144-
145104 // getMimeType determines the MIME type for a given file path
146105 func (s *SchemeHandler) getMimeType(filename string) string {
147106 ext := strings.ToLower(filepath.Ext(filename))
@@ -2,7 +2,6 @@ package api
2 2
3 import (3 import (
4 "embed"4 "embed"
5- "encoding/json"
6 "log"5 "log"
7 "mime"6 "mime"
8 neturl "net/url"7 neturl "net/url"
@@ -19,7 +18,6 @@ type SchemeHandler struct {
19 assets embed.FS18 assets embed.FS
20 parserService *services.ParserService19 parserService *services.ParserService
21 browserService *services.BrowserService20 browserService *services.BrowserService
22- config *config.Config
23 }21 }
24 22
25 // NewSchemeHandler creates a new scheme handler23 // NewSchemeHandler creates a new scheme handler
@@ -27,13 +25,11 @@ func NewSchemeHandler(
27 assets embed.FS,25 assets embed.FS,
28 parserService *services.ParserService,26 parserService *services.ParserService,
29 browserService *services.BrowserService,27 browserService *services.BrowserService,
30- cfg *config.Config,
31 ) *SchemeHandler {28 ) *SchemeHandler {
32 return &SchemeHandler{29 return &SchemeHandler{
33 assets: assets,30 assets: assets,
34 parserService: parserService,31 parserService: parserService,
35 browserService: browserService,32 browserService: browserService,
36- config: cfg,
37 }33 }
38 }34 }
39 35
@@ -56,11 +52,6 @@ func (s *SchemeHandler) Handle(uri string, cfg *config.Config) (string, []byte,
56 52
57 // handleAsset serves static assets from embedded filesystem53 // handleAsset serves static assets from embedded filesystem
58 func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {54 func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
59- // Check if this is an API route
60- if strings.HasPrefix(u.Path, "/api/") {
61- return s.handleAPI(u)
62- }
63-
64 // Resolve target path inside embed FS55 // Resolve target path inside embed FS
65 var rel string56 var rel string
66 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {57 if u.Opaque == constants.HomepagePath || (u.Host == constants.HomepagePath && (u.Path == "" || u.Path == "/")) || (u.Host == "" && (u.Path == "" || u.Path == "/")) {
@@ -110,38 +101,6 @@ func (s *SchemeHandler) handleAsset(u *neturl.URL) (string, []byte, bool) {
110 return mt, data, true101 return mt, data, true
111 }102 }
112 103
113-// handleAPI handles API routes
114-func (s *SchemeHandler) handleAPI(u *neturl.URL) (string, []byte, bool) {
115- switch u.Path {
116- case "/api/config":
117- return s.handleConfigAPI()
118- default:
119- log.Printf("[scheme] API route not found: %s", u.Path)
120- return "", nil, false
121- }
122-}
123-
124-// handleConfigAPI returns the application configuration as JSON
125-func (s *SchemeHandler) handleConfigAPI() (string, []byte, bool) {
126- if s.config == nil {
127- log.Printf("[scheme] config is nil, cannot serve /api/config")
128- return "", nil, false
129- }
130-
131- // Create a response with search shortcuts
132- response := map[string]interface{}{
133- "search_shortcuts": s.config.SearchShortcuts,
134- }
135-
136- data, err := json.Marshal(response)
137- if err != nil {
138- log.Printf("[scheme] failed to marshal config: %v", err)
139- return "", nil, false
140- }
141-
142- return "application/json", data, true
143-}
144-
145 // getMimeType determines the MIME type for a given file path104 // getMimeType determines the MIME type for a given file path
146 func (s *SchemeHandler) getMimeType(filename string) string {105 func (s *SchemeHandler) getMimeType(filename string) string {
147 ext := strings.ToLower(filepath.Ext(filename))106 ext := strings.ToLower(filepath.Ext(filename))
M internal/app/browser/browser.go +1 -1
@@ -139,7 +139,7 @@ func (app *BrowserApp) Initialize() error {
139139 app.browserService = services.NewBrowserService(app.config, app.queries)
140140
141141 // Initialize handlers
142- app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService, app.config)
142+ app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService)
143143 app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)
144144
145145 return nil
@@ -139,7 +139,7 @@ func (app *BrowserApp) Initialize() error {
139 app.browserService = services.NewBrowserService(app.config, app.queries)139 app.browserService = services.NewBrowserService(app.config, app.queries)
140 140
141 // Initialize handlers141 // Initialize handlers
142- app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService, app.config)142+ app.schemeHandler = api.NewSchemeHandler(app.assets, app.parserService, app.browserService)
143 app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)143 app.messageHandler = messaging.NewHandler(app.parserService, app.browserService)
144 144
145 return nil145 return nil
M internal/app/messaging/handler.go +65 -0
@@ -131,6 +131,10 @@ func (h *Handler) Handle(payload string) {
131131 h.handleConsoleMessage(msg)
132132 case "request-webview-id":
133133 h.handleWebViewIDRequest(msg)
134+ case "get_search_shortcuts":
135+ h.handleGetSearchShortcuts(msg)
136+ case "get_color_palettes":
137+ h.handleGetColorPalettes(msg)
134138 }
135139 }
136140
@@ -601,3 +605,64 @@ func (h *Handler) handleWebViewIDRequest(msg Message) {
601605 log.Printf("[messaging] Failed to send webview ID: %v", err)
602606 }
603607 }
608+
609+// handleGetSearchShortcuts sends search shortcuts configuration to JavaScript
610+func (h *Handler) handleGetSearchShortcuts(msg Message) {
611+ if h.webView == nil {
612+ log.Printf("[messaging] Cannot provide search shortcuts - no webview available")
613+ return
614+ }
615+
616+ if h.webView.IsDestroyed() {
617+ log.Printf("[messaging] Cannot provide search shortcuts - webview is destroyed")
618+ return
619+ }
620+
621+ // Get search shortcuts from browser service config
622+ shortcuts, err := h.browserService.GetSearchShortcuts(context.Background())
623+ if err != nil {
624+ log.Printf("[messaging] Failed to get search shortcuts: %v", err)
625+ _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to get search shortcuts')")
626+ return
627+ }
628+
629+ // Marshal to JSON
630+ b, err := json.Marshal(shortcuts)
631+ if err != nil {
632+ log.Printf("[messaging] Failed to marshal search shortcuts: %v", err)
633+ _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to load search shortcuts')")
634+ return
635+ }
636+
637+ // Inject the search shortcuts into the page
638+ log.Printf("[messaging] Sending search shortcuts to JavaScript")
639+ _ = h.webView.InjectScript("window.__dumber_search_shortcuts && window.__dumber_search_shortcuts(" + string(b) + ")")
640+}
641+
642+// handleGetColorPalettes sends color palettes configuration to JavaScript
643+func (h *Handler) handleGetColorPalettes(msg Message) {
644+ if h.webView == nil {
645+ log.Printf("[messaging] Cannot provide color palettes - no webview available")
646+ return
647+ }
648+
649+ if h.webView.IsDestroyed() {
650+ log.Printf("[messaging] Cannot provide color palettes - webview is destroyed")
651+ return
652+ }
653+
654+ // Get color palettes from browser service config
655+ palettes := h.browserService.GetColorPalettesForMessaging()
656+
657+ // Marshal to JSON
658+ b, err := json.Marshal(palettes)
659+ if err != nil {
660+ log.Printf("[messaging] Failed to marshal color palettes: %v", err)
661+ _ = h.webView.InjectScript("window.__dumber_color_palettes_error && window.__dumber_color_palettes_error('Failed to load color palettes')")
662+ return
663+ }
664+
665+ // Inject the color palettes into the page
666+ log.Printf("[messaging] Sending color palettes to JavaScript")
667+ _ = h.webView.InjectScript("window.__dumber_color_palettes && window.__dumber_color_palettes(" + string(b) + ")")
668+}
@@ -131,6 +131,10 @@ func (h *Handler) Handle(payload string) {
131 h.handleConsoleMessage(msg)131 h.handleConsoleMessage(msg)
132 case "request-webview-id":132 case "request-webview-id":
133 h.handleWebViewIDRequest(msg)133 h.handleWebViewIDRequest(msg)
134+ case "get_search_shortcuts":
135+ h.handleGetSearchShortcuts(msg)
136+ case "get_color_palettes":
137+ h.handleGetColorPalettes(msg)
134 }138 }
135 }139 }
136 140
@@ -601,3 +605,64 @@ func (h *Handler) handleWebViewIDRequest(msg Message) {
601 log.Printf("[messaging] Failed to send webview ID: %v", err)605 log.Printf("[messaging] Failed to send webview ID: %v", err)
602 }606 }
603 }607 }
608+
609+// handleGetSearchShortcuts sends search shortcuts configuration to JavaScript
610+func (h *Handler) handleGetSearchShortcuts(msg Message) {
611+ if h.webView == nil {
612+ log.Printf("[messaging] Cannot provide search shortcuts - no webview available")
613+ return
614+ }
615+
616+ if h.webView.IsDestroyed() {
617+ log.Printf("[messaging] Cannot provide search shortcuts - webview is destroyed")
618+ return
619+ }
620+
621+ // Get search shortcuts from browser service config
622+ shortcuts, err := h.browserService.GetSearchShortcuts(context.Background())
623+ if err != nil {
624+ log.Printf("[messaging] Failed to get search shortcuts: %v", err)
625+ _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to get search shortcuts')")
626+ return
627+ }
628+
629+ // Marshal to JSON
630+ b, err := json.Marshal(shortcuts)
631+ if err != nil {
632+ log.Printf("[messaging] Failed to marshal search shortcuts: %v", err)
633+ _ = h.webView.InjectScript("window.__dumber_search_shortcuts_error && window.__dumber_search_shortcuts_error('Failed to load search shortcuts')")
634+ return
635+ }
636+
637+ // Inject the search shortcuts into the page
638+ log.Printf("[messaging] Sending search shortcuts to JavaScript")
639+ _ = h.webView.InjectScript("window.__dumber_search_shortcuts && window.__dumber_search_shortcuts(" + string(b) + ")")
640+}
641+
642+// handleGetColorPalettes sends color palettes configuration to JavaScript
643+func (h *Handler) handleGetColorPalettes(msg Message) {
644+ if h.webView == nil {
645+ log.Printf("[messaging] Cannot provide color palettes - no webview available")
646+ return
647+ }
648+
649+ if h.webView.IsDestroyed() {
650+ log.Printf("[messaging] Cannot provide color palettes - webview is destroyed")
651+ return
652+ }
653+
654+ // Get color palettes from browser service config
655+ palettes := h.browserService.GetColorPalettesForMessaging()
656+
657+ // Marshal to JSON
658+ b, err := json.Marshal(palettes)
659+ if err != nil {
660+ log.Printf("[messaging] Failed to marshal color palettes: %v", err)
661+ _ = h.webView.InjectScript("window.__dumber_color_palettes_error && window.__dumber_color_palettes_error('Failed to load color palettes')")
662+ return
663+ }
664+
665+ // Inject the color palettes into the page
666+ log.Printf("[messaging] Sending color palettes to JavaScript")
667+ _ = h.webView.InjectScript("window.__dumber_color_palettes && window.__dumber_color_palettes(" + string(b) + ")")
668+}
M internal/services/browser.go +17 -0
@@ -677,3 +677,20 @@ func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI stri
677677 log.Printf("[favicon] Failed to create favicon cache: %v", err)
678678 }
679679 }
680+
681+// ColorPalettesResponse holds light and dark palettes for JSON marshaling
682+type ColorPalettesResponse struct {
683+ Light config.ColorPalette `json:"light"`
684+ Dark config.ColorPalette `json:"dark"`
685+}
686+
687+// GetColorPalettesForMessaging returns the color palettes from config
688+func (s *BrowserService) GetColorPalettesForMessaging() ColorPalettesResponse {
689+ if s.config == nil {
690+ return ColorPalettesResponse{}
691+ }
692+ return ColorPalettesResponse{
693+ Light: s.config.Appearance.LightPalette,
694+ Dark: s.config.Appearance.DarkPalette,
695+ }
696+}
@@ -677,3 +677,20 @@ func (s *BrowserService) handleFaviconURIChanged(pageURL string, faviconURI stri
677 log.Printf("[favicon] Failed to create favicon cache: %v", err)677 log.Printf("[favicon] Failed to create favicon cache: %v", err)
678 }678 }
679 }679 }
680+
681+// ColorPalettesResponse holds light and dark palettes for JSON marshaling
682+type ColorPalettesResponse struct {
683+ Light config.ColorPalette `json:"light"`
684+ Dark config.ColorPalette `json:"dark"`
685+}
686+
687+// GetColorPalettesForMessaging returns the color palettes from config
688+func (s *BrowserService) GetColorPalettesForMessaging() ColorPalettesResponse {
689+ if s.config == nil {
690+ return ColorPalettesResponse{}
691+ }
692+ return ColorPalettesResponse{
693+ Light: s.config.Appearance.LightPalette,
694+ Dark: s.config.Appearance.DarkPalette,
695+ }
696+}
M pkg/webkit/webview_cgo.go +1 -13
@@ -2212,19 +2212,7 @@ func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w
22122212 log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState)
22132213 script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)
22142214
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- }
2215+ // Color palettes and search shortcuts are now loaded via messaging bridge
22282216
22292217 log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
22302218 return script
@@ -2212,19 +2212,7 @@ func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w
2212 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)
2213 script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)2213 script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState)
2214 2214
2215- // Inject color palette JSON2215+ // Color palettes and search shortcuts are now loaded via messaging bridge
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 2216
2229 log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))2217 log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes))
2230 return script2218 return script

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