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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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( | |||
| 53 | 53 | ||
| 54 | 54 | // OnKeyPressed handles a key press event. | |
| 55 | 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 | 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 | 59 | func (uc *InsertAccentUseCase) OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool { | |
| 59 | 60 | log := logging.FromContext(ctx) | |
| 60 | 61 | ||
| @@ -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) { | |||
| 631 | 631 | a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) { | |
| 632 | 632 | a.handleModeChange(ctx, from, to) | |
| 633 | 633 | }) | |
| 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 | |
| 636 | 643 | if a.sessionManager != nil && a.sessionManager.IsVisible() { | |
| 637 | - | return true | |
| 644 | + | return input.RoutePassToWidget | |
| 638 | 645 | } | |
| 639 | 646 | if a.tabPicker != nil && a.tabPicker.IsVisible() { | |
| 640 | - | return true | |
| 647 | + | return input.RoutePassToWidget | |
| 641 | 648 | } | |
| 649 | + | ||
| 642 | 650 | wsView := a.activeWorkspaceView() | |
| 643 | 651 | if wsView == nil { | |
| 644 | - | return false | |
| 652 | + | return input.RouteHandleShortcuts | |
| 645 | 653 | } | |
| 654 | + | ||
| 646 | 655 | 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 | |
| 649 | 662 | } | |
| 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 | |
| 651 | 675 | }) | |
| 652 | 676 | // Wire accent handler for dead keys support | |
| 653 | 677 | 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 visible | 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 | |||
| 636 | if a.sessionManager != nil && a.sessionManager.IsVisible() { | 643 | if a.sessionManager != nil && a.sessionManager.IsVisible() { | ||
| 637 | - | return true | 644 | + | return input.RoutePassToWidget |
| 638 | } | 645 | } | ||
| 639 | if a.tabPicker != nil && a.tabPicker.IsVisible() { | 646 | if a.tabPicker != nil && a.tabPicker.IsVisible() { | ||
| 640 | - | return true | 647 | + | return input.RoutePassToWidget |
| 641 | } | 648 | } | ||
| 649 | + | ||||
| 642 | wsView := a.activeWorkspaceView() | 650 | wsView := a.activeWorkspaceView() | ||
| 643 | if wsView == nil { | 651 | if wsView == nil { | ||
| 644 | - | return false | 652 | + | return input.RouteHandleShortcuts |
| 645 | } | 653 | } | ||
| 654 | + | ||||
| 646 | if wsView.IsOmniboxVisible() { | 655 | if wsView.IsOmniboxVisible() { | ||
| 647 | - | // Let Alt-modified keys through for pane navigation | 656 | + | // Omnibox: Alt-modified keys go to shortcuts (pane navigation), |
| 648 | - | return modifiers&input.ModAlt == 0 | 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 | |||
| 649 | } | 662 | } | ||
| 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 | |||
| 651 | }) | 675 | }) | ||
| 652 | // Wire accent handler for dead keys support | 676 | // Wire accent handler for dead keys support | ||
| 653 | if a.insertAccentUC != nil { | 677 | if a.insertAccentUC != nil { | ||