detached e2080dd

refactor(omnibox): move keyboard blocking to main-world

commit
e2080dd036b29e1cb821971a6d2ce3782932336a
author
bnema · · 2025-11-18 22:22 UTC
parent
72a6840
signature
unsigned
3 files · +128 -0
M CHANGELOG.md +4 -0
@@ -6,10 +6,14 @@ All notable changes to this project will be documented in this file.
66
77 ### Added
88 - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits.
9+- **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active.
910
1011 ### Fixed
1112 - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation.
1213
14+### Changed
15+- **Refactor keyboard blocking**: Move logic to main-world.ts, reduce handler.go verbosity.
16+
1317 ## [0.14.1] - 2025-11-18
1418
1519 ### Added
@@ -6,10 +6,14 @@ All notable changes to this project will be documented in this file.
6 6
7 ### Added7 ### Added
8 - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits.8 - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits.
9+- **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active.
9 10
10 ### Fixed11 ### Fixed
11 - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation.12 - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation.
12 13
14+### Changed
15+- **Refactor keyboard blocking**: Move logic to main-world.ts, reduce handler.go verbosity.
16+
13 ## [0.14.1] - 2025-11-1817 ## [0.14.1] - 2025-11-18
14 18
15 ### Added19 ### Added
M gui/src/injected/main-world.ts +104 -0
@@ -36,6 +36,12 @@ declare global {
3636 __dumber_webview_id?: string | number;
3737 __dumber_is_active?: boolean;
3838 __dumber_teardown?: () => void;
39+ __dumber_enableKeyboardBlocking?: () => void;
40+ __dumber_disableKeyboardBlocking?: () => void;
41+ __dumber_keyboard_blocking_active?: boolean;
42+ __dumber_keyboard_blocker?: (e: Event) => void;
43+ __dumber_focus_guard?: (e: Event) => void;
44+ __dumber_click_blocker?: (e: Event) => void;
3945 webkit?: {
4046 messageHandlers?: {
4147 dumber?: {
@@ -994,6 +1000,104 @@ interface DumberAPI {
9941000
9951001 // Initialize console capture
9961002 setupConsoleCapture();
1003+
1004+ // ============================================================================
1005+ // KEYBOARD AND FOCUS BLOCKING FOR OMNIBOX
1006+ // ============================================================================
1007+
1008+ const SHADOW_HOST_ID = 'dumber-ui-root';
1009+
1010+ const isFromOmnibox = (target: EventTarget | null): boolean => {
1011+ if (!target || !(target instanceof Element)) {
1012+ return false;
1013+ }
1014+ return target.id === SHADOW_HOST_ID || target.closest(`#${SHADOW_HOST_ID}`) !== null;
1015+ };
1016+
1017+ window.__dumber_enableKeyboardBlocking = function(): void {
1018+ if (window.__dumber_keyboard_blocking_active) {
1019+ return;
1020+ }
1021+
1022+ window.__dumber_keyboard_blocking_active = true;
1023+
1024+ window.__dumber_keyboard_blocker = function(e: Event): void {
1025+ if (isFromOmnibox(e.target)) {
1026+ return;
1027+ }
1028+ e.stopPropagation();
1029+ e.stopImmediatePropagation();
1030+ };
1031+
1032+ window.__dumber_focus_guard = function(e: Event): void {
1033+ if (e.type === 'focus') {
1034+ if (!isFromOmnibox(e.target)) {
1035+ e.stopPropagation();
1036+ e.stopImmediatePropagation();
1037+ e.preventDefault();
1038+ }
1039+ } else if (e.type === 'blur') {
1040+ if (isFromOmnibox(e.target)) {
1041+ e.stopPropagation();
1042+ e.stopImmediatePropagation();
1043+ e.preventDefault();
1044+ setTimeout(() => {
1045+ const shadowHost = document.getElementById(SHADOW_HOST_ID);
1046+ if (shadowHost && shadowHost.shadowRoot && window.__dumber_keyboard_blocking_active) {
1047+ const input = shadowHost.shadowRoot.querySelector('.omnibox-input-field') as HTMLInputElement;
1048+ if (input) {
1049+ input.focus();
1050+ }
1051+ }
1052+ }, 0);
1053+ }
1054+ }
1055+ };
1056+
1057+ window.__dumber_click_blocker = function(e: Event): void {
1058+ if (isFromOmnibox(e.target)) {
1059+ return;
1060+ }
1061+ e.stopPropagation();
1062+ e.stopImmediatePropagation();
1063+ e.preventDefault();
1064+ };
1065+
1066+ document.addEventListener('keydown', window.__dumber_keyboard_blocker, true);
1067+ document.addEventListener('keyup', window.__dumber_keyboard_blocker, true);
1068+ document.addEventListener('keypress', window.__dumber_keyboard_blocker, true);
1069+ document.addEventListener('focus', window.__dumber_focus_guard, true);
1070+ document.addEventListener('blur', window.__dumber_focus_guard, true);
1071+ document.addEventListener('click', window.__dumber_click_blocker, true);
1072+ document.addEventListener('mousedown', window.__dumber_click_blocker, true);
1073+ };
1074+
1075+ window.__dumber_disableKeyboardBlocking = function(): void {
1076+ if (!window.__dumber_keyboard_blocking_active) {
1077+ return;
1078+ }
1079+
1080+ window.__dumber_keyboard_blocking_active = false;
1081+
1082+ if (window.__dumber_keyboard_blocker) {
1083+ document.removeEventListener('keydown', window.__dumber_keyboard_blocker, true);
1084+ document.removeEventListener('keyup', window.__dumber_keyboard_blocker, true);
1085+ document.removeEventListener('keypress', window.__dumber_keyboard_blocker, true);
1086+ delete window.__dumber_keyboard_blocker;
1087+ }
1088+
1089+ if (window.__dumber_focus_guard) {
1090+ document.removeEventListener('focus', window.__dumber_focus_guard, true);
1091+ document.removeEventListener('blur', window.__dumber_focus_guard, true);
1092+ delete window.__dumber_focus_guard;
1093+ }
1094+
1095+ if (window.__dumber_click_blocker) {
1096+ document.removeEventListener('click', window.__dumber_click_blocker, true);
1097+ document.removeEventListener('mousedown', window.__dumber_click_blocker, true);
1098+ delete window.__dumber_click_blocker;
1099+ }
1100+ };
9971101 } catch (err) {
9981102 console.warn("[dumber] unified bridge init failed", err);
9991103 }
@@ -36,6 +36,12 @@ declare global {
36 __dumber_webview_id?: string | number;36 __dumber_webview_id?: string | number;
37 __dumber_is_active?: boolean;37 __dumber_is_active?: boolean;
38 __dumber_teardown?: () => void;38 __dumber_teardown?: () => void;
39+ __dumber_enableKeyboardBlocking?: () => void;
40+ __dumber_disableKeyboardBlocking?: () => void;
41+ __dumber_keyboard_blocking_active?: boolean;
42+ __dumber_keyboard_blocker?: (e: Event) => void;
43+ __dumber_focus_guard?: (e: Event) => void;
44+ __dumber_click_blocker?: (e: Event) => void;
39 webkit?: {45 webkit?: {
40 messageHandlers?: {46 messageHandlers?: {
41 dumber?: {47 dumber?: {
@@ -994,6 +1000,104 @@ interface DumberAPI {
994 1000
995 // Initialize console capture1001 // Initialize console capture
996 setupConsoleCapture();1002 setupConsoleCapture();
1003+
1004+ // ============================================================================
1005+ // KEYBOARD AND FOCUS BLOCKING FOR OMNIBOX
1006+ // ============================================================================
1007+
1008+ const SHADOW_HOST_ID = 'dumber-ui-root';
1009+
1010+ const isFromOmnibox = (target: EventTarget | null): boolean => {
1011+ if (!target || !(target instanceof Element)) {
1012+ return false;
1013+ }
1014+ return target.id === SHADOW_HOST_ID || target.closest(`#${SHADOW_HOST_ID}`) !== null;
1015+ };
1016+
1017+ window.__dumber_enableKeyboardBlocking = function(): void {
1018+ if (window.__dumber_keyboard_blocking_active) {
1019+ return;
1020+ }
1021+
1022+ window.__dumber_keyboard_blocking_active = true;
1023+
1024+ window.__dumber_keyboard_blocker = function(e: Event): void {
1025+ if (isFromOmnibox(e.target)) {
1026+ return;
1027+ }
1028+ e.stopPropagation();
1029+ e.stopImmediatePropagation();
1030+ };
1031+
1032+ window.__dumber_focus_guard = function(e: Event): void {
1033+ if (e.type === 'focus') {
1034+ if (!isFromOmnibox(e.target)) {
1035+ e.stopPropagation();
1036+ e.stopImmediatePropagation();
1037+ e.preventDefault();
1038+ }
1039+ } else if (e.type === 'blur') {
1040+ if (isFromOmnibox(e.target)) {
1041+ e.stopPropagation();
1042+ e.stopImmediatePropagation();
1043+ e.preventDefault();
1044+ setTimeout(() => {
1045+ const shadowHost = document.getElementById(SHADOW_HOST_ID);
1046+ if (shadowHost && shadowHost.shadowRoot && window.__dumber_keyboard_blocking_active) {
1047+ const input = shadowHost.shadowRoot.querySelector('.omnibox-input-field') as HTMLInputElement;
1048+ if (input) {
1049+ input.focus();
1050+ }
1051+ }
1052+ }, 0);
1053+ }
1054+ }
1055+ };
1056+
1057+ window.__dumber_click_blocker = function(e: Event): void {
1058+ if (isFromOmnibox(e.target)) {
1059+ return;
1060+ }
1061+ e.stopPropagation();
1062+ e.stopImmediatePropagation();
1063+ e.preventDefault();
1064+ };
1065+
1066+ document.addEventListener('keydown', window.__dumber_keyboard_blocker, true);
1067+ document.addEventListener('keyup', window.__dumber_keyboard_blocker, true);
1068+ document.addEventListener('keypress', window.__dumber_keyboard_blocker, true);
1069+ document.addEventListener('focus', window.__dumber_focus_guard, true);
1070+ document.addEventListener('blur', window.__dumber_focus_guard, true);
1071+ document.addEventListener('click', window.__dumber_click_blocker, true);
1072+ document.addEventListener('mousedown', window.__dumber_click_blocker, true);
1073+ };
1074+
1075+ window.__dumber_disableKeyboardBlocking = function(): void {
1076+ if (!window.__dumber_keyboard_blocking_active) {
1077+ return;
1078+ }
1079+
1080+ window.__dumber_keyboard_blocking_active = false;
1081+
1082+ if (window.__dumber_keyboard_blocker) {
1083+ document.removeEventListener('keydown', window.__dumber_keyboard_blocker, true);
1084+ document.removeEventListener('keyup', window.__dumber_keyboard_blocker, true);
1085+ document.removeEventListener('keypress', window.__dumber_keyboard_blocker, true);
1086+ delete window.__dumber_keyboard_blocker;
1087+ }
1088+
1089+ if (window.__dumber_focus_guard) {
1090+ document.removeEventListener('focus', window.__dumber_focus_guard, true);
1091+ document.removeEventListener('blur', window.__dumber_focus_guard, true);
1092+ delete window.__dumber_focus_guard;
1093+ }
1094+
1095+ if (window.__dumber_click_blocker) {
1096+ document.removeEventListener('click', window.__dumber_click_blocker, true);
1097+ document.removeEventListener('mousedown', window.__dumber_click_blocker, true);
1098+ delete window.__dumber_click_blocker;
1099+ }
1100+ };
997 } catch (err) {1101 } catch (err) {
998 console.warn("[dumber] unified bridge init failed", err);1102 console.warn("[dumber] unified bridge init failed", err);
999 }1103 }
M internal/app/messaging/handler.go +20 -0
@@ -126,6 +126,8 @@ func (h *Handler) Handle(payload string) {
126126 h.handleToggleFavorite(msg)
127127 case "is_favorite":
128128 h.handleIsFavorite(msg)
129+ case "keyboard_blocking":
130+ h.handleKeyboardBlocking(msg)
129131 }
130132 }
131133
@@ -852,3 +854,21 @@ func (h *Handler) handleIsFavorite(msg Message) {
852854
853855 _ = h.webView.InjectScript("window.__dumber_is_favorite && window.__dumber_is_favorite(" + string(b) + ")")
854856 }
857+
858+// handleKeyboardBlocking enables or disables keyboard event blocking for omnibox
859+func (h *Handler) handleKeyboardBlocking(msg Message) {
860+ if h.webView == nil {
861+ return
862+ }
863+
864+ var script string
865+ if msg.Action == "enable" {
866+ script = "window.__dumber_enableKeyboardBlocking?.()"
867+ } else {
868+ script = "window.__dumber_disableKeyboardBlocking?.()"
869+ }
870+
871+ if err := h.webView.InjectScript(script); err != nil {
872+ log.Printf("[ERROR] Failed to inject keyboard blocking script: %v", err)
873+ }
874+}
@@ -126,6 +126,8 @@ func (h *Handler) Handle(payload string) {
126 h.handleToggleFavorite(msg)126 h.handleToggleFavorite(msg)
127 case "is_favorite":127 case "is_favorite":
128 h.handleIsFavorite(msg)128 h.handleIsFavorite(msg)
129+ case "keyboard_blocking":
130+ h.handleKeyboardBlocking(msg)
129 }131 }
130 }132 }
131 133
@@ -852,3 +854,21 @@ func (h *Handler) handleIsFavorite(msg Message) {
852 854
853 _ = h.webView.InjectScript("window.__dumber_is_favorite && window.__dumber_is_favorite(" + string(b) + ")")855 _ = h.webView.InjectScript("window.__dumber_is_favorite && window.__dumber_is_favorite(" + string(b) + ")")
854 }856 }
857+
858+// handleKeyboardBlocking enables or disables keyboard event blocking for omnibox
859+func (h *Handler) handleKeyboardBlocking(msg Message) {
860+ if h.webView == nil {
861+ return
862+ }
863+
864+ var script string
865+ if msg.Action == "enable" {
866+ script = "window.__dumber_enableKeyboardBlocking?.()"
867+ } else {
868+ script = "window.__dumber_disableKeyboardBlocking?.()"
869+ }
870+
871+ if err := h.webView.InjectScript(script); err != nil {
872+ log.Printf("[ERROR] Failed to inject keyboard blocking script: %v", err)
873+ }
874+}

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