v0.14.0 76d1892

messaging.ts RawHistoryFolder

1/**
2 * Omnibox Message Bridge
3 *
4 * TypeScript interface for Go-JavaScript communication
5 */
6
7import type { OmniboxMessage, OmniboxMessageBridge, Suggestion, Favorite, SearchShortcut } from "./types";
8import { omniboxStore } from "./stores.svelte.ts";
9
10export class OmniboxBridge implements OmniboxMessageBridge {
11  // Forward messages via CustomEvent to main world bridge
12  // The main world will forward to webkit.messageHandlers.dumber
13  postMessage(msg: OmniboxMessage): void {
14    console.log("📤 [DEBUG] Posting message to backend via bridge:", msg);
15    try {
16      // Dispatch CustomEvent that main-world bridge will listen to
17      document.dispatchEvent(new CustomEvent("dumber:isolated-message", {
18        detail: { payload: msg }
19      }));
20      console.log("✅ [DEBUG] Dispatched isolated message event");
21    } catch (e) {
22      console.error("❌ [DEBUG] Failed to dispatch message:", e);
23      // Fallback navigation if dispatch fails
24      if (msg.type === "navigate" && typeof msg.url === "string" && msg.url) {
25        try {
26          window.location.href = msg.url;
27        } catch (navError) {
28          console.error("Fallback navigation failed:", navError);
29        }
30      }
31    }
32  }
33
34  /**
35   * Update suggestions from Go backend
36   */
37  setSuggestions(suggestions: Suggestion[]): void {
38    console.log("📝 [DEBUG] Received suggestions from backend:", suggestions);
39    omniboxStore.updateSuggestions(suggestions);
40  }
41
42  /**
43   * Update search shortcuts from Go backend
44   */
45  setSearchShortcuts(shortcuts: Record<string, SearchShortcut>): void {
46    console.log("📝 [DEBUG] Received search shortcuts from backend:", shortcuts);
47    omniboxStore.updateSearchShortcuts(shortcuts);
48  }
49
50  /**
51   * Handle navigation request
52   */
53  navigate(url: string): void {
54    console.log("🚀 Omnibox navigate called with:", url);
55    this.postMessage({ type: "navigate", url });
56  }
57
58  /**
59   * Handle search query
60   */
61  query(searchTerm: string, limit?: number): void {
62    const q = searchTerm ?? "";
63    const lim = limit || omniboxStore.config.defaultLimit;
64    console.log("🔍 [DEBUG] Sending query to backend:", { q, limit: lim });
65    // Send to native handler; Go will compute suggestions and call setSuggestions
66    this.postMessage({ type: "query", q, limit: lim });
67  }
68
69  /**
70   * Fetch search shortcuts from backend via messaging bridge
71   */
72  async fetchSearchShortcuts(): Promise<void> {
73    return new Promise((resolve, reject) => {
74      // Set up one-time response handler
75      const originalCallback = (window as any).__dumber_search_shortcuts;
76      (window as any).__dumber_search_shortcuts = (data: unknown) => {
77        try {
78          // Type guard
79          if (typeof data !== "object" || data === null) {
80            reject(new Error("Invalid shortcuts data"));
81            return;
82          }
83          const dataObj = data as Record<string, unknown>;
84          // Normalize to match our SearchShortcut type
85          const normalized: Record<string, SearchShortcut> = {};
86          for (const [key, value] of Object.entries(dataObj)) {
87            const v = value as Record<string, unknown>;
88            normalized[key] = {
89              url: (v.url ?? v.URL ?? "") as string,
90              description: (v.description ?? v.Description ?? "") as string,
91            };
92          }
93          this.setSearchShortcuts(normalized);
94
95          // Restore original callback if it existed
96          if (originalCallback) {
97            (window as any).__dumber_search_shortcuts = originalCallback;
98          }
99
100          resolve();
101        } catch (error) {
102          console.error("Failed to process search shortcuts:", error);
103          reject(error);
104        }
105      };
106
107      // Send message to Go backend
108      const bridge = window.webkit?.messageHandlers?.dumber;
109      if (bridge && typeof bridge.postMessage === "function") {
110        bridge.postMessage(
111          JSON.stringify({
112            type: "get_search_shortcuts",
113          }),
114        );
115      } else {
116        reject(new Error("WebKit message handler not available"));
117      }
118    });
119  }
120
121  /**
122   * Update favorites from Go backend
123   */
124  setFavorites(favorites: Favorite[]): void {
125    console.log("📝 [DEBUG] Received favorites from backend:", favorites);
126    omniboxStore.updateFavorites(favorites);
127  }
128
129  /**
130   * Fetch favorites from backend via messaging bridge
131   */
132  async fetchFavorites(): Promise<void> {
133    return new Promise((resolve, reject) => {
134      // Set up one-time event listener for favorites response
135      const handleFavorites = ((event: CustomEvent) => {
136        try {
137          const { favorites } = event.detail;
138          if (!Array.isArray(favorites)) {
139            reject(new Error("Invalid favorites data"));
140            return;
141          }
142          console.log("📝 [DEBUG] Received favorites via CustomEvent:", favorites);
143          this.setFavorites(favorites as Favorite[]);
144
145          // Clean up event listener
146          document.removeEventListener("dumber:favorites", handleFavorites as EventListener);
147
148          resolve();
149        } catch (error) {
150          console.error("Failed to process favorites:", error);
151          document.removeEventListener("dumber:favorites", handleFavorites as EventListener);
152          reject(error);
153        }
154      }) as EventListener;
155
156      // Listen for favorites event from main world bridge
157      document.addEventListener("dumber:favorites", handleFavorites as EventListener);
158
159      // Send message to Go backend via CustomEvent bridge (like other omnibox messages)
160      this.postMessage({ type: "get_favorites" });
161
162      // Timeout after 5 seconds
163      setTimeout(() => {
164        document.removeEventListener("dumber:favorites", handleFavorites as EventListener);
165        reject(new Error("Favorites fetch timeout"));
166      }, 5000);
167    });
168  }
169
170  /**
171   * Toggle favorite status for a URL
172   */
173  toggleFavorite(url: string, title: string, faviconURL: string): void {
174    console.log("⭐ [DEBUG] Toggling favorite:", { url, title, faviconURL });
175    this.postMessage({ type: "toggle_favorite", url, title, faviconURL });
176  }
177
178  // Suggestions are returned via setSuggestions() when native handler responds
179}
180
181// Singleton instance
182export const omniboxBridge = new OmniboxBridge();
183
184/**
185 * Debounced query function for search input
186 */
187export function debouncedQuery(searchTerm: string): void {
188  omniboxStore.clearDebounceTimer();
189
190  const timerId = window.setTimeout(() => {
191    omniboxBridge.query(searchTerm);
192  }, omniboxStore.config.debounceDelay);
193
194  omniboxStore.setDebounceTimer(timerId);
195}
196
197// Extend global window interface for Go bridge compatibility
198declare global {
199  interface Window {
200    webkit?: {
201      messageHandlers?: {
202        dumber?: {
203          postMessage: (message: string) => void;
204        };
205      };
206    };
207    // Omnibox API for Go bridge
208    __dumber_omnibox?: {
209      setSuggestions: (suggestions: Suggestion[]) => void;
210      toggle: () => void;
211      open: (mode?: string, query?: string) => void;
212      close: () => void;
213      findQuery: (query: string) => void;
214      setActive: (active: boolean) => void;
215    };
216  }
217}

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