detached 30952c4

fix(keyboard): debounce shortcuts and surface missing APIs

commit
30952c4c7d94ba0034b604e3adeded0974ca6eeb
author
bamen · · 2025-09-17 05:17 UTC
parent
0066178
signature
unsigned
4 files · +61 -42
M gui/src/components/omnibox/OmniboxFind.svelte +1 -0
@@ -87,6 +87,7 @@
8787 aria-selected={isSelected}
8888 onmouseenter={() => handleItemMouseEnter(index)}
8989 onclick={() => handleItemClick(index)}
90+ onkeydown={(e) => e.key === 'Enter' && handleItemClick(index)}
9091 >
9192 <!-- Match context -->
9293 <div
@@ -87,6 +87,7 @@
87 aria-selected={isSelected}87 aria-selected={isSelected}
88 onmouseenter={() => handleItemMouseEnter(index)}88 onmouseenter={() => handleItemMouseEnter(index)}
89 onclick={() => handleItemClick(index)}89 onclick={() => handleItemClick(index)}
90+ onkeydown={(e) => e.key === 'Enter' && handleItemClick(index)}
90 >91 >
91 <!-- Match context -->92 <!-- Match context -->
92 <div93 <div
M gui/src/injected/gui.ts +12 -8
@@ -9,6 +9,7 @@
99 import { initializeToast, type ToastConfig } from './modules/toast';
1010 import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox';
1111 import { keyboardService, type KeyboardService } from '../lib/keyboard';
12+// Note: color-scheme module is loaded separately at document-start by WebKit
1213
1314 // Global interface for the unified GUI system
1415 interface DumberGUI {
@@ -177,10 +178,10 @@ if (!window.__dumber_gui_ready) {
177178 if (window.__dumber_omnibox?.toggle) {
178179 console.log('✅ Using Svelte omnibox toggle');
179180 window.__dumber_omnibox.toggle();
180- } else {
181- console.log('⚠️ Falling back to keyboard service');
182- keyboardService.handleNativeShortcut('cmdorctrl+l');
181+ return;
183182 }
183+
184+ throw new Error('Omnibox toggle requested but API is unavailable');
184185 } catch (error) {
185186 console.error('❌ Error in __dumber_toggle:', error);
186187 }
@@ -190,9 +191,10 @@ if (!window.__dumber_gui_ready) {
190191 try {
191192 if (window.__dumber_omnibox?.open) {
192193 window.__dumber_omnibox.open('find', query);
193- } else {
194- keyboardService.handleNativeShortcut('cmdorctrl+f');
194+ return;
195195 }
196+
197+ throw new Error('Omnibox find requested but API is unavailable');
196198 } catch (error) {
197199 console.error('❌ Error in __dumber_find_open:', error);
198200 }
@@ -201,9 +203,11 @@ if (!window.__dumber_gui_ready) {
201203 window.__dumber_find_close = () => {
202204 if (window.__dumber_omnibox?.close) {
203205 window.__dumber_omnibox.close();
204- } else {
205- keyboardService.handleNativeShortcut('escape');
206+ return;
206207 }
208+
209+ const error = new Error('Omnibox close requested but API is unavailable');
210+ console.error('❌ Error in __dumber_find_close:', error);
207211 };
208212
209213 // Find query function that Go uses to set find query
@@ -229,4 +233,4 @@ if (!window.__dumber_gui_ready) {
229233 };
230234
231235 }
232-}⏎̸
236+}
@@ -9,6 +9,7 @@
9 import { initializeToast, type ToastConfig } from './modules/toast';9 import { initializeToast, type ToastConfig } from './modules/toast';
10 import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox';10 import { initializeOmnibox, type OmniboxInitConfig } from './modules/omnibox';
11 import { keyboardService, type KeyboardService } from '../lib/keyboard';11 import { keyboardService, type KeyboardService } from '../lib/keyboard';
12+// Note: color-scheme module is loaded separately at document-start by WebKit
12 13
13 // Global interface for the unified GUI system14 // Global interface for the unified GUI system
14 interface DumberGUI {15 interface DumberGUI {
@@ -177,10 +178,10 @@ if (!window.__dumber_gui_ready) {
177 if (window.__dumber_omnibox?.toggle) {178 if (window.__dumber_omnibox?.toggle) {
178 console.log('✅ Using Svelte omnibox toggle');179 console.log('✅ Using Svelte omnibox toggle');
179 window.__dumber_omnibox.toggle();180 window.__dumber_omnibox.toggle();
180- } else {181+ return;
181- console.log('⚠️ Falling back to keyboard service');
182- keyboardService.handleNativeShortcut('cmdorctrl+l');
183 }182 }
183+
184+ throw new Error('Omnibox toggle requested but API is unavailable');
184 } catch (error) {185 } catch (error) {
185 console.error('❌ Error in __dumber_toggle:', error);186 console.error('❌ Error in __dumber_toggle:', error);
186 }187 }
@@ -190,9 +191,10 @@ if (!window.__dumber_gui_ready) {
190 try {191 try {
191 if (window.__dumber_omnibox?.open) {192 if (window.__dumber_omnibox?.open) {
192 window.__dumber_omnibox.open('find', query);193 window.__dumber_omnibox.open('find', query);
193- } else {194+ return;
194- keyboardService.handleNativeShortcut('cmdorctrl+f');
195 }195 }
196+
197+ throw new Error('Omnibox find requested but API is unavailable');
196 } catch (error) {198 } catch (error) {
197 console.error('❌ Error in __dumber_find_open:', error);199 console.error('❌ Error in __dumber_find_open:', error);
198 }200 }
@@ -201,9 +203,11 @@ if (!window.__dumber_gui_ready) {
201 window.__dumber_find_close = () => {203 window.__dumber_find_close = () => {
202 if (window.__dumber_omnibox?.close) {204 if (window.__dumber_omnibox?.close) {
203 window.__dumber_omnibox.close();205 window.__dumber_omnibox.close();
204- } else {206+ return;
205- keyboardService.handleNativeShortcut('escape');
206 }207 }
208+
209+ const error = new Error('Omnibox close requested but API is unavailable');
210+ console.error('❌ Error in __dumber_find_close:', error);
207 };211 };
208 212
209 // Find query function that Go uses to set find query213 // Find query function that Go uses to set find query
@@ -229,4 +233,4 @@ if (!window.__dumber_gui_ready) {
229 };233 };
230 234
231 }235 }
232-}⏎̸236+}
M gui/src/lib/keyboard/KeyboardService.ts +7 -11
@@ -385,12 +385,10 @@ export const keyboardService = new KeyboardService({
385385 if (window.__dumber_omnibox?.toggle) {
386386 console.log('🎯 Using omnibox API directly');
387387 window.__dumber_omnibox.toggle();
388- } else if (typeof window.__dumber_toggle === 'function') {
389- console.log('🎯 Using legacy toggle function');
390- window.__dumber_toggle();
391- } else {
392- console.warn('⚠️ No omnibox toggle function available');
388+ return;
393389 }
390+
391+ console.error('❌ Omnibox toggle requested but API is unavailable');
394392 } catch (error) {
395393 console.error('❌ Error in omnibox open handler:', error);
396394 }
@@ -404,12 +402,10 @@ export const keyboardService = new KeyboardService({
404402 if (window.__dumber_omnibox?.open) {
405403 console.log('🔍 Using omnibox find API directly');
406404 window.__dumber_omnibox.open('find');
407- } else if (typeof window.__dumber_find_open === 'function') {
408- console.log('🔍 Using legacy find function');
409- window.__dumber_find_open();
410- } else {
411- console.warn('⚠️ No find function available');
405+ return;
412406 }
407+
408+ console.error('❌ Omnibox find requested but API is unavailable');
413409 } catch (error) {
414410 console.error('❌ Error in find open handler:', error);
415411 }
@@ -444,4 +440,4 @@ export const keyboardService = new KeyboardService({
444440
445441 // Export types for use in components
446442 export type { ShortcutConfig, ComponentShortcuts, KeyboardServiceConfig } from './types';
447-export { COMMON_SHORTCUTS } from './types';⏎̸
443+export { COMMON_SHORTCUTS } from './types';
@@ -385,12 +385,10 @@ export const keyboardService = new KeyboardService({
385 if (window.__dumber_omnibox?.toggle) {385 if (window.__dumber_omnibox?.toggle) {
386 console.log('🎯 Using omnibox API directly');386 console.log('🎯 Using omnibox API directly');
387 window.__dumber_omnibox.toggle();387 window.__dumber_omnibox.toggle();
388- } else if (typeof window.__dumber_toggle === 'function') {388+ return;
389- console.log('🎯 Using legacy toggle function');
390- window.__dumber_toggle();
391- } else {
392- console.warn('⚠️ No omnibox toggle function available');
393 }389 }
390+
391+ console.error('❌ Omnibox toggle requested but API is unavailable');
394 } catch (error) {392 } catch (error) {
395 console.error('❌ Error in omnibox open handler:', error);393 console.error('❌ Error in omnibox open handler:', error);
396 }394 }
@@ -404,12 +402,10 @@ export const keyboardService = new KeyboardService({
404 if (window.__dumber_omnibox?.open) {402 if (window.__dumber_omnibox?.open) {
405 console.log('🔍 Using omnibox find API directly');403 console.log('🔍 Using omnibox find API directly');
406 window.__dumber_omnibox.open('find');404 window.__dumber_omnibox.open('find');
407- } else if (typeof window.__dumber_find_open === 'function') {405+ return;
408- console.log('🔍 Using legacy find function');
409- window.__dumber_find_open();
410- } else {
411- console.warn('⚠️ No find function available');
412 }406 }
407+
408+ console.error('❌ Omnibox find requested but API is unavailable');
413 } catch (error) {409 } catch (error) {
414 console.error('❌ Error in find open handler:', error);410 console.error('❌ Error in find open handler:', error);
415 }411 }
@@ -444,4 +440,4 @@ export const keyboardService = new KeyboardService({
444 440
445 // Export types for use in components441 // Export types for use in components
446 export type { ShortcutConfig, ComponentShortcuts, KeyboardServiceConfig } from './types';442 export type { ShortcutConfig, ComponentShortcuts, KeyboardServiceConfig } from './types';
447-export { COMMON_SHORTCUTS } from './types';⏎̸443+export { COMMON_SHORTCUTS } from './types';
M pkg/webkit/keyboard_cgo.go +41 -23
@@ -96,6 +96,25 @@ var (
9696 lastKeyTime = make(map[uintptr]map[string]time.Time)
9797 )
9898
99+const shortcutDebounceWindow = 120 * time.Millisecond
100+
101+// shouldDebounceShortcut returns true when the given key should be skipped
102+// because it was delivered within the debounce window for the same view.
103+func shouldDebounceShortcut(uid uintptr, key string, now time.Time) bool {
104+ regMu.Lock()
105+ defer regMu.Unlock()
106+
107+ if _, ok := lastKeyTime[uid]; !ok {
108+ lastKeyTime[uid] = make(map[string]time.Time)
109+ }
110+ last := lastKeyTime[uid][key]
111+ if now.Sub(last) < shortcutDebounceWindow {
112+ return true
113+ }
114+ lastKeyTime[uid][key] = now
115+ return false
116+}
117+
99118 // Locale hint function removed; no layout-specific remaps.
100119
101120 // RegisterKeyboardShortcut registers a callback under an accelerator string.
@@ -192,8 +211,9 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
192211 normalized = ""
193212 }
194213
214+ now := time.Now()
195215 // Forward normalized shortcut to GUI KeyboardService via DOM event if present
196- if normalized != "" {
216+ if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) {
197217 regMu.RLock()
198218 vw := viewByID[uid]
199219 regMu.RUnlock()
@@ -210,32 +230,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
210230 }
211231 for _, name := range candidates {
212232 if cb, ok := reg[name]; ok {
213- // Debounce duplicates within 120ms for the same view+key
214- regMu.Lock()
215- if _, ok := lastKeyTime[uid]; !ok {
216- lastKeyTime[uid] = make(map[string]time.Time)
217- }
218- last := lastKeyTime[uid][name]
219- now := time.Now()
220- if now.Sub(last) < 120*time.Millisecond {
221- // Skip duplicate
222- regMu.Unlock()
233+ if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
223234 return
224235 }
225- lastKeyTime[uid][name] = now
226- regMu.Unlock()
227236 log.Printf("[accelerator] %s", name)
228237 cb()
229- // Additionally forward well-known GUI shortcuts to the in-page GUI via a DOM event
230- // so the isolated user content world KeyboardService can handle them uniformly.
231- if name == "cmdorctrl+l" || name == "cmdorctrl+f" || name == "cmdorctrl+shift+c" {
232- regMu.RLock()
233- vw := viewByID[uid]
234- regMu.RUnlock()
235- if vw != nil {
236- _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", name))
237- }
238- }
239238 break
240239 }
241240 }
@@ -337,6 +336,25 @@ m.setAttribute('content', d ? 'dark light' : 'light dark');
337336 let s=document.getElementById('__dumber_theme_style');
338337 if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
339338 s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
339+// Call the unified theme setter for Tailwind dark mode
340+if (window.__dumber_setTheme) {
341+ window.__dumber_setTheme(cs);
342+} else {
343+ // Retry after a short delay in case bridge hasn't loaded yet
344+ setTimeout(() => {
345+ if (window.__dumber_setTheme) {
346+ window.__dumber_setTheme(cs);
347+ } else {
348+ // Fallback: directly apply dark class
349+ console.warn('[dumber] Theme setter not available, using fallback');
350+ if (d) {
351+ document.documentElement.classList.add('dark');
352+ } else {
353+ document.documentElement.classList.remove('dark');
354+ }
355+ }
356+ }, 100);
357+}
340358 } catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
341359 _ = vw.InjectScript(js)
342360 }
@@ -96,6 +96,25 @@ var (
96 lastKeyTime = make(map[uintptr]map[string]time.Time)96 lastKeyTime = make(map[uintptr]map[string]time.Time)
97 )97 )
98 98
99+const shortcutDebounceWindow = 120 * time.Millisecond
100+
101+// shouldDebounceShortcut returns true when the given key should be skipped
102+// because it was delivered within the debounce window for the same view.
103+func shouldDebounceShortcut(uid uintptr, key string, now time.Time) bool {
104+ regMu.Lock()
105+ defer regMu.Unlock()
106+
107+ if _, ok := lastKeyTime[uid]; !ok {
108+ lastKeyTime[uid] = make(map[string]time.Time)
109+ }
110+ last := lastKeyTime[uid][key]
111+ if now.Sub(last) < shortcutDebounceWindow {
112+ return true
113+ }
114+ lastKeyTime[uid][key] = now
115+ return false
116+}
117+
99 // Locale hint function removed; no layout-specific remaps.118 // Locale hint function removed; no layout-specific remaps.
100 119
101 // RegisterKeyboardShortcut registers a callback under an accelerator string.120 // RegisterKeyboardShortcut registers a callback under an accelerator string.
@@ -192,8 +211,9 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
192 normalized = ""211 normalized = ""
193 }212 }
194 213
214+ now := time.Now()
195 // Forward normalized shortcut to GUI KeyboardService via DOM event if present215 // Forward normalized shortcut to GUI KeyboardService via DOM event if present
196- if normalized != "" {216+ if normalized != "" && !shouldDebounceShortcut(uid, "dom:"+normalized, now) {
197 regMu.RLock()217 regMu.RLock()
198 vw := viewByID[uid]218 vw := viewByID[uid]
199 regMu.RUnlock()219 regMu.RUnlock()
@@ -210,32 +230,11 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) {
210 }230 }
211 for _, name := range candidates {231 for _, name := range candidates {
212 if cb, ok := reg[name]; ok {232 if cb, ok := reg[name]; ok {
213- // Debounce duplicates within 120ms for the same view+key233+ if shouldDebounceShortcut(uid, "shortcut:"+name, now) {
214- regMu.Lock()
215- if _, ok := lastKeyTime[uid]; !ok {
216- lastKeyTime[uid] = make(map[string]time.Time)
217- }
218- last := lastKeyTime[uid][name]
219- now := time.Now()
220- if now.Sub(last) < 120*time.Millisecond {
221- // Skip duplicate
222- regMu.Unlock()
223 return234 return
224 }235 }
225- lastKeyTime[uid][name] = now
226- regMu.Unlock()
227 log.Printf("[accelerator] %s", name)236 log.Printf("[accelerator] %s", name)
228 cb()237 cb()
229- // Additionally forward well-known GUI shortcuts to the in-page GUI via a DOM event
230- // so the isolated user content world KeyboardService can handle them uniformly.
231- if name == "cmdorctrl+l" || name == "cmdorctrl+f" || name == "cmdorctrl+shift+c" {
232- regMu.RLock()
233- vw := viewByID[uid]
234- regMu.RUnlock()
235- if vw != nil {
236- _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", name))
237- }
238- }
239 break238 break
240 }239 }
241 }240 }
@@ -337,6 +336,25 @@ m.setAttribute('content', d ? 'dark light' : 'light dark');
337 let s=document.getElementById('__dumber_theme_style');336 let s=document.getElementById('__dumber_theme_style');
338 if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }337 if(!s){ s=document.createElement('style'); s.id='__dumber_theme_style'; document.documentElement.appendChild(s); }
339 s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';338 s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}';
339+// Call the unified theme setter for Tailwind dark mode
340+if (window.__dumber_setTheme) {
341+ window.__dumber_setTheme(cs);
342+} else {
343+ // Retry after a short delay in case bridge hasn't loaded yet
344+ setTimeout(() => {
345+ if (window.__dumber_setTheme) {
346+ window.__dumber_setTheme(cs);
347+ } else {
348+ // Fallback: directly apply dark class
349+ console.warn('[dumber] Theme setter not available, using fallback');
350+ if (d) {
351+ document.documentElement.classList.add('dark');
352+ } else {
353+ document.documentElement.classList.remove('dark');
354+ }
355+ }
356+ }, 100);
357+}
340 } catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)358 } catch(e) { console.warn('[dumber] theme runtime update failed', e); } })();`, dval)
341 _ = vw.InjectScript(js)359 _ = vw.InjectScript(js)
342 }360 }

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