feat(controls): enhance keyboard shortcuts and webkit integration with unified event bridge
- commit
- 89bee03bbc32228de39bb5be5139aceac15dfab1
- author
- bamen · · 2025-09-15 21:18 UTC
- committer
- bamen · · 2025-09-15 21:22 UTC
- parent
- 1eff705
- signature
- unsigned
7 files · +64 -17
M gui/src/lib/keyboard/KeyboardService.ts +1 -0
| @@ -21,6 +21,7 @@ export class KeyboardService { | |||
| 21 | 21 | private globalShortcuts: ShortcutConfig[] = []; | |
| 22 | 22 | private eventListeners = new Map<KeyboardEventType, Set<(event: KeyboardEvent) => void>>(); | |
| 23 | 23 | private debug: boolean = false; | |
| 24 | + | // Mode-aware keymaps (e.g., 'vim', 'emacs') are deferred for now | |
| 24 | 25 | ||
| 25 | 26 | constructor(config: KeyboardServiceConfig = {}) { | |
| 26 | 27 | this.debug = config.debug ?? false; | |
| @@ -21,6 +21,7 @@ export class KeyboardService { | |||||
| 21 | private globalShortcuts: ShortcutConfig[] = []; | 21 | private globalShortcuts: ShortcutConfig[] = []; | ||
| 22 | private eventListeners = new Map<KeyboardEventType, Set<(event: KeyboardEvent) => void>>(); | 22 | private eventListeners = new Map<KeyboardEventType, Set<(event: KeyboardEvent) => void>>(); | ||
| 23 | private debug: boolean = false; | 23 | private debug: boolean = false; | ||
| 24 | + | // Mode-aware keymaps (e.g., 'vim', 'emacs') are deferred for now | |||
| 24 | 25 | ||||
| 25 | constructor(config: KeyboardServiceConfig = {}) { | 26 | constructor(config: KeyboardServiceConfig = {}) { | ||
| 26 | this.debug = config.debug ?? false; | 27 | this.debug = config.debug ?? false; | ||
M internal/app/browser/shortcuts.go +6 -6
| @@ -31,20 +31,20 @@ func (s *ShortcutHandler) RegisterShortcuts() { | |||
| 31 | 31 | ||
| 32 | 32 | // Omnibox (Ctrl+L): use new keyboard service bridge | |
| 33 | 33 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() { | |
| 34 | - | log.Printf("Shortcut: Omnibox toggle via keyboard service") | |
| 35 | - | _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+l')") | |
| 34 | + | log.Printf("Shortcut: Omnibox toggle") | |
| 35 | + | // Handled in GUI KeyboardService via unified 'dumber:key' event | |
| 36 | 36 | }) | |
| 37 | 37 | ||
| 38 | 38 | // Find in page (Ctrl+F): use keyboard service bridge | |
| 39 | 39 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+f", func() { | |
| 40 | - | log.Printf("Shortcut: Find in page via keyboard service") | |
| 41 | - | _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')") | |
| 40 | + | log.Printf("Shortcut: Find in page") | |
| 41 | + | // Handled in GUI KeyboardService via unified 'dumber:key' event | |
| 42 | 42 | }) | |
| 43 | 43 | ||
| 44 | 44 | // Copy URL (Ctrl+Shift+C): use keyboard service with native fallback | |
| 45 | 45 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() { | |
| 46 | - | log.Printf("Shortcut: Copy URL via keyboard service") | |
| 47 | - | _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+shift+c')") | |
| 46 | + | log.Printf("Shortcut: Copy URL") | |
| 47 | + | // Handled in GUI KeyboardService via unified 'dumber:key' event | |
| 48 | 48 | }) | |
| 49 | 49 | ||
| 50 | 50 | // Page refresh shortcuts | |
| @@ -31,20 +31,20 @@ func (s *ShortcutHandler) RegisterShortcuts() { | |||||
| 31 | 31 | ||||
| 32 | // Omnibox (Ctrl+L): use new keyboard service bridge | 32 | // Omnibox (Ctrl+L): use new keyboard service bridge | ||
| 33 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() { | 33 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+l", func() { | ||
| 34 | - | log.Printf("Shortcut: Omnibox toggle via keyboard service") | 34 | + | log.Printf("Shortcut: Omnibox toggle") |
| 35 | - | _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+l')") | 35 | + | // Handled in GUI KeyboardService via unified 'dumber:key' event |
| 36 | }) | 36 | }) | ||
| 37 | 37 | ||||
| 38 | // Find in page (Ctrl+F): use keyboard service bridge | 38 | // Find in page (Ctrl+F): use keyboard service bridge | ||
| 39 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+f", func() { | 39 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+f", func() { | ||
| 40 | - | log.Printf("Shortcut: Find in page via keyboard service") | 40 | + | log.Printf("Shortcut: Find in page") |
| 41 | - | _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+f')") | 41 | + | // Handled in GUI KeyboardService via unified 'dumber:key' event |
| 42 | }) | 42 | }) | ||
| 43 | 43 | ||||
| 44 | // Copy URL (Ctrl+Shift+C): use keyboard service with native fallback | 44 | // Copy URL (Ctrl+Shift+C): use keyboard service with native fallback | ||
| 45 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() { | 45 | _ = s.webView.RegisterKeyboardShortcut("cmdorctrl+shift+c", func() { | ||
| 46 | - | log.Printf("Shortcut: Copy URL via keyboard service") | 46 | + | log.Printf("Shortcut: Copy URL") |
| 47 | - | _ = s.webView.InjectScript("window.__dumber_keyboard && window.__dumber_keyboard.handleNativeShortcut('cmdorctrl+shift+c')") | 47 | + | // Handled in GUI KeyboardService via unified 'dumber:key' event |
| 48 | }) | 48 | }) | ||
| 49 | 49 | ||||
| 50 | // Page refresh shortcuts | 50 | // Page refresh shortcuts | ||
M internal/app/control/clipboard.go +3 -3
| @@ -25,15 +25,15 @@ func (c *ClipboardController) CopyCurrentURL() { | |||
| 25 | 25 | currentURL := c.webView.GetCurrentURL() | |
| 26 | 26 | if currentURL == "" { | |
| 27 | 27 | log.Printf("No URL to copy") | |
| 28 | - | _ = c.webView.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("No URL to copy", 2000)`) | |
| 28 | + | _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("No URL to copy", 2000) : (window.__dumber_showToast && window.__dumber_showToast("No URL to copy", 2000)))`) | |
| 29 | 29 | return | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | if err := clipboard.CopyToClipboard(currentURL); err != nil { | |
| 33 | 33 | log.Printf("Failed to copy URL to clipboard: %v", err) | |
| 34 | - | _ = c.webView.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("Failed to copy URL", 2000)`) | |
| 34 | + | _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("Failed to copy URL", 2000) : (window.__dumber_showToast && window.__dumber_showToast("Failed to copy URL", 2000)))`) | |
| 35 | 35 | } else { | |
| 36 | 36 | log.Printf("URL copied to clipboard: %s", currentURL) | |
| 37 | - | _ = c.webView.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`) | |
| 37 | + | _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("URL copied to clipboard", 2000) : (window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)))`) | |
| 38 | 38 | } | |
| 39 | 39 | } | |
| @@ -25,15 +25,15 @@ func (c *ClipboardController) CopyCurrentURL() { | |||||
| 25 | currentURL := c.webView.GetCurrentURL() | 25 | currentURL := c.webView.GetCurrentURL() | ||
| 26 | if currentURL == "" { | 26 | if currentURL == "" { | ||
| 27 | log.Printf("No URL to copy") | 27 | log.Printf("No URL to copy") | ||
| 28 | - | _ = c.webView.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("No URL to copy", 2000)`) | 28 | + | _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("No URL to copy", 2000) : (window.__dumber_showToast && window.__dumber_showToast("No URL to copy", 2000)))`) |
| 29 | return | 29 | return | ||
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | if err := clipboard.CopyToClipboard(currentURL); err != nil { | 32 | if err := clipboard.CopyToClipboard(currentURL); err != nil { | ||
| 33 | log.Printf("Failed to copy URL to clipboard: %v", err) | 33 | log.Printf("Failed to copy URL to clipboard: %v", err) | ||
| 34 | - | _ = c.webView.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("Failed to copy URL", 2000)`) | 34 | + | _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("Failed to copy URL", 2000) : (window.__dumber_showToast && window.__dumber_showToast("Failed to copy URL", 2000)))`) |
| 35 | } else { | 35 | } else { | ||
| 36 | log.Printf("URL copied to clipboard: %s", currentURL) | 36 | log.Printf("URL copied to clipboard: %s", currentURL) | ||
| 37 | - | _ = c.webView.InjectScript(`window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)`) | 37 | + | _ = c.webView.InjectScript(`(window.__dumber?.toast?.show ? window.__dumber.toast.show("URL copied to clipboard", 2000) : (window.__dumber_showToast && window.__dumber_showToast("URL copied to clipboard", 2000)))`) |
| 38 | } | 38 | } | ||
| 39 | } | 39 | } | ||
M internal/app/control/zoom.go +12 -4
| @@ -100,13 +100,21 @@ func (z *ZoomController) showZoomToast(level float64) { | |||
| 100 | 100 | const percentage = %d; | |
| 101 | 101 | let toastShown = false; | |
| 102 | 102 | ||
| 103 | - | // Strategy 1: Try the Svelte toast system first | |
| 104 | - | if (typeof window.__dumber_showZoomToast === 'function') { | |
| 105 | - | console.log('[dumber] Using Svelte zoom toast system'); | |
| 103 | + | // Prefer unified page-world API | |
| 104 | + | if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.zoom === 'function') { | |
| 105 | + | console.log('[dumber] Using unified toast.zoom'); | |
| 106 | + | window.__dumber.toast.zoom(zoomLevel); | |
| 107 | + | toastShown = true; | |
| 108 | + | } else if (typeof window.__dumber_showZoomToast === 'function') { | |
| 109 | + | console.log('[dumber] Using legacy zoom toast'); | |
| 106 | 110 | window.__dumber_showZoomToast(zoomLevel); | |
| 107 | 111 | toastShown = true; | |
| 112 | + | } else if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.show === 'function') { | |
| 113 | + | console.log('[dumber] Using unified toast.show'); | |
| 114 | + | window.__dumber.toast.show('Zoom: ' + percentage + '%%', 1500, 'info'); | |
| 115 | + | toastShown = true; | |
| 108 | 116 | } else if (typeof window.__dumber_showToast === 'function') { | |
| 109 | - | console.log('[dumber] Using Svelte general toast system'); | |
| 117 | + | console.log('[dumber] Using legacy toast.show'); | |
| 110 | 118 | window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info'); | |
| 111 | 119 | toastShown = true; | |
| 112 | 120 | } | |
| @@ -100,13 +100,21 @@ func (z *ZoomController) showZoomToast(level float64) { | |||||
| 100 | const percentage = %d; | 100 | const percentage = %d; | ||
| 101 | let toastShown = false; | 101 | let toastShown = false; | ||
| 102 | 102 | ||||
| 103 | - | // Strategy 1: Try the Svelte toast system first | 103 | + | // Prefer unified page-world API |
| 104 | - | if (typeof window.__dumber_showZoomToast === 'function') { | 104 | + | if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.zoom === 'function') { |
| 105 | - | console.log('[dumber] Using Svelte zoom toast system'); | 105 | + | console.log('[dumber] Using unified toast.zoom'); |
| 106 | + | window.__dumber.toast.zoom(zoomLevel); | |||
| 107 | + | toastShown = true; | |||
| 108 | + | } else if (typeof window.__dumber_showZoomToast === 'function') { | |||
| 109 | + | console.log('[dumber] Using legacy zoom toast'); | |||
| 106 | window.__dumber_showZoomToast(zoomLevel); | 110 | window.__dumber_showZoomToast(zoomLevel); | ||
| 107 | toastShown = true; | 111 | toastShown = true; | ||
| 112 | + | } else if (window.__dumber && window.__dumber.toast && typeof window.__dumber.toast.show === 'function') { | |||
| 113 | + | console.log('[dumber] Using unified toast.show'); | |||
| 114 | + | window.__dumber.toast.show('Zoom: ' + percentage + '%%', 1500, 'info'); | |||
| 115 | + | toastShown = true; | |||
| 108 | } else if (typeof window.__dumber_showToast === 'function') { | 116 | } else if (typeof window.__dumber_showToast === 'function') { | ||
| 109 | - | console.log('[dumber] Using Svelte general toast system'); | 117 | + | console.log('[dumber] Using legacy toast.show'); |
| 110 | window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info'); | 118 | window.__dumber_showToast('Zoom: ' + percentage + '%%', 1500, 'info'); | ||
| 111 | toastShown = true; | 119 | toastShown = true; | ||
| 112 | } | 120 | } | ||
M pkg/webkit/find.go +4 -3
| @@ -11,7 +11,8 @@ func (w *WebView) OpenFind(initial string) error { | |||
| 11 | 11 | return ErrNotImplemented | |
| 12 | 12 | } | |
| 13 | 13 | quoted := strconv.Quote(initial) | |
| 14 | - | js := fmt.Sprintf("window.__dumber_find_open && window.__dumber_find_open(%s)", quoted) | |
| 14 | + | // Prefer unified omnibox open via keyboard shortcut; fallback to legacy helper | |
| 15 | + | js := fmt.Sprintf("(window.__dumber?.omnibox?.open ? window.__dumber.omnibox.open('find', %s) : (window.__dumber_find_open && window.__dumber_find_open(%s)))", quoted, quoted) | |
| 15 | 16 | return w.InjectScript(js) | |
| 16 | 17 | } | |
| 17 | 18 | ||
| @@ -21,7 +22,7 @@ func (w *WebView) FindQuery(q string) error { | |||
| 21 | 22 | return ErrNotImplemented | |
| 22 | 23 | } | |
| 23 | 24 | quoted := strconv.Quote(q) | |
| 24 | - | js := fmt.Sprintf("window.__dumber_find_query && window.__dumber_find_query(%s)", quoted) | |
| 25 | + | js := fmt.Sprintf("(window.__dumber?.omnibox?.findQuery ? window.__dumber.omnibox.findQuery(%s) : (window.__dumber_find_query && window.__dumber_find_query(%s)))", quoted, quoted) | |
| 25 | 26 | return w.InjectScript(js) | |
| 26 | 27 | } | |
| 27 | 28 | ||
| @@ -30,5 +31,5 @@ func (w *WebView) CloseFind() error { | |||
| 30 | 31 | if w == nil { | |
| 31 | 32 | return ErrNotImplemented | |
| 32 | 33 | } | |
| 33 | - | return w.InjectScript("window.__dumber_find_close && window.__dumber_find_close()") | |
| 34 | + | return w.InjectScript("(window.__dumber?.omnibox?.close ? window.__dumber.omnibox.close() : (window.__dumber_find_close && window.__dumber_find_close()))") | |
| 34 | 35 | } | |
| @@ -11,7 +11,8 @@ func (w *WebView) OpenFind(initial string) error { | |||||
| 11 | return ErrNotImplemented | 11 | return ErrNotImplemented | ||
| 12 | } | 12 | } | ||
| 13 | quoted := strconv.Quote(initial) | 13 | quoted := strconv.Quote(initial) | ||
| 14 | - | js := fmt.Sprintf("window.__dumber_find_open && window.__dumber_find_open(%s)", quoted) | 14 | + | // Prefer unified omnibox open via keyboard shortcut; fallback to legacy helper |
| 15 | + | js := fmt.Sprintf("(window.__dumber?.omnibox?.open ? window.__dumber.omnibox.open('find', %s) : (window.__dumber_find_open && window.__dumber_find_open(%s)))", quoted, quoted) | |||
| 15 | return w.InjectScript(js) | 16 | return w.InjectScript(js) | ||
| 16 | } | 17 | } | ||
| 17 | 18 | ||||
| @@ -21,7 +22,7 @@ func (w *WebView) FindQuery(q string) error { | |||||
| 21 | return ErrNotImplemented | 22 | return ErrNotImplemented | ||
| 22 | } | 23 | } | ||
| 23 | quoted := strconv.Quote(q) | 24 | quoted := strconv.Quote(q) | ||
| 24 | - | js := fmt.Sprintf("window.__dumber_find_query && window.__dumber_find_query(%s)", quoted) | 25 | + | js := fmt.Sprintf("(window.__dumber?.omnibox?.findQuery ? window.__dumber.omnibox.findQuery(%s) : (window.__dumber_find_query && window.__dumber_find_query(%s)))", quoted, quoted) |
| 25 | return w.InjectScript(js) | 26 | return w.InjectScript(js) | ||
| 26 | } | 27 | } | ||
| 27 | 28 | ||||
| @@ -30,5 +31,5 @@ func (w *WebView) CloseFind() error { | |||||
| 30 | if w == nil { | 31 | if w == nil { | ||
| 31 | return ErrNotImplemented | 32 | return ErrNotImplemented | ||
| 32 | } | 33 | } | ||
| 33 | - | return w.InjectScript("window.__dumber_find_close && window.__dumber_find_close()") | 34 | + | return w.InjectScript("(window.__dumber?.omnibox?.close ? window.__dumber.omnibox.close() : (window.__dumber_find_close && window.__dumber_find_close()))") |
| 34 | } | 35 | } | ||
M pkg/webkit/keyboard_cgo.go +34 -1
| @@ -178,6 +178,30 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 178 | 178 | candidates = append([]string{"alt+" + keyName}, candidates...) | |
| 179 | 179 | } | |
| 180 | 180 | ||
| 181 | + | // Compute a single normalized shortcut string to forward to GUI (single source of truth) | |
| 182 | + | normalized := "" | |
| 183 | + | if ctrl && shift { | |
| 184 | + | normalized = "cmdorctrl+shift+" + keyName | |
| 185 | + | } else if ctrl { | |
| 186 | + | normalized = "cmdorctrl+" + keyName | |
| 187 | + | } else if alt { | |
| 188 | + | normalized = "alt+" + keyName | |
| 189 | + | } else if keyName != "" { | |
| 190 | + | // For future modes like Vim, we may want to forward non-modified keys | |
| 191 | + | // For now, only forward if a modifier is present to avoid noise | |
| 192 | + | normalized = "" | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // Forward normalized shortcut to GUI KeyboardService via DOM event if present | |
| 196 | + | if normalized != "" { | |
| 197 | + | regMu.RLock() | |
| 198 | + | vw := viewByID[uid] | |
| 199 | + | regMu.RUnlock() | |
| 200 | + | if vw != nil { | |
| 201 | + | _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", normalized)) | |
| 202 | + | } | |
| 203 | + | } | |
| 204 | + | ||
| 181 | 205 | regMu.RLock() | |
| 182 | 206 | reg := viewShortcuts[uid] | |
| 183 | 207 | regMu.RUnlock() | |
| @@ -202,6 +226,16 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||
| 202 | 226 | regMu.Unlock() | |
| 203 | 227 | log.Printf("[accelerator] %s", name) | |
| 204 | 228 | 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 | + | } | |
| 205 | 239 | break | |
| 206 | 240 | } | |
| 207 | 241 | } | |
| @@ -307,7 +341,6 @@ s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}'; | |||
| 307 | 341 | _ = vw.InjectScript(js) | |
| 308 | 342 | } | |
| 309 | 343 | ||
| 310 | - | ||
| 311 | 344 | //export goOnUcmMessage | |
| 312 | 345 | func goOnUcmMessage(id C.ulong, json *C.char) { | |
| 313 | 346 | uid := uintptr(id) | |
| @@ -178,6 +178,30 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 178 | candidates = append([]string{"alt+" + keyName}, candidates...) | 178 | candidates = append([]string{"alt+" + keyName}, candidates...) | ||
| 179 | } | 179 | } | ||
| 180 | 180 | ||||
| 181 | + | // Compute a single normalized shortcut string to forward to GUI (single source of truth) | |||
| 182 | + | normalized := "" | |||
| 183 | + | if ctrl && shift { | |||
| 184 | + | normalized = "cmdorctrl+shift+" + keyName | |||
| 185 | + | } else if ctrl { | |||
| 186 | + | normalized = "cmdorctrl+" + keyName | |||
| 187 | + | } else if alt { | |||
| 188 | + | normalized = "alt+" + keyName | |||
| 189 | + | } else if keyName != "" { | |||
| 190 | + | // For future modes like Vim, we may want to forward non-modified keys | |||
| 191 | + | // For now, only forward if a modifier is present to avoid noise | |||
| 192 | + | normalized = "" | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | // Forward normalized shortcut to GUI KeyboardService via DOM event if present | |||
| 196 | + | if normalized != "" { | |||
| 197 | + | regMu.RLock() | |||
| 198 | + | vw := viewByID[uid] | |||
| 199 | + | regMu.RUnlock() | |||
| 200 | + | if vw != nil { | |||
| 201 | + | _ = vw.InjectScript(fmt.Sprintf("document.dispatchEvent(new CustomEvent('dumber:key',{detail:{shortcut:'%s'}}));", normalized)) | |||
| 202 | + | } | |||
| 203 | + | } | |||
| 204 | + | ||||
| 181 | regMu.RLock() | 205 | regMu.RLock() | ||
| 182 | reg := viewShortcuts[uid] | 206 | reg := viewShortcuts[uid] | ||
| 183 | regMu.RUnlock() | 207 | regMu.RUnlock() | ||
| @@ -202,6 +226,16 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) { | |||||
| 202 | regMu.Unlock() | 226 | regMu.Unlock() | ||
| 203 | log.Printf("[accelerator] %s", name) | 227 | log.Printf("[accelerator] %s", name) | ||
| 204 | cb() | 228 | 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 | + | } | |||
| 205 | break | 239 | break | ||
| 206 | } | 240 | } | ||
| 207 | } | 241 | } | ||
| @@ -307,7 +341,6 @@ s.textContent=':root{color-scheme:' + (d?'dark':'light') + ';}'; | |||||
| 307 | _ = vw.InjectScript(js) | 341 | _ = vw.InjectScript(js) | ||
| 308 | } | 342 | } | ||
| 309 | 343 | ||||
| 310 | - | ||||
| 311 | //export goOnUcmMessage | 344 | //export goOnUcmMessage | ||
| 312 | func goOnUcmMessage(id C.ulong, json *C.char) { | 345 | func goOnUcmMessage(id C.ulong, json *C.char) { | ||
| 313 | uid := uintptr(id) | 346 | uid := uintptr(id) | ||
M pkg/webkit/settings.go +4 -0
| @@ -40,6 +40,10 @@ type Config struct { | |||
| 40 | 40 | ||
| 41 | 41 | // CodecPreferences for media playback | |
| 42 | 42 | CodecPreferences CodecPreferencesConfig | |
| 43 | + | ||
| 44 | + | // APIToken holds the per-session token used to authenticate dumb://api requests | |
| 45 | + | // It's optional; if empty, API token enforcement may be disabled by the app. | |
| 46 | + | APIToken string | |
| 43 | 47 | } | |
| 44 | 48 | ||
| 45 | 49 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | |
| @@ -40,6 +40,10 @@ type Config struct { | |||||
| 40 | 40 | ||||
| 41 | // CodecPreferences for media playback | 41 | // CodecPreferences for media playback | ||
| 42 | CodecPreferences CodecPreferencesConfig | 42 | CodecPreferences CodecPreferencesConfig | ||
| 43 | + | ||||
| 44 | + | // APIToken holds the per-session token used to authenticate dumb://api requests | |||
| 45 | + | // It's optional; if empty, API token enforcement may be disabled by the app. | |||
| 46 | + | APIToken string | |||
| 43 | } | 47 | } | ||
| 44 | 48 | ||||
| 45 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | 49 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | ||