detached d7fd740

fix(omnibox): implement webkit-level keyboard event isolation to prevent page interference

commit
d7fd74078c85f63bbb678567c9afed014a105032
author
bamen · · 2025-09-23 04:42 UTC
parent
df4e539
signature
unsigned
7 files · +204 -1
M CHANGELOG.md +1 -0
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
55 ## [Unreleased]
66
77 ### Fixed
8+- **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality
89 - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement
910 - **Workspace architecture**: Simplified focus tracking by removing URI change handlers and pane ID injection complexity, improving reliability and reducing codebase by 85 lines
1011 - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
5 ## [Unreleased]5 ## [Unreleased]
6 6
7 ### Fixed7 ### Fixed
8+- **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality
8 - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement9 - **Active pane detection**: Replaced complex pane ID abstraction with persistent webview IDs, fixing omnibox unresponsiveness after navigation. Omnibox now works immediately after page navigation without requiring mouse movement
9 - **Workspace architecture**: Simplified focus tracking by removing URI change handlers and pane ID injection complexity, improving reliability and reducing codebase by 85 lines10 - **Workspace architecture**: Simplified focus tracking by removing URI change handlers and pane ID injection complexity, improving reliability and reducing codebase by 85 lines
10 - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation11 - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation
M gui/package-lock.json +15 -0 generated file: collapsed
M gui/src/components/omnibox/Omnibox.svelte +37 -1
@@ -116,13 +116,47 @@
116116 };
117117 }
118118
119- // Focus input when component becomes visible
119+ // Focus input when component becomes visible and manage page event blocking
120120 $effect(() => {
121121 if (visible && inputElement) {
122122 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();
123128 }
124129 });
125130
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+
126160
127161 // Apply faded styling effect (matching original JS implementation)
128162 // NOTE: to avoid text becoming blurry while animating, we animate the
@@ -218,6 +252,8 @@
218252 }
219253 }
220254
255+
256+
221257 onMount(() => {
222258 syncTheme();
223259 observeThemeChanges();
@@ -116,13 +116,47 @@
116 };116 };
117 }117 }
118 118
119- // Focus input when component becomes visible119+ // Focus input when component becomes visible and manage page event blocking
120 $effect(() => {120 $effect(() => {
121 if (visible && inputElement) {121 if (visible && inputElement) {
122 inputElement.focus();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();
123 }128 }
124 });129 });
125 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+
126 160
127 // Apply faded styling effect (matching original JS implementation)161 // Apply faded styling effect (matching original JS implementation)
128 // NOTE: to avoid text becoming blurry while animating, we animate the162 // NOTE: to avoid text becoming blurry while animating, we animate the
@@ -218,6 +252,8 @@
218 }252 }
219 }253 }
220 254
255+
256+
221 onMount(() => {257 onMount(() => {
222 syncTheme();258 syncTheme();
223 observeThemeChanges();259 observeThemeChanges();
M gui/src/components/omnibox/OmniboxInput.svelte +6 -0
@@ -69,11 +69,14 @@
6969 function handleKeyDown(event: KeyboardEvent) {
7070 switch (event.key) {
7171 case 'Escape':
72+ event.preventDefault();
73+ event.stopPropagation();
7274 omniboxStore.close();
7375 break;
7476
7577 case 'Enter':
7678 event.preventDefault();
79+ event.stopPropagation();
7780 handleEnterKey(event);
7881 break;
7982
@@ -85,6 +88,9 @@
8588 break;
8689
8790 default:
91+ // For normal typing keys, only stop propagation to prevent page handlers
92+ // but don't prevent default so typing still works in the input
93+ event.stopPropagation();
8894 // Any other key should restore full opacity
8995 omniboxStore.setFaded(false);
9096 break;
@@ -69,11 +69,14 @@
69 function handleKeyDown(event: KeyboardEvent) {69 function handleKeyDown(event: KeyboardEvent) {
70 switch (event.key) {70 switch (event.key) {
71 case 'Escape':71 case 'Escape':
72+ event.preventDefault();
73+ event.stopPropagation();
72 omniboxStore.close();74 omniboxStore.close();
73 break;75 break;
74 76
75 case 'Enter':77 case 'Enter':
76 event.preventDefault();78 event.preventDefault();
79+ event.stopPropagation();
77 handleEnterKey(event);80 handleEnterKey(event);
78 break;81 break;
79 82
@@ -85,6 +88,9 @@
85 break;88 break;
86 89
87 default:90 default:
91+ // For normal typing keys, only stop propagation to prevent page handlers
92+ // but don't prevent default so typing still works in the input
93+ event.stopPropagation();
88 // Any other key should restore full opacity94 // Any other key should restore full opacity
89 omniboxStore.setFaded(false);95 omniboxStore.setFaded(false);
90 break;96 break;
M pkg/webkit/keyboard_cgo.go +42 -0
@@ -408,6 +408,11 @@ func goOnUcmMessage(id C.ulong, json *C.char) {
408408 return
409409 }
410410
411+ // Handle keyboard blocking messages
412+ if handleKeyboardBlockingMessage(vw, goPayload) {
413+ return
414+ }
415+
411416 vw.dispatchScriptMessage(goPayload)
412417 }
413418
@@ -684,3 +689,40 @@ func handleRenderingBackendMessage(payload string) bool {
684689
685690 return true
686691 }
692+
693+// handleKeyboardBlockingMessage handles keyboard blocking control messages from omnibox
694+func handleKeyboardBlockingMessage(vw *WebView, payload string) bool {
695+ if vw == nil {
696+ return false
697+ }
698+
699+ var msg struct {
700+ Type string `json:"type"`
701+ Action string `json:"action"`
702+ }
703+
704+ if err := json.Unmarshal([]byte(payload), &msg); err != nil {
705+ return false
706+ }
707+
708+ if msg.Type != "keyboard_blocking" {
709+ return false
710+ }
711+
712+ switch msg.Action {
713+ case "enable":
714+ log.Printf("[webkit] Enabling page keyboard blocking for omnibox")
715+ if err := vw.EnablePageKeyboardBlocking(); err != nil {
716+ log.Printf("[webkit] Failed to enable keyboard blocking: %v", err)
717+ }
718+ case "disable":
719+ log.Printf("[webkit] Disabling page keyboard blocking")
720+ if err := vw.DisablePageKeyboardBlocking(); err != nil {
721+ log.Printf("[webkit] Failed to disable keyboard blocking: %v", err)
722+ }
723+ default:
724+ log.Printf("[webkit] Unknown keyboard blocking action: %s", msg.Action)
725+ }
726+
727+ return true
728+}
@@ -408,6 +408,11 @@ func goOnUcmMessage(id C.ulong, json *C.char) {
408 return408 return
409 }409 }
410 410
411+ // Handle keyboard blocking messages
412+ if handleKeyboardBlockingMessage(vw, goPayload) {
413+ return
414+ }
415+
411 vw.dispatchScriptMessage(goPayload)416 vw.dispatchScriptMessage(goPayload)
412 }417 }
413 418
@@ -684,3 +689,40 @@ func handleRenderingBackendMessage(payload string) bool {
684 689
685 return true690 return true
686 }691 }
692+
693+// handleKeyboardBlockingMessage handles keyboard blocking control messages from omnibox
694+func handleKeyboardBlockingMessage(vw *WebView, payload string) bool {
695+ if vw == nil {
696+ return false
697+ }
698+
699+ var msg struct {
700+ Type string `json:"type"`
701+ Action string `json:"action"`
702+ }
703+
704+ if err := json.Unmarshal([]byte(payload), &msg); err != nil {
705+ return false
706+ }
707+
708+ if msg.Type != "keyboard_blocking" {
709+ return false
710+ }
711+
712+ switch msg.Action {
713+ case "enable":
714+ log.Printf("[webkit] Enabling page keyboard blocking for omnibox")
715+ if err := vw.EnablePageKeyboardBlocking(); err != nil {
716+ log.Printf("[webkit] Failed to enable keyboard blocking: %v", err)
717+ }
718+ case "disable":
719+ log.Printf("[webkit] Disabling page keyboard blocking")
720+ if err := vw.DisablePageKeyboardBlocking(); err != nil {
721+ log.Printf("[webkit] Failed to disable keyboard blocking: %v", err)
722+ }
723+ default:
724+ log.Printf("[webkit] Unknown keyboard blocking action: %s", msg.Action)
725+ }
726+
727+ return true
728+}
M pkg/webkit/shortcuts_cgo.go +102 -0
@@ -5,6 +5,7 @@ package webkit
55 /*
66 #cgo pkg-config: gtk4 webkitgtk-6.0
77 #include <gtk/gtk.h>
8+#include <webkit/webkit.h>
89 #include <stdint.h>
910 #include <stdlib.h>
1011
@@ -142,3 +143,104 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err
142143
143144 return nil
144145 }
146+
147+// Keyboard event blocking functionality for omnibox isolation
148+
149+// keyboardBlockerScript holds reference to the main-world keyboard blocking script
150+var keyboardBlockerScript *C.WebKitUserScript = nil
151+
152+// EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events
153+// This prevents page JavaScript from receiving keyboard events while omnibox is active
154+func (w *WebView) EnablePageKeyboardBlocking() error {
155+ if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
156+ return ErrWebViewNotInitialized
157+ }
158+
159+ // Remove any existing blocker script first
160+ w.DisablePageKeyboardBlocking()
161+
162+ // Script to block all keyboard and related events in the main world
163+ blockingScript := `
164+ (() => {
165+ // Store original state to restore later
166+ if (!window.__dumber_event_blocker_installed) {
167+ window.__dumber_event_blocker_installed = true;
168+
169+ // List of events to block
170+ const eventTypes = [
171+ 'keydown', 'keyup', 'keypress',
172+ 'input', 'beforeinput',
173+ 'compositionstart', 'compositionupdate', 'compositionend'
174+ ];
175+
176+ // Block all keyboard-related events at capture phase
177+ const blockEvent = (e) => {
178+ e.stopImmediatePropagation();
179+ e.preventDefault();
180+ e.stopPropagation();
181+ return false;
182+ };
183+
184+ // Install capture-phase event blockers
185+ eventTypes.forEach(type => {
186+ document.addEventListener(type, blockEvent, true);
187+ });
188+
189+ // Store cleanup function
190+ window.__dumber_cleanup_event_blocker = () => {
191+ eventTypes.forEach(type => {
192+ document.removeEventListener(type, blockEvent, true);
193+ });
194+ delete window.__dumber_event_blocker_installed;
195+ delete window.__dumber_cleanup_event_blocker;
196+ };
197+
198+ console.log('[dumber] Page keyboard event blocking enabled');
199+ }
200+ })();
201+ `
202+
203+ cScript := C.CString(blockingScript)
204+ defer C.free(unsafe.Pointer(cScript))
205+
206+ // Inject into main world (nil world parameter = main world)
207+ keyboardBlockerScript = C.webkit_user_script_new(
208+ (*C.gchar)(cScript),
209+ C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
210+ C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
211+ nil, nil) // nil = main world
212+
213+ if keyboardBlockerScript == nil {
214+ return ErrNotImplemented
215+ }
216+
217+ C.webkit_user_content_manager_add_script(w.native.ucm, keyboardBlockerScript)
218+ log.Printf("[webkit] Page keyboard event blocking enabled")
219+ return nil
220+}
221+
222+// DisablePageKeyboardBlocking removes the keyboard blocking script and restores normal page behavior
223+func (w *WebView) DisablePageKeyboardBlocking() error {
224+ if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
225+ return ErrWebViewNotInitialized
226+ }
227+
228+ // Execute cleanup script to remove event listeners
229+ cleanupScript := `
230+ if (window.__dumber_cleanup_event_blocker) {
231+ window.__dumber_cleanup_event_blocker();
232+ console.log('[dumber] Page keyboard event blocking disabled');
233+ }
234+ `
235+ w.InjectScript(cleanupScript)
236+
237+ // Remove the user script if it exists
238+ if keyboardBlockerScript != nil {
239+ C.webkit_user_content_manager_remove_script(w.native.ucm, keyboardBlockerScript)
240+ C.webkit_user_script_unref(keyboardBlockerScript)
241+ keyboardBlockerScript = nil
242+ log.Printf("[webkit] Keyboard blocker script removed")
243+ }
244+
245+ return nil
246+}
@@ -5,6 +5,7 @@ package webkit
5 /*5 /*
6 #cgo pkg-config: gtk4 webkitgtk-6.06 #cgo pkg-config: gtk4 webkitgtk-6.0
7 #include <gtk/gtk.h>7 #include <gtk/gtk.h>
8+#include <webkit/webkit.h>
8 #include <stdint.h>9 #include <stdint.h>
9 #include <stdlib.h>10 #include <stdlib.h>
10 11
@@ -142,3 +143,104 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err
142 143
143 return nil144 return nil
144 }145 }
146+
147+// Keyboard event blocking functionality for omnibox isolation
148+
149+// keyboardBlockerScript holds reference to the main-world keyboard blocking script
150+var keyboardBlockerScript *C.WebKitUserScript = nil
151+
152+// EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events
153+// This prevents page JavaScript from receiving keyboard events while omnibox is active
154+func (w *WebView) EnablePageKeyboardBlocking() error {
155+ if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
156+ return ErrWebViewNotInitialized
157+ }
158+
159+ // Remove any existing blocker script first
160+ w.DisablePageKeyboardBlocking()
161+
162+ // Script to block all keyboard and related events in the main world
163+ blockingScript := `
164+ (() => {
165+ // Store original state to restore later
166+ if (!window.__dumber_event_blocker_installed) {
167+ window.__dumber_event_blocker_installed = true;
168+
169+ // List of events to block
170+ const eventTypes = [
171+ 'keydown', 'keyup', 'keypress',
172+ 'input', 'beforeinput',
173+ 'compositionstart', 'compositionupdate', 'compositionend'
174+ ];
175+
176+ // Block all keyboard-related events at capture phase
177+ const blockEvent = (e) => {
178+ e.stopImmediatePropagation();
179+ e.preventDefault();
180+ e.stopPropagation();
181+ return false;
182+ };
183+
184+ // Install capture-phase event blockers
185+ eventTypes.forEach(type => {
186+ document.addEventListener(type, blockEvent, true);
187+ });
188+
189+ // Store cleanup function
190+ window.__dumber_cleanup_event_blocker = () => {
191+ eventTypes.forEach(type => {
192+ document.removeEventListener(type, blockEvent, true);
193+ });
194+ delete window.__dumber_event_blocker_installed;
195+ delete window.__dumber_cleanup_event_blocker;
196+ };
197+
198+ console.log('[dumber] Page keyboard event blocking enabled');
199+ }
200+ })();
201+ `
202+
203+ cScript := C.CString(blockingScript)
204+ defer C.free(unsafe.Pointer(cScript))
205+
206+ // Inject into main world (nil world parameter = main world)
207+ keyboardBlockerScript = C.webkit_user_script_new(
208+ (*C.gchar)(cScript),
209+ C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
210+ C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
211+ nil, nil) // nil = main world
212+
213+ if keyboardBlockerScript == nil {
214+ return ErrNotImplemented
215+ }
216+
217+ C.webkit_user_content_manager_add_script(w.native.ucm, keyboardBlockerScript)
218+ log.Printf("[webkit] Page keyboard event blocking enabled")
219+ return nil
220+}
221+
222+// DisablePageKeyboardBlocking removes the keyboard blocking script and restores normal page behavior
223+func (w *WebView) DisablePageKeyboardBlocking() error {
224+ if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
225+ return ErrWebViewNotInitialized
226+ }
227+
228+ // Execute cleanup script to remove event listeners
229+ cleanupScript := `
230+ if (window.__dumber_cleanup_event_blocker) {
231+ window.__dumber_cleanup_event_blocker();
232+ console.log('[dumber] Page keyboard event blocking disabled');
233+ }
234+ `
235+ w.InjectScript(cleanupScript)
236+
237+ // Remove the user script if it exists
238+ if keyboardBlockerScript != nil {
239+ C.webkit_user_content_manager_remove_script(w.native.ucm, keyboardBlockerScript)
240+ C.webkit_user_script_unref(keyboardBlockerScript)
241+ keyboardBlockerScript = nil
242+ log.Printf("[webkit] Keyboard blocker script removed")
243+ }
244+
245+ return nil
246+}
M pkg/webkit/webview_cgo.go +1 -0
@@ -2580,6 +2580,7 @@ func (w *WebView) ID() string {
25802580 return strconv.FormatUint(uint64(w.id), 10)
25812581 }
25822582
2583+
25832584 // PrefersDarkTheme returns true if GTK is configured to prefer dark theme
25842585 func PrefersDarkTheme() bool {
25852586 return C.gtk_prefers_dark() != 0
@@ -2580,6 +2580,7 @@ func (w *WebView) ID() string {
2580 return strconv.FormatUint(uint64(w.id), 10)2580 return strconv.FormatUint(uint64(w.id), 10)
2581 }2581 }
2582 2582
2583+
2583 // PrefersDarkTheme returns true if GTK is configured to prefer dark theme2584 // PrefersDarkTheme returns true if GTK is configured to prefer dark theme
2584 func PrefersDarkTheme() bool {2585 func PrefersDarkTheme() bool {
2585 return C.gtk_prefers_dark() != 02586 return C.gtk_prefers_dark() != 0

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