1/**
2 * Omnibox Message Bridge
3 *
4 * TypeScript interface for Go-JavaScript communication
5 */
6
7import type { OmniboxMessage, OmniboxMessageBridge, Suggestion, SearchShortcut } from "./types";
8import { omniboxStore } from "./stores.svelte.ts";
9
10export class OmniboxBridge implements OmniboxMessageBridge {
11 // Forward messages to the native WebKit message handler when available
12 // Fallback: for navigate, perform direct location change
13 postMessage(msg: OmniboxMessage): void {
14 console.log("📤 [DEBUG] Posting message to backend:", msg);
15 // NOTE: Do NOT detach postMessage from its receiver. In WKWebView,
16 // postMessage must be called on the UserMessageHandler instance.
17 const bridge = window.webkit?.messageHandlers?.dumber;
18 if (bridge && typeof bridge.postMessage === "function") {
19 try {
20 console.log("📱 [DEBUG] Using webkit message handler");
21 bridge.postMessage(JSON.stringify(msg));
22 return;
23 } catch (e) {
24 console.warn(
25 "postMessage to native handler failed, using fallback:",
26 e,
27 );
28 }
29 }
30 console.log("⚠️ [DEBUG] No webkit bridge available");
31 // Fallback navigation if no native bridge is available
32 if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) {
33 try {
34 window.location.href = msg.url;
35 } catch (e) {
36 console.error("Fallback navigation failed:", e);
37 }
38 }
39 }
40
41 /**
42 * Update suggestions from Go backend
43 */
44 setSuggestions(suggestions: Suggestion[]): void {
45 console.log("📝 [DEBUG] Received suggestions from backend:", suggestions);
46 omniboxStore.updateSuggestions(suggestions);
47 }
48
49 /**
50 * Update search shortcuts from Go backend
51 */
52 setSearchShortcuts(shortcuts: Record<string, SearchShortcut>): void {
53 console.log("📝 [DEBUG] Received search shortcuts from backend:", shortcuts);
54 omniboxStore.updateSearchShortcuts(shortcuts);
55 }
56
57 /**
58 * Handle navigation request
59 */
60 navigate(url: string): void {
61 console.log("🚀 Omnibox navigate called with:", url);
62 this.postMessage({ type: "navigate", url });
63 }
64
65 /**
66 * Handle search query
67 */
68 query(searchTerm: string, limit?: number): void {
69 const q = searchTerm ?? "";
70 const lim = limit || omniboxStore.config.defaultLimit;
71 console.log("🔍 [DEBUG] Sending query to backend:", { q, limit: lim });
72 // Send to native handler; Go will compute suggestions and call setSuggestions
73 this.postMessage({ type: "query", q, limit: lim });
74 }
75
76 /**
77 * Fetch search shortcuts from backend via messaging bridge
78 */
79 async fetchSearchShortcuts(): Promise<void> {
80 return new Promise((resolve, reject) => {
81 // Set up one-time response handler
82 const originalCallback = (window as any).__dumber_search_shortcuts;
83 (window as any).__dumber_search_shortcuts = (data: unknown) => {
84 try {
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 to match our SearchShortcut type
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 url: (v.url ?? v.URL ?? "") as string,
97 description: (v.description ?? v.Description ?? "") as string,
98 };
99 }
100 this.setSearchShortcuts(normalized);
101
102 // Restore original callback if it existed
103 if (originalCallback) {
104 (window as any).__dumber_search_shortcuts = originalCallback;
105 }
106
107 resolve();
108 } catch (error) {
109 console.error("Failed to process search shortcuts:", error);
110 reject(error);
111 }
112 };
113
114 // Send message to Go backend
115 const bridge = window.webkit?.messageHandlers?.dumber;
116 if (bridge && typeof bridge.postMessage === "function") {
117 bridge.postMessage(
118 JSON.stringify({
119 type: "get_search_shortcuts",
120 }),
121 );
122 } else {
123 reject(new Error("WebKit message handler not available"));
124 }
125 });
126 }
127
128 // Suggestions are returned via setSuggestions() when native handler responds
129}
130
131// Singleton instance
132export const omniboxBridge = new OmniboxBridge();
133
134/**
135 * Debounced query function for search input
136 */
137export function debouncedQuery(searchTerm: string): void {
138 omniboxStore.clearDebounceTimer();
139
140 const timerId = window.setTimeout(() => {
141 omniboxBridge.query(searchTerm);
142 }, omniboxStore.config.debounceDelay);
143
144 omniboxStore.setDebounceTimer(timerId);
145}
146
147// Extend global window interface for Go bridge compatibility
148declare global {
149 interface Window {
150 webkit?: {
151 messageHandlers?: {
152 dumber?: {
153 postMessage: (message: string) => void;
154 };
155 };
156 };
157 // Omnibox API for Go bridge
158 __dumber_omnibox?: {
159 setSuggestions: (suggestions: Suggestion[]) => void;
160 toggle: () => void;
161 open: (mode?: string, query?: string) => void;
162 close: () => void;
163 findQuery: (query: string) => void;
164 setActive: (active: boolean) => void;
165 };
166 }
167}