v0.12.1 1453d21

Omnibox.svelte RawHistoryFolder

1<!--
2  Omnibox Component
3
4  Main container for omnibox/find functionality using Svelte 5 runes
5-->
6<script lang="ts">
7  import { onMount, onDestroy } from 'svelte';
8  import OmniboxInput from './OmniboxInput.svelte';
9  import OmniboxSuggestions from './OmniboxSuggestions.svelte';
10  import OmniboxFind from './OmniboxFind.svelte';
11  import { omniboxStore } from './stores.svelte.ts';
12  import { omniboxBridge } from './messaging';
13
14  // Reactive state from store
15  let visible = $derived(omniboxStore.visible);
16  let mode = $derived(omniboxStore.mode);
17  let faded = $derived(omniboxStore.faded);
18
19  // Component refs
20  let boxElement: HTMLDivElement;
21  let blurLayerElement: HTMLDivElement;
22  let inputElement = $state<HTMLInputElement>();
23
24  // Responsive styling state
25  let responsiveStyles = $state({
26    width: 'min(90vw, 720px)',
27    padding: '8px 12px',
28    fontSize: '16px',
29    inputPadding: '10px 12px'
30  });
31
32  // Track theme mode derived from injected color-scheme module
33  let isDarkMode = $state(false);
34  let themeObserver: MutationObserver | null = null;
35
36  function syncTheme() {
37    if (typeof document === 'undefined') return;
38
39    const globalWindow = typeof window === 'undefined' ? undefined : window;
40
41    if (document.documentElement.classList.contains('dark')) {
42      isDarkMode = true;
43      return;
44    }
45
46    if (globalWindow?.matchMedia) {
47      try {
48        const prefersDark = globalWindow.matchMedia('(prefers-color-scheme: dark)').matches;
49        if (typeof prefersDark === 'boolean') {
50          isDarkMode = prefersDark;
51          return;
52        }
53      } catch {
54        // matchMedia might be unavailable in some environments
55      }
56    }
57
58    // Fallback to injected GTK preference flag if available
59    isDarkMode = Boolean((globalWindow as any)?.__dumber_gtk_prefers_dark);
60  }
61
62  function observeThemeChanges() {
63    if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') {
64      return;
65    }
66
67    if (themeObserver) {
68      themeObserver.disconnect();
69    }
70
71    themeObserver = new MutationObserver(syncTheme);
72    themeObserver.observe(document.documentElement, {
73      attributes: true,
74      attributeFilter: ['class']
75    });
76  }
77
78  // Handle click outside to close
79  function handleOverlayClick(event: MouseEvent) {
80    const target = event.target as HTMLElement;
81    if (target && boxElement && !boxElement.contains(target)) {
82      event.preventDefault();
83      event.stopPropagation();
84      omniboxStore.close();
85    }
86  }
87
88  // Handle box click to prevent propagation
89  function handleBoxClick(event: MouseEvent) {
90    event.stopPropagation();
91  }
92
93  // Handle mouse enter to focus input and clear fade
94  function handleMouseEnter() {
95    if (visible && inputElement) {
96      try {
97        inputElement.focus({ preventScroll: true });
98      } catch {
99        inputElement.focus();
100      }
101      omniboxStore.setFaded(false);
102    }
103  }
104
105  // Update responsive styles based on viewport width (handles zoom levels)
106  function updateResponsiveStyles() {
107    const vw = window.innerWidth;
108    const isSmall = vw < 640;
109    const isLarge = vw > 1920;
110
111    responsiveStyles = {
112      width: isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)',
113      padding: isSmall ? '6px 8px' : '8px 12px',
114      fontSize: isSmall ? '14px' : isLarge ? '17px' : '16px',
115      inputPadding: isSmall ? '8px 10px' : '10px 12px'
116    };
117  }
118
119  // Focus input when component becomes visible and manage page event blocking
120  $effect(() => {
121    if (visible && inputElement) {
122      inputElement.focus();
123      // Enable keyboard event blocking in main world when omnibox opens
124      enablePageEventBlocking();
125    } else if (!visible) {
126      // Disable keyboard event blocking when omnibox closes
127      disablePageEventBlocking();
128    }
129  });
130
131  // Functions to control main-world event blocking
132  function enablePageEventBlocking() {
133    try {
134      // Send message to Go backend to enable keyboard blocking
135      if (window.webkit?.messageHandlers?.dumber) {
136        window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
137          type: 'keyboard_blocking',
138          action: 'enable'
139        }));
140      }
141    } catch (error) {
142      console.warn('[omnibox] Failed to enable page event blocking:', error);
143    }
144  }
145
146  function disablePageEventBlocking() {
147    try {
148      // Send message to Go backend to disable keyboard blocking
149      if (window.webkit?.messageHandlers?.dumber) {
150        window.webkit.messageHandlers.dumber.postMessage(JSON.stringify({
151          type: 'keyboard_blocking',
152          action: 'disable'
153        }));
154      }
155    } catch (error) {
156      console.warn('[omnibox] Failed to disable page event blocking:', error);
157    }
158  }
159
160
161  // Apply faded styling effect (matching original JS implementation)
162  // NOTE: to avoid text becoming blurry while animating, we animate the
163  // blur via an absolutely-positioned layer's opacity instead of
164  // animating `backdrop-filter` on the container. This keeps the text
165  // on a separate compositing layer and prevents browser antialiasing
166  // artifacts during transitions.
167  $effect(() => {
168    if (!boxElement || !inputElement || !blurLayerElement) return;
169
170    // Ensure subtle transitions are present on the container so
171    // background-color (and other cheap properties) animate smoothly.
172    try {
173      if (!boxElement.style.transition) {
174        boxElement.style.transition = 'background-color 120ms ease, border-color 120ms ease';
175      }
176    } catch (e) {
177      // defensive: ignore if setting styles fails for some reason
178    }
179
180    const surfaceMix = isDarkMode ? '55%' : '88%';
181    const inputMix = isDarkMode ? '52%' : '90%';
182    const borderMix = isDarkMode ? '62%' : '85%';
183    const blurMix = isDarkMode ? '22%' : '12%';
184
185    const applyFade = faded && mode === 'find';
186
187    const containerBackground = applyFade
188      ? `color-mix(in srgb, var(--dynamic-surface) ${surfaceMix}, transparent)`
189      : 'var(--dynamic-surface)';
190    const inputBackground = applyFade
191      ? `color-mix(in srgb, var(--dynamic-bg) ${inputMix}, transparent)`
192      : 'var(--dynamic-bg)';
193    const borderColor = applyFade
194      ? `color-mix(in srgb, var(--dynamic-border) ${borderMix}, transparent)`
195      : 'var(--dynamic-border)';
196    const blurTint = `color-mix(in srgb, var(--dynamic-bg) ${blurMix}, transparent)`;
197
198    boxElement.style.background = containerBackground;
199    boxElement.style.color = 'var(--dynamic-text)';
200    boxElement.style.setProperty('--dumber-border-color', borderColor);
201    // Force the omnibox to use our neutral surfaces so it stays in sync with the shell palette
202    boxElement.style.setProperty('--dumber-omnibox-surface', 'color-mix(in srgb, var(--dynamic-bg) 88%, var(--dynamic-surface) 12%)');
203
204    inputElement.style.background = inputBackground;
205    inputElement.style.color = 'var(--dynamic-text)';
206    inputElement.style.setProperty('--dumber-input-bg', inputBackground);
207    inputElement.style.setProperty('--dumber-input-border-base', borderColor);
208    inputElement.style.setProperty('--dumber-placeholder-color', 'var(--dynamic-muted)');
209
210    blurLayerElement.style.setProperty('--dumber-blur-color', blurTint);
211    blurLayerElement.style.background = 'var(--dumber-blur-color)';
212
213    let accentColor = 'var(--dynamic-accent)';
214    try {
215      const computedAccent = getComputedStyle(boxElement).getPropertyValue('--dynamic-accent');
216      if (computedAccent && computedAccent.trim()) {
217        accentColor = computedAccent.trim();
218      }
219    } catch {
220      // If computed styles fail, keep fallback accent color
221    }
222    inputElement.style.setProperty('--dumber-input-accent', accentColor);
223    boxElement.style.setProperty('--dumber-input-accent', 'var(--dynamic-muted)');
224
225    const isFocused = document.activeElement === inputElement;
226
227    if (!isFocused) {
228      inputElement.style.setProperty('--dumber-input-border-color', 'var(--dynamic-border)');
229    }
230
231    if (applyFade) {
232      blurLayerElement.style.opacity = '1';
233    } else {
234      blurLayerElement.style.opacity = '0';
235    }
236  });
237
238  // Global click handler to close omnibox when clicking anywhere outside
239  function handleGlobalClick(event: MouseEvent) {
240    if (!visible) return;
241
242    const target = event.target as HTMLElement;
243    const omniboxContainer = document.getElementById('dumber-omnibox-root');
244
245    // Close if clicking outside the omnibox container
246    if (omniboxContainer && !omniboxContainer.contains(target)) {
247      omniboxStore.close();
248    }
249  }
250
251
252
253  onMount(() => {
254    syncTheme();
255    observeThemeChanges();
256
257    console.log('🔧 Omnibox component mounted');
258
259    // Initialize responsive styles
260    updateResponsiveStyles();
261
262    // Add resize listener for responsive updates (handles zoom changes)
263    window.addEventListener('resize', updateResponsiveStyles);
264
265    // Add global click listener
266    document.addEventListener('click', handleGlobalClick, true);
267
268    try {
269      // Set up global API for Go bridge
270      window.__dumber_omnibox = {
271        setSuggestions: (suggestions) => {
272          try {
273            console.log('📋 setSuggestions called with:', suggestions?.length || 0, 'items');
274            omniboxBridge.setSuggestions(suggestions);
275          } catch (error) {
276            console.error('Error in setSuggestions:', error);
277          }
278        },
279        toggle: () => {
280          try {
281            console.log('🔄 Omnibox toggle called, current visible:', omniboxStore.visible);
282            omniboxStore.toggle();
283            console.log('🔄 After toggle, visible:', omniboxStore.visible);
284          } catch (error) {
285            console.error('Error in toggle:', error);
286          }
287        },
288        open: (mode, query) => {
289          try {
290            console.log('🚪 Omnibox open called with mode:', mode, 'query:', query);
291            omniboxStore.open(mode as any, query);
292            console.log('🚪 After open, visible:', omniboxStore.visible, 'mode:', omniboxStore.mode);
293          } catch (error) {
294            console.error('Error in open:', error);
295          }
296        },
297        close: () => {
298          try {
299            console.log('🚪 Omnibox close called');
300            omniboxStore.close();
301          } catch (error) {
302            console.error('Error in close:', error);
303          }
304        },
305        findQuery: (query) => {
306          try {
307            console.log('🔍 Find query called with:', query);
308            if (omniboxStore.mode !== 'find') {
309              omniboxStore.setMode('find');
310            }
311            if (!omniboxStore.visible) {
312              omniboxStore.setVisible(true);
313            }
314            omniboxStore.setInputValue(query || '');
315            // Find will be triggered by input value change
316          } catch (error) {
317            console.error('Error in findQuery:', error);
318          }
319        },
320        setActive: (active) => {
321          try {
322            console.log('🎯 Omnibox setActive called with:', active);
323            // For now, this is a no-op but maintains API compatibility
324            // In the future, this could control focus management or visibility
325          } catch (error) {
326            console.error('Error in setActive:', error);
327          }
328        }
329      };
330
331      console.log('✅ Omnibox API exposed:', Object.keys(window.__dumber_omnibox));
332
333      // Check for pending suggestions that arrived before API was ready
334      if ((window as any).__dumber_omnibox_pending_suggestions) {
335        console.log('🔄 [OMNIBOX] Processing pending suggestions');
336        const pending = (window as any).__dumber_omnibox_pending_suggestions;
337        omniboxBridge.setSuggestions(pending);
338        delete (window as any).__dumber_omnibox_pending_suggestions;
339      }
340
341      // Auto-open omnibox on about:blank pages
342      if (window.location.href === 'about:blank') {
343        console.log('🚀 Auto-opening omnibox for about:blank');
344        omniboxStore.open('omnibox');
345      }
346    } catch (error) {
347      console.error('❌ Failed to set up omnibox global API:', error);
348    }
349  });
350
351  onDestroy(() => {
352    // Cleanup resize listener
353    window.removeEventListener('resize', updateResponsiveStyles);
354
355    // Cleanup global click listener
356    document.removeEventListener('click', handleGlobalClick, true);
357
358    if (themeObserver) {
359      themeObserver.disconnect();
360      themeObserver = null;
361    }
362
363    // Reset omnibox state completely
364    omniboxStore.reset();
365
366    // Cleanup global API
367    if (window.__dumber_omnibox) {
368      delete window.__dumber_omnibox;
369    }
370  });
371</script>
372
373<!-- Overlay container -->
374<div
375  class="fixed inset-0 z-[2147483647]"
376  style="display: {visible ? 'block' : 'none'};
377         position: fixed !important;
378         top: 0 !important;
379         left: 0 !important;
380         right: 0 !important;
381         bottom: 0 !important;
382         z-index: 2147483647 !important;
383         pointer-events: none !important;
384         margin: 0 !important;
385         padding: 0 !important;
386         isolation: isolate;"
387  onmousedown={handleOverlayClick}
388  role="presentation"
389>
390  <!-- Main omnibox container -->
391  <div
392    bind:this={boxElement}
393    class="dumber-omnibox-container omnibox-base terminal-omnibox"
394    style="position: relative !important;
395           left: 50% !important;
396           transform: translateX(-50%) !important;
397           margin-top: 20vh !important;
398           margin-left: 0 !important;
399           margin-right: 0 !important;
400           width: {responsiveStyles.width};
401           padding: {responsiveStyles.padding};
402           font-family: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', Menlo, monospace;
403           pointer-events: auto !important;
404           box-sizing: border-box !important;
405           background: var(--dumber-omnibox-surface, color-mix(in srgb, var(--dynamic-bg) 88%, var(--dynamic-surface) 12%));
406           color: var(--dynamic-text);
407           border: 1px solid var(--dumber-border-color, var(--dynamic-border));
408           box-shadow:
409             0 20px 60px rgba(0, 0, 0, 0.45),
410             0 10px 30px rgba(0, 0, 0, 0.35),
411             0 5px 15px rgba(0, 0, 0, 0.25),
412             inset 0 0 0 1px color-mix(in srgb, var(--dynamic-border) 22%, transparent);
413           --dumber-border-color: var(--dynamic-border);"
414    onmousedown={handleBoxClick}
415    onmouseenter={handleMouseEnter}
416    role="dialog"
417    tabindex="-1"
418    aria-modal="true"
419    aria-label={mode === 'find' ? 'Find in page' : 'Omnibox search'}
420  >
421    <!-- Blur layer: absolute full-cover element providing backdrop blur.
422         We animate its opacity rather than the container's backdrop-filter
423         to avoid causing text to blur during transitions. -->
424    <div
425      bind:this={blurLayerElement}
426      class="dumber-omnibox-blur-layer"
427      aria-hidden="true"
428      style="position: absolute; inset: 0; pointer-events: none; border-radius: 0; opacity: 0;"
429    ></div>
430    <div class="dumber-omnibox-content">
431      <!-- Input component -->
432      <OmniboxInput bind:inputElement {responsiveStyles} />
433
434      <!-- Content based on mode -->
435      {#if mode === 'omnibox'}
436        <OmniboxSuggestions />
437      {:else if mode === 'find'}
438        <OmniboxFind />
439      {/if}
440    </div>
441  </div>
442</div>
443
444<style>
445  .dumber-omnibox-container {
446    background: var(--dumber-omnibox-surface, color-mix(in srgb, var(--dynamic-bg) 88%, var(--dynamic-surface) 12%));
447    color: var(--dynamic-text);
448    border: 1px solid var(--dumber-border-color, var(--dynamic-border));
449    transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
450    border-radius: 0;
451    box-shadow:
452      0 20px 60px rgba(0, 0, 0, 0.45),
453      0 10px 30px rgba(0, 0, 0, 0.35),
454      0 5px 15px rgba(0, 0, 0, 0.25),
455      inset 0 0 0 1px color-mix(in srgb, var(--dynamic-border) 22%, transparent);
456  }
457
458  .dumber-omnibox-container:focus-within {
459    border-color: color-mix(in srgb, var(--dumber-border-color, var(--dynamic-border)) 55%, var(--dynamic-text) 45%);
460    box-shadow:
461      0 25px 75px rgba(0, 0, 0, 0.5),
462      0 15px 40px rgba(0, 0, 0, 0.4),
463      0 8px 20px rgba(0, 0, 0, 0.3),
464      inset 0 0 0 1px color-mix(in srgb, var(--dynamic-border) 28%, transparent);
465  }
466
467  :global(.dumber-omnibox-container input) {
468    background: var(--dumber-input-bg, color-mix(in srgb, var(--dynamic-bg) 92%, var(--dynamic-surface) 8%));
469    color: var(--dynamic-text);
470    border: 1px solid var(--dumber-input-border-color, var(--dynamic-border));
471    transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
472    font-family:
473      "JetBrains Mono",
474      "Fira Code",
475      "SFMono-Regular",
476      Menlo,
477      monospace;
478    font-size: inherit;
479    letter-spacing: 0.05em;
480    text-transform: none;
481  }
482
483  :global(.dumber-omnibox-container input::placeholder) {
484    color: var(--dynamic-muted);
485    letter-spacing: 0.08em;
486    text-transform: uppercase;
487  }
488
489  /* Blur layer styles: use `backdrop-filter` but animate opacity only. */
490  .dumber-omnibox-blur-layer {
491    backdrop-filter: blur(1.5px) saturate(105%);
492    -webkit-backdrop-filter: blur(1.5px) saturate(105%);
493    background: var(--dumber-blur-color, color-mix(in srgb, var(--dynamic-bg) 12%, transparent));
494    transition: opacity 140ms ease;
495    will-change: opacity;
496    z-index: 0;
497    mix-blend-mode: normal;
498    border-radius: 0;
499  }
500
501  /* Ensure the actual content (input, suggestions) sits above the blur layer */
502  .dumber-omnibox-content {
503    position: relative;
504    z-index: 1;
505    /* promote to its own layer to avoid being rasterized with the blur */
506    transform: translateZ(0);
507    backface-visibility: hidden;
508  }
509  /* Additional custom styles if needed */
510</style>

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