v0.9.0 636c44c

gui.ts RawHistoryFolder

1/**
2 * Unified GUI Bundle Entry Point
3 *
4 * Single entry point for all WebKit GUI functionality.
5 * Loads immediately and provides modular initialization system.
6 */
7
8// Import all GUI modules
9import { initializeToast, type ToastConfig } from "./modules/toast";
10import { initializeOmnibox, type OmniboxInitConfig } from "./modules/omnibox";
11import {
12  initializeWorkspace,
13  type WorkspaceConfigPayload,
14  type WorkspaceRuntime,
15} from "./modules/workspace";
16import { initializeWindowOpenInterceptor } from "./modules/window-open";
17import { keyboardService, type KeyboardService } from "$lib/keyboard";
18import type { Suggestion } from "../components/omnibox/types";
19// Note: color-scheme module is loaded separately at document-start by WebKit
20
21// Global interface for the unified GUI system
22interface DumberGUI {
23  initializeToast: (config?: ToastConfig) => Promise<void>;
24  initializeOmnibox: (config?: OmniboxInitConfig) => Promise<void>;
25  keyboard: KeyboardService;
26  isReady: boolean;
27}
28
29// (legacy suggestions normalization removed; omnibox now fetches suggestions directly via API)
30
31// DOM readiness utility - handles all document ready states
32function whenDOMReady(callback: () => void) {
33  if (document.readyState === "loading") {
34    document.addEventListener("DOMContentLoaded", callback, { once: true });
35  } else if (document.readyState === "interactive" && !document.body) {
36    // Interactive but no body yet, wait a bit more
37    setTimeout(() => whenDOMReady(callback), 10);
38  } else {
39    // DOM ready and body exists
40    callback();
41  }
42}
43
44// Install a page-world bridge so Go can call window.__dumber_omnibox_suggestions()
45// Page-world bridge is injected by WebKit at document-start; no-op here to avoid duplicates.
46
47// Create global namespace
48declare global {
49  interface Window {
50    __dumber_gui?: DumberGUI;
51    __dumber_gui_ready?: boolean;
52    __dumber_showToast?: (
53      message: string,
54      duration?: number,
55      type?: "info" | "success" | "error",
56    ) => number | void;
57    __dumber_showZoomToast?: (level: number) => void;
58    __dumber_keyboard?: KeyboardService;
59    __dumber_workspace_config?: WorkspaceConfigPayload;
60    __dumber_workspace?: WorkspaceRuntime;
61    __dumber_pane?: { id: string; active: boolean };
62    __dumber_omnibox?: {
63      setSuggestions: (suggestions: Suggestion[]) => void;
64      toggle: () => void;
65      open: (mode?: string, query?: string) => void;
66      close: () => void;
67      findQuery: (query: string) => void;
68      setActive: (active: boolean) => void;
69    };
70    __dumber_gui_bootstrap?: () => void;
71    // Legacy compatibility functions for Go bridge
72    __dumber_toggle?: () => void;
73    __dumber_find_open?: (query?: string) => void;
74    __dumber_find_close?: () => void;
75    __dumber_find_query?: (query: string) => void;
76  }
77}
78
79// Prevent multiple initialization with enhanced logging
80if (!window.__dumber_gui_ready) {
81  // Skip initialization in iframes - GUI only needed in main frame
82  if (window.self !== window.top) {
83    console.log("🚫 Dumber GUI skipped in iframe");
84  } else {
85    window.__dumber_gui_ready = true;
86
87    let workspaceHasFocus = true; // Assume focus initially, will be corrected by focus events
88    let currentPaneId = window.__dumber_pane?.id || "unknown";
89    let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events
90
91    document.addEventListener("dumber:workspace-focus", (event: Event) => {
92      const detail = (event as CustomEvent).detail || {};
93      console.log(
94        "[workspace focus event]",
95        detail,
96        "prev focus=",
97        workspaceHasFocus,
98      );
99
100      hasReceivedFocusEvent = true;
101
102      // Update focus state
103      if (typeof detail?.active === "boolean") {
104        workspaceHasFocus = detail.active;
105      } else {
106        workspaceHasFocus = detail !== false;
107      }
108
109      // Update pane tracking
110      if (detail?.paneId) {
111        currentPaneId = detail.paneId;
112      }
113
114      // Update global pane state
115      if (window.__dumber_pane) {
116        window.__dumber_pane.active = workspaceHasFocus;
117      }
118
119      console.log(
120        "[workspace focus updated] focus=",
121        workspaceHasFocus,
122        "paneId=",
123        currentPaneId,
124      );
125
126      // Handle GUI visibility based on focus
127      if (window.__dumber_omnibox) {
128        if (workspaceHasFocus) {
129          window.__dumber_omnibox.setActive(true);
130        } else {
131          window.__dumber_omnibox.setActive(false);
132          window.__dumber_omnibox.close();
133        }
134      }
135    });
136
137    // Initialize Svelte GUI systems immediately
138    whenDOMReady(async () => {
139      try {
140        // Page-world bridge is injected by WebKit; avoid duplicate injection here
141
142        // Initialize window.open interceptor first (must be early)
143        initializeWindowOpenInterceptor();
144
145        // Initialize toast system first
146        await initializeToast();
147
148        // Verify toast functions are available
149        if (typeof window.__dumber_showToast !== "function") {
150          throw new Error("Toast functions not exposed after initialization");
151        }
152
153        // Initialize omnibox system
154        await initializeOmnibox();
155
156        // Initialize workspace controls (pane/tab scaffolding)
157        initializeWorkspace(window.__dumber_workspace_config);
158
159        console.log("✅ All GUI systems initialized successfully");
160      } catch (e) {
161        console.error("❌ Failed to initialize Svelte toast system:", e);
162
163        // Enhanced fallback with better error recovery
164        window.__dumber_showToast = (
165          message: string,
166          duration: number = 2500,
167        ) => {
168          // Remove existing toasts to prevent overlap
169          document
170            .querySelectorAll(".dumber-fallback-toast")
171            .forEach((el) => el.remove());
172
173          const toast = document.createElement("div");
174          toast.className = "dumber-fallback-toast";
175          toast.textContent = message;
176          toast.style.cssText = `
177          position: fixed;
178          bottom: 20px;
179          right: 20px;
180          background: rgba(0,0,0,0.9);
181          color: white;
182          padding: 12px 16px;
183          border-radius: 6px;
184          z-index: 2147483647;
185          font-family: system-ui, -apple-system, sans-serif;
186          font-size: 14px;
187          opacity: 0;
188          transform: translateX(100%);
189          transition: all 0.3s ease;
190          max-width: 300px;
191          word-wrap: break-word;
192        `;
193
194          if (document.body) {
195            document.body.appendChild(toast);
196
197            // Animate in
198            requestAnimationFrame(() => {
199              toast.style.opacity = "1";
200              toast.style.transform = "translateX(0)";
201            });
202
203            // Animate out and remove
204            setTimeout(() => {
205              toast.style.opacity = "0";
206              toast.style.transform = "translateX(100%)";
207              setTimeout(() => toast.remove(), 300);
208            }, duration);
209          }
210        };
211
212        window.__dumber_showZoomToast = (level: number) => {
213          const percentage = Math.round(level * 100);
214          window.__dumber_showToast!(`Zoom: ${percentage}%`, 1500);
215        };
216
217        // Expose other required functions
218        window.__dumber_dismissToast = () => {
219          document
220            .querySelectorAll(".dumber-fallback-toast")
221            .forEach((el) => el.remove());
222        };
223
224        window.__dumber_clearToasts = () => {
225          document
226            .querySelectorAll(".dumber-fallback-toast")
227            .forEach((el) => el.remove());
228        };
229      }
230    });
231
232    // Expose keyboard service globally for Go bridge
233    window.__dumber_keyboard = keyboardService;
234
235    // Initialize global keyboard and mouse event listeners for the keyboard service
236    whenDOMReady(() => {
237      // Global keyboard event listener (capture phase for high priority)
238      document.addEventListener(
239        "keydown",
240        (event) => {
241          keyboardService.handleKeyboardEvent(event);
242        },
243        true,
244      );
245
246      // Global mouse event listener for navigation buttons
247      document.addEventListener(
248        "mousedown",
249        (event) => {
250          keyboardService.handleMouseEvent(event);
251        },
252        true,
253      );
254
255      console.log("✅ KeyboardService initialized with global listeners");
256
257      // Listen to bridge keyboard events from page-world and forward to KeyboardService
258      document.addEventListener("dumber:key", (e: Event) => {
259        const detail = (e as CustomEvent).detail || {};
260        if (detail && typeof detail.shortcut === "string") {
261          keyboardService.handleNativeShortcut(detail.shortcut);
262        }
263      });
264
265      document.addEventListener("dumber:ui:shortcut", (e: Event) => {
266        const detail = (e as CustomEvent).detail || {};
267        const action = detail?.action;
268        const eventPaneId = detail?.paneId;
269        const source = detail?.source || "unknown";
270
271        if (typeof action !== "string") {
272          return;
273        }
274
275        // Enhanced focus checking with pane ID validation
276        const isForThisPane = !eventPaneId || eventPaneId === currentPaneId;
277        // Only apply strict focus checking to omnibox actions, be permissive for others
278        const isOmniboxAction = action.startsWith("omnibox-");
279        const shouldHandle =
280          isForThisPane &&
281          (isOmniboxAction
282            ? hasReceivedFocusEvent
283              ? workspaceHasFocus
284              : true
285            : true);
286
287        if (!shouldHandle) {
288          console.log("[dumber shortcuts] ignored action", {
289            action,
290            workspaceHasFocus,
291            isForThisPane,
292            eventPaneId,
293            currentPaneId,
294            source,
295            detail,
296            hasReceivedFocusEvent,
297          });
298          return;
299        }
300
301        console.log("[dumber shortcuts] handling action", {
302          action,
303          source,
304          paneId: currentPaneId,
305        });
306
307        const omnibox = window.__dumber_omnibox;
308
309        switch (action) {
310          case "omnibox-nav-toggle":
311            omnibox?.open?.("omnibox", detail?.query);
312            break;
313          case "omnibox-open":
314            omnibox?.open?.("omnibox", detail?.query);
315            break;
316          case "omnibox-find-toggle":
317            omnibox?.open?.("find", detail?.query);
318            break;
319          case "omnibox-close":
320            omnibox?.close?.();
321            break;
322          default:
323            console.warn("[dumber] Unknown UI shortcut action:", action);
324        }
325      });
326
327      // Suggestions are now fetched via API directly by the omnibox component; no page-bridge handlers needed
328    });
329
330    // Legacy compatibility functions for existing Go code
331    window.__dumber_toggle = () => {
332      try {
333        console.log("🎯 __dumber_toggle called");
334        console.log("🔧 Omnibox API available:", !!window.__dumber_omnibox);
335        if (window.__dumber_omnibox?.toggle) {
336          console.log("✅ Using Svelte omnibox toggle");
337          window.__dumber_omnibox.toggle();
338          return;
339        }
340
341        throw new Error("Omnibox toggle requested but API is unavailable");
342      } catch (error) {
343        console.error("❌ Error in __dumber_toggle:", error);
344      }
345    };
346
347    window.__dumber_find_open = (query?: string) => {
348      try {
349        if (window.__dumber_omnibox?.open) {
350          window.__dumber_omnibox.open("find", query);
351          return;
352        }
353
354        throw new Error("Omnibox find requested but API is unavailable");
355      } catch (error) {
356        console.error("❌ Error in __dumber_find_open:", error);
357      }
358    };
359
360    window.__dumber_find_close = () => {
361      if (window.__dumber_omnibox?.close) {
362        window.__dumber_omnibox.close();
363        return;
364      }
365
366      const error = new Error("Omnibox close requested but API is unavailable");
367      console.error("❌ Error in __dumber_find_close:", error);
368    };
369
370    // Find query function that Go uses to set find query
371    window.__dumber_find_query = (query: string) => {
372      if (window.__dumber_omnibox?.findQuery) {
373        window.__dumber_omnibox.findQuery(query);
374      }
375    };
376
377    // Create the global GUI object
378    window.__dumber_gui = {
379      initializeToast: async (config?: ToastConfig) => {
380        return await initializeToast(config);
381      },
382
383      initializeOmnibox: async (config?: OmniboxInitConfig) => {
384        return await initializeOmnibox(config);
385      },
386
387      keyboard: keyboardService,
388
389      isReady: true,
390    };
391
392    // Bootstrap function for lazy GUI initialization by workspace manager
393    window.__dumber_gui_bootstrap = function () {
394      console.log("[gui-bootstrap] Initializing GUI for workspace");
395
396      // Pane ID will be updated via focus events
397
398      // Ensure omnibox is available for this pane
399      if (!window.__dumber_omnibox) {
400        console.log(
401          `[gui-bootstrap] Omnibox not yet available for pane ${currentPaneId}, will be loaded when needed`,
402        );
403      } else {
404        console.log(
405          `[gui-bootstrap] Omnibox already available for pane ${currentPaneId}`,
406        );
407        // Ensure omnibox is aware of the current pane
408        window.__dumber_omnibox.setActive(workspaceHasFocus);
409      }
410
411      console.log(
412        `[gui-bootstrap] GUI bootstrap complete for pane ${currentPaneId}`,
413      );
414    };
415  }
416}

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