detached 357118c

feat(app): wire unified key routing with native dead key support

Replace SetShouldBypassInput with SetRouteKey in app initialization.
The routing callback encodes the full policy:
- Overlays -> RoutePassToWidget (full keyboard control)
- Omnibox -> RouteAccentDetection (long-press accents for GTK Entry)
- WebView text/dead keys -> RoutePassToWidget (native IM compose)
- Shortcut-modified keys -> RouteHandleShortcuts (Ctrl+L, Alt+1, etc.)

Update accent handler comments to reflect GTK Entry-only scope.
commit
357118c3cb6af56f1f8656c23289f7d2a2ae1adb
author
bnema · · 2026-03-10 22:04 UTC
committer
brice · · 2026-03-11 04:05 UTC
parent
0185904
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +34 -9
M internal/application/usecase/insert_accent.go +2 -1
@@ -53,8 +53,9 @@ func NewInsertAccentUseCase(
5353
5454 // OnKeyPressed handles a key press event.
5555 // Returns true if the use case is handling this key and the caller should suppress it.
56-// Note: Due to WebKit's input handling, key suppression may not be fully effective.
56+// Note: For GTK Entry widgets, key suppression may not be fully effective.
5757 // When the picker shows, we delete any repeated characters that slipped through.
58+// WebView text inputs use native IM compose instead of this accent picker.
5859 func (uc *InsertAccentUseCase) OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool {
5960 log := logging.FromContext(ctx)
6061
@@ -53,8 +53,9 @@ func NewInsertAccentUseCase(
53 53
54 // OnKeyPressed handles a key press event.54 // OnKeyPressed handles a key press event.
55 // Returns true if the use case is handling this key and the caller should suppress it.55 // Returns true if the use case is handling this key and the caller should suppress it.
56-// Note: Due to WebKit's input handling, key suppression may not be fully effective.56+// Note: For GTK Entry widgets, key suppression may not be fully effective.
57 // When the picker shows, we delete any repeated characters that slipped through.57 // When the picker shows, we delete any repeated characters that slipped through.
58+// WebView text inputs use native IM compose instead of this accent picker.
58 func (uc *InsertAccentUseCase) OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool {59 func (uc *InsertAccentUseCase) OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool {
59 log := logging.FromContext(ctx)60 log := logging.FromContext(ctx)
60 61
M internal/ui/app.go +32 -8
@@ -631,23 +631,47 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
631631 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
632632 a.handleModeChange(ctx, from, to)
633633 })
634- a.keyboardHandler.SetShouldBypassInput(func(modifiers input.Modifier) bool {
635- // Bypass keyboard handler when modals are visible
634+ // Wire key routing policy.
635+ // Determines how each key event should be handled based on UI state:
636+ // - Overlays visible (session manager, tab picker): pass all keys to widget
637+ // - Omnibox visible: accent detection for text keys, shortcuts for modified keys
638+ // - WebView focused: pass text/dead keys to WebKit IM for native compose,
639+ // intercept shortcut-modified keys (Ctrl+, Alt+)
640+ // - Default: handle through shortcut system
641+ a.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute {
642+ // Overlays take full keyboard control
636643 if a.sessionManager != nil && a.sessionManager.IsVisible() {
637- return true
644+ return input.RoutePassToWidget
638645 }
639646 if a.tabPicker != nil && a.tabPicker.IsVisible() {
640- return true
647+ return input.RoutePassToWidget
641648 }
649+
642650 wsView := a.activeWorkspaceView()
643651 if wsView == nil {
644- return false
652+ return input.RouteHandleShortcuts
645653 }
654+
646655 if wsView.IsOmniboxVisible() {
647- // Let Alt-modified keys through for pane navigation
648- return modifiers&input.ModAlt == 0
656+ // Omnibox: Alt-modified keys go to shortcuts (pane navigation),
657+ // text keys go to accent detection for long-press accent picker
658+ if kc.Modifiers&input.ModAlt != 0 {
659+ return input.RouteHandleShortcuts
660+ }
661+ return input.RouteAccentDetection
649662 }
650- return false
663+
664+ // WebView focused: text/dead keys pass through for native IM compose,
665+ // shortcut-modified keys (Ctrl+, Alt+) are handled by shortcut system
666+ if input.IsShortcutModified(kc.Modifiers) {
667+ return input.RouteHandleShortcuts
668+ }
669+ if input.IsTextInputKey(kc.Keyval) {
670+ return input.RoutePassToWidget
671+ }
672+
673+ // Non-text, non-shortcut keys (F-keys, arrows, etc.): shortcut system
674+ return input.RouteHandleShortcuts
651675 })
652676 // Wire accent handler for dead keys support
653677 if a.insertAccentUC != nil {
@@ -631,23 +631,47 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
631 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {631 a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
632 a.handleModeChange(ctx, from, to)632 a.handleModeChange(ctx, from, to)
633 })633 })
634- a.keyboardHandler.SetShouldBypassInput(func(modifiers input.Modifier) bool {634+ // Wire key routing policy.
635- // Bypass keyboard handler when modals are visible635+ // Determines how each key event should be handled based on UI state:
636+ // - Overlays visible (session manager, tab picker): pass all keys to widget
637+ // - Omnibox visible: accent detection for text keys, shortcuts for modified keys
638+ // - WebView focused: pass text/dead keys to WebKit IM for native compose,
639+ // intercept shortcut-modified keys (Ctrl+, Alt+)
640+ // - Default: handle through shortcut system
641+ a.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute {
642+ // Overlays take full keyboard control
636 if a.sessionManager != nil && a.sessionManager.IsVisible() {643 if a.sessionManager != nil && a.sessionManager.IsVisible() {
637- return true644+ return input.RoutePassToWidget
638 }645 }
639 if a.tabPicker != nil && a.tabPicker.IsVisible() {646 if a.tabPicker != nil && a.tabPicker.IsVisible() {
640- return true647+ return input.RoutePassToWidget
641 }648 }
649+
642 wsView := a.activeWorkspaceView()650 wsView := a.activeWorkspaceView()
643 if wsView == nil {651 if wsView == nil {
644- return false652+ return input.RouteHandleShortcuts
645 }653 }
654+
646 if wsView.IsOmniboxVisible() {655 if wsView.IsOmniboxVisible() {
647- // Let Alt-modified keys through for pane navigation656+ // Omnibox: Alt-modified keys go to shortcuts (pane navigation),
648- return modifiers&input.ModAlt == 0657+ // text keys go to accent detection for long-press accent picker
658+ if kc.Modifiers&input.ModAlt != 0 {
659+ return input.RouteHandleShortcuts
660+ }
661+ return input.RouteAccentDetection
649 }662 }
650- return false663+
664+ // WebView focused: text/dead keys pass through for native IM compose,
665+ // shortcut-modified keys (Ctrl+, Alt+) are handled by shortcut system
666+ if input.IsShortcutModified(kc.Modifiers) {
667+ return input.RouteHandleShortcuts
668+ }
669+ if input.IsTextInputKey(kc.Keyval) {
670+ return input.RoutePassToWidget
671+ }
672+
673+ // Non-text, non-shortcut keys (F-keys, arrows, etc.): shortcut system
674+ return input.RouteHandleShortcuts
651 })675 })
652 // Wire accent handler for dead keys support676 // Wire accent handler for dead keys support
653 if a.insertAccentUC != nil {677 if a.insertAccentUC != nil {

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