detached b05099c

fix(input): use bubble phase + IM fallback for dead keys, move accent detection to widget level

Dead-key compose (us intl layout) was broken because the window-level
EventControllerKey in capture phase intercepted events before WebKit's
GtkIMContext could handle them. Wayland compositors without text-input-v3
(e.g. niri) also need GTK_IM_MODULE=gtk-im-context-simple as fallback.

Changes:
- Switch window key controller from capture to bubble phase
- Set GTK_IM_MODULE=gtk-im-context-simple at startup when unset
- Move long-press accent detection to widget level:
  - Omnibox/find bar: local key controller callbacks
  - WebView: JS keydown/keyup script + message bridge handlers
- Remove now-redundant dead-key bypass state (pendingDeadKey, isDeadKey)
- Export KeyvalToRune for use by widget-level accent callbacks
commit
b05099ccc710f633a678cb82e64869cc6c900a09
author
brice · · 2026-03-11 05:46 UTC
parent
476d2f2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +268 -49
M cmd/dumber/main.go +9 -0
@@ -73,6 +73,15 @@ func main() {
7373 }
7474
7575 func runGUI() int {
76+ // Wayland compositors that lack text-input-v3 (e.g. niri) don't support
77+ // IBus/Fcitx input methods, causing dead-key compose to break. Fall back to
78+ // GTK's built-in simple IM context so dead keys work out of the box, but
79+ // only when the user hasn't explicitly chosen an IM module.
80+ if os.Getenv("GTK_IM_MODULE") == "" {
81+ fmt.Fprintf(os.Stderr, "dumber: GTK_IM_MODULE unset, defaulting to gtk-im-context-simple for dead-key support\n")
82+ _ = os.Setenv("GTK_IM_MODULE", "gtk-im-context-simple")
83+ }
84+
7685 runtime.LockOSThread()
7786 component.SetSkeletonVersion(version)
7887 timer := bootstrap.NewStartupTimer()
@@ -73,6 +73,15 @@ func main() {
73 }73 }
74 74
75 func runGUI() int {75 func runGUI() int {
76+ // Wayland compositors that lack text-input-v3 (e.g. niri) don't support
77+ // IBus/Fcitx input methods, causing dead-key compose to break. Fall back to
78+ // GTK's built-in simple IM context so dead keys work out of the box, but
79+ // only when the user hasn't explicitly chosen an IM module.
80+ if os.Getenv("GTK_IM_MODULE") == "" {
81+ fmt.Fprintf(os.Stderr, "dumber: GTK_IM_MODULE unset, defaulting to gtk-im-context-simple for dead-key support\n")
82+ _ = os.Setenv("GTK_IM_MODULE", "gtk-im-context-simple")
83+ }
84+
76 runtime.LockOSThread()85 runtime.LockOSThread()
77 component.SetSkeletonVersion(version)86 component.SetSkeletonVersion(version)
78 timer := bootstrap.NewStartupTimer()87 timer := bootstrap.NewStartupTimer()
M internal/infrastructure/webkit/handlers/register.go +53 -0
@@ -2,6 +2,7 @@ package handlers
22
33 import (
44 "context"
5+ "encoding/json"
56 "fmt"
67
78 "github.com/bnema/dumber/internal/application/port"
@@ -9,8 +10,16 @@ import (
910 "github.com/bnema/dumber/internal/infrastructure/config"
1011 "github.com/bnema/dumber/internal/infrastructure/webkit"
1112 "github.com/bnema/dumber/internal/infrastructure/webkit/handlers/homepage"
13+ "github.com/bnema/dumber/internal/logging"
1214 )
1315
16+// AccentKeyHandler is implemented by the InsertAccentUseCase to receive
17+// key press/release events forwarded from WebView JS via the message bridge.
18+type AccentKeyHandler interface {
19+ OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool
20+ OnKeyReleased(ctx context.Context, char rune)
21+}
22+
1423 // Config holds all dependencies for message handlers.
1524 type Config struct {
1625 HistoryUC *usecase.SearchHistoryUseCase
@@ -18,6 +27,7 @@ type Config struct {
1827 Clipboard port.Clipboard
1928 ConfigGetter func() *config.Config
2029 OnClipboardCopied func(textLen int) // Called when auto-copy completes (for toast notification)
30+ AccentHandler AccentKeyHandler // Handles accent key press/release events from WebView inputs
2131 }
2232
2333 // RegisterAll registers all message handlers with the router.
@@ -61,6 +71,49 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
6171 return nil
6272 }
6373
74+// RegisterAccentHandlers registers accent key press/release handlers with the router.
75+// Must be called after the AccentKeyHandler is initialized (i.e., after initAccentPicker).
76+func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, handler AccentKeyHandler) error {
77+ if err := router.RegisterHandler("accent_key_press", webkit.MessageHandlerFunc(
78+ func(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
79+ var p struct {
80+ Char string `json:"char"`
81+ Shift bool `json:"shift"`
82+ }
83+ if err := json.Unmarshal(payload, &p); err != nil {
84+ return nil, err
85+ }
86+ if len(p.Char) == 1 {
87+ handler.OnKeyPressed(ctx, rune(p.Char[0]), p.Shift)
88+ }
89+ return nil, nil
90+ },
91+ )); err != nil {
92+ return err
93+ }
94+
95+ if err := router.RegisterHandler("accent_key_release", webkit.MessageHandlerFunc(
96+ func(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
97+ var p struct {
98+ Char string `json:"char"`
99+ }
100+ if err := json.Unmarshal(payload, &p); err != nil {
101+ return nil, err
102+ }
103+ if len(p.Char) == 1 {
104+ handler.OnKeyReleased(ctx, rune(p.Char[0]))
105+ }
106+ return nil, nil
107+ },
108+ )); err != nil {
109+ return err
110+ }
111+
112+ log := logging.FromContext(ctx)
113+ log.Info().Msg("registered accent key handlers")
114+ return nil
115+}
116+
64117 // createKeybindingsHandler wires the gateway and use cases for keybindings.
65118 func createKeybindingsHandler() (*KeybindingsHandler, error) {
66119 mgr := config.GetManager()
@@ -2,6 +2,7 @@ package handlers
2 2
3 import (3 import (
4 "context"4 "context"
5+ "encoding/json"
5 "fmt"6 "fmt"
6 7
7 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
@@ -9,8 +10,16 @@ import (
9 "github.com/bnema/dumber/internal/infrastructure/config"10 "github.com/bnema/dumber/internal/infrastructure/config"
10 "github.com/bnema/dumber/internal/infrastructure/webkit"11 "github.com/bnema/dumber/internal/infrastructure/webkit"
11 "github.com/bnema/dumber/internal/infrastructure/webkit/handlers/homepage"12 "github.com/bnema/dumber/internal/infrastructure/webkit/handlers/homepage"
13+ "github.com/bnema/dumber/internal/logging"
12 )14 )
13 15
16+// AccentKeyHandler is implemented by the InsertAccentUseCase to receive
17+// key press/release events forwarded from WebView JS via the message bridge.
18+type AccentKeyHandler interface {
19+ OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool
20+ OnKeyReleased(ctx context.Context, char rune)
21+}
22+
14 // Config holds all dependencies for message handlers.23 // Config holds all dependencies for message handlers.
15 type Config struct {24 type Config struct {
16 HistoryUC *usecase.SearchHistoryUseCase25 HistoryUC *usecase.SearchHistoryUseCase
@@ -18,6 +27,7 @@ type Config struct {
18 Clipboard port.Clipboard27 Clipboard port.Clipboard
19 ConfigGetter func() *config.Config28 ConfigGetter func() *config.Config
20 OnClipboardCopied func(textLen int) // Called when auto-copy completes (for toast notification)29 OnClipboardCopied func(textLen int) // Called when auto-copy completes (for toast notification)
30+ AccentHandler AccentKeyHandler // Handles accent key press/release events from WebView inputs
21 }31 }
22 32
23 // RegisterAll registers all message handlers with the router.33 // RegisterAll registers all message handlers with the router.
@@ -61,6 +71,49 @@ func RegisterAll(ctx context.Context, router *webkit.MessageRouter, cfg Config)
61 return nil71 return nil
62 }72 }
63 73
74+// RegisterAccentHandlers registers accent key press/release handlers with the router.
75+// Must be called after the AccentKeyHandler is initialized (i.e., after initAccentPicker).
76+func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, handler AccentKeyHandler) error {
77+ if err := router.RegisterHandler("accent_key_press", webkit.MessageHandlerFunc(
78+ func(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
79+ var p struct {
80+ Char string `json:"char"`
81+ Shift bool `json:"shift"`
82+ }
83+ if err := json.Unmarshal(payload, &p); err != nil {
84+ return nil, err
85+ }
86+ if len(p.Char) == 1 {
87+ handler.OnKeyPressed(ctx, rune(p.Char[0]), p.Shift)
88+ }
89+ return nil, nil
90+ },
91+ )); err != nil {
92+ return err
93+ }
94+
95+ if err := router.RegisterHandler("accent_key_release", webkit.MessageHandlerFunc(
96+ func(ctx context.Context, _ webkit.WebViewID, payload json.RawMessage) (any, error) {
97+ var p struct {
98+ Char string `json:"char"`
99+ }
100+ if err := json.Unmarshal(payload, &p); err != nil {
101+ return nil, err
102+ }
103+ if len(p.Char) == 1 {
104+ handler.OnKeyReleased(ctx, rune(p.Char[0]))
105+ }
106+ return nil, nil
107+ },
108+ )); err != nil {
109+ return err
110+ }
111+
112+ log := logging.FromContext(ctx)
113+ log.Info().Msg("registered accent key handlers")
114+ return nil
115+}
116+
64 // createKeybindingsHandler wires the gateway and use cases for keybindings.117 // createKeybindingsHandler wires the gateway and use cases for keybindings.
65 func createKeybindingsHandler() (*KeybindingsHandler, error) {118 func createKeybindingsHandler() (*KeybindingsHandler, error) {
66 mgr := config.GetManager()119 mgr := config.GetManager()
M internal/infrastructure/webkit/injector.go +52 -0
@@ -141,6 +141,45 @@ const autoCopySelectionScript = `(function() {
141141 });
142142 })();`
143143
144+// accentDetectionScript detects keydown/keyup of a-z keys in WebView inputs and
145+// forwards them to the Go-side InsertAccentUseCase via the message bridge.
146+// Go handles timing, accent lookup, and picker display — JS only reports events.
147+const accentDetectionScript = `(function() {
148+ 'use strict';
149+
150+ // Track pressed key for release matching
151+ let pressedKey = null;
152+
153+ document.addEventListener('keydown', function(e) {
154+ // Only handle a-z keys, with optional Shift
155+ // Skip if Ctrl or Alt are held (those are shortcuts)
156+ if (e.ctrlKey || e.altKey || e.metaKey) return;
157+
158+ // Only single character keys a-z
159+ const key = e.key.toLowerCase();
160+ if (key.length !== 1 || key < 'a' || key > 'z') return;
161+
162+ pressedKey = key;
163+
164+ // Send to Go for accent detection
165+ window.webkit.messageHandlers.dumber.postMessage({
166+ type: 'accent_key_press',
167+ payload: { char: key, shift: e.shiftKey }
168+ });
169+ }, true); // capture phase to see events first
170+
171+ document.addEventListener('keyup', function(e) {
172+ const key = e.key.toLowerCase();
173+ if (key === pressedKey) {
174+ pressedKey = null;
175+ window.webkit.messageHandlers.dumber.postMessage({
176+ type: 'accent_key_release',
177+ payload: { char: key }
178+ });
179+ }
180+ }, true);
181+})();`
182+
144183 // webRTCCompatScript maps legacy Safari-prefixed WebRTC globals to standard names.
145184 // Some pages gate support on window.RTCPeerConnection and report false negatives
146185 // when only webkit-prefixed constructors are present.
@@ -333,5 +372,18 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
333372 log.Debug().Msg("auto-copy selection script injected")
334373 }
335374
375+ // 7. Inject accent key detection for all pages (unconditional)
376+ // JS only reports keydown/keyup events; Go handles timing and picker display.
377+ addScript(
378+ webkit.NewUserScript(
379+ accentDetectionScript,
380+ webkit.UserContentInjectTopFrameValue,
381+ webkit.UserScriptInjectAtDocumentEndValue,
382+ nil, // all pages
383+ nil,
384+ ),
385+ "accent-key-detection",
386+ )
387+
336388 log.Debug().Bool("prefers_dark", prefersDark).Bool("auto_copy", autoCopyEnabled).Msg("scripts injected")
337389 }
@@ -141,6 +141,45 @@ const autoCopySelectionScript = `(function() {
141 });141 });
142 })();`142 })();`
143 143
144+// accentDetectionScript detects keydown/keyup of a-z keys in WebView inputs and
145+// forwards them to the Go-side InsertAccentUseCase via the message bridge.
146+// Go handles timing, accent lookup, and picker display — JS only reports events.
147+const accentDetectionScript = `(function() {
148+ 'use strict';
149+
150+ // Track pressed key for release matching
151+ let pressedKey = null;
152+
153+ document.addEventListener('keydown', function(e) {
154+ // Only handle a-z keys, with optional Shift
155+ // Skip if Ctrl or Alt are held (those are shortcuts)
156+ if (e.ctrlKey || e.altKey || e.metaKey) return;
157+
158+ // Only single character keys a-z
159+ const key = e.key.toLowerCase();
160+ if (key.length !== 1 || key < 'a' || key > 'z') return;
161+
162+ pressedKey = key;
163+
164+ // Send to Go for accent detection
165+ window.webkit.messageHandlers.dumber.postMessage({
166+ type: 'accent_key_press',
167+ payload: { char: key, shift: e.shiftKey }
168+ });
169+ }, true); // capture phase to see events first
170+
171+ document.addEventListener('keyup', function(e) {
172+ const key = e.key.toLowerCase();
173+ if (key === pressedKey) {
174+ pressedKey = null;
175+ window.webkit.messageHandlers.dumber.postMessage({
176+ type: 'accent_key_release',
177+ payload: { char: key }
178+ });
179+ }
180+ }, true);
181+})();`
182+
144 // webRTCCompatScript maps legacy Safari-prefixed WebRTC globals to standard names.183 // webRTCCompatScript maps legacy Safari-prefixed WebRTC globals to standard names.
145 // Some pages gate support on window.RTCPeerConnection and report false negatives184 // Some pages gate support on window.RTCPeerConnection and report false negatives
146 // when only webkit-prefixed constructors are present.185 // when only webkit-prefixed constructors are present.
@@ -333,5 +372,18 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo
333 log.Debug().Msg("auto-copy selection script injected")372 log.Debug().Msg("auto-copy selection script injected")
334 }373 }
335 374
375+ // 7. Inject accent key detection for all pages (unconditional)
376+ // JS only reports keydown/keyup events; Go handles timing and picker display.
377+ addScript(
378+ webkit.NewUserScript(
379+ accentDetectionScript,
380+ webkit.UserContentInjectTopFrameValue,
381+ webkit.UserScriptInjectAtDocumentEndValue,
382+ nil, // all pages
383+ nil,
384+ ),
385+ "accent-key-detection",
386+ )
387+
336 log.Debug().Bool("prefers_dark", prefersDark).Bool("auto_copy", autoCopyEnabled).Msg("scripts injected")388 log.Debug().Bool("prefers_dark", prefersDark).Bool("auto_copy", autoCopyEnabled).Msg("scripts injected")
337 }389 }
M internal/ui/app.go +61 -2
@@ -197,6 +197,7 @@ func New(deps *Dependencies) (*App, error) {
197197 FavoritesUC: deps.FavoritesUC,
198198 Clipboard: deps.Clipboard,
199199 ConfigGetter: config.Get,
200+ // AccentHandler is registered after initAccentPicker in onActivate
200201 OnClipboardCopied: func(textLen int) {
201202 // Show brief toast on auto-copy (similar to zellij footer notification)
202203 // Must schedule on GTK main thread since this is called from WebKit handler
@@ -291,6 +292,7 @@ func (a *App) onActivate(ctx context.Context) {
291292 a.installCrashReportNotifier(ctx)
292293 a.initFocusAndBorderOverlay()
293294 a.initAccentPicker(ctx)
295+ a.registerAccentHandlers(ctx)
294296 a.initDownloadHandler(ctx)
295297
296298 a.initCoordinators(ctx)
@@ -298,7 +300,7 @@ func (a *App) onActivate(ctx context.Context) {
298300 logging.Trace().Mark("coordinators_init")
299301 a.initKeyboardHandler(ctx)
300302 a.initOmniboxConfig(ctx)
301- a.initFindBarConfig()
303+ a.initFindBarConfig(ctx)
302304 a.initSessionManager(ctx)
303305 a.initTabPicker(ctx)
304306 a.initSnapshotService(ctx)
@@ -541,6 +543,21 @@ func (a *App) initAccentPicker(ctx context.Context) {
541543 log.Debug().Msg("accent picker initialized")
542544 }
543545
546+// registerAccentHandlers registers the accent key press/release message handlers
547+// with the router. Must be called after initAccentPicker so insertAccentUC is non-nil.
548+func (a *App) registerAccentHandlers(ctx context.Context) {
549+ log := logging.FromContext(ctx)
550+
551+ if a.router == nil || a.insertAccentUC == nil {
552+ log.Debug().Msg("router or insertAccentUC not available, skipping accent handler registration")
553+ return
554+ }
555+
556+ if err := handlers.RegisterAccentHandlers(ctx, a.router, a.insertAccentUC); err != nil {
557+ log.Error().Err(err).Msg("failed to register accent handlers")
558+ }
559+}
560+
544561 func (a *App) initDownloadHandler(ctx context.Context) {
545562 log := logging.FromContext(ctx)
546563
@@ -737,12 +754,33 @@ func (a *App) initOmniboxConfig(ctx context.Context) {
737754 a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget())
738755 }
739756 },
757+ OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool {
758+ if a.insertAccentUC == nil {
759+ return false
760+ }
761+ if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 {
762+ return false
763+ }
764+ shiftHeld := state&gdk.ShiftMaskValue != 0
765+ if char := input.KeyvalToRune(keyval); char != 0 {
766+ return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld)
767+ }
768+ return false
769+ },
770+ OnAccentKeyRelease: func(keyval uint) {
771+ if a.insertAccentUC == nil {
772+ return
773+ }
774+ if char := input.KeyvalToRune(keyval); char != 0 {
775+ a.insertAccentUC.OnKeyReleased(ctx, char)
776+ }
777+ },
740778 }
741779 a.navCoord.SetOmniboxProvider(a)
742780 log.Debug().Msg("omnibox config stored, provider set")
743781 }
744782
745-func (a *App) initFindBarConfig() {
783+func (a *App) initFindBarConfig(ctx context.Context) {
746784 // Store find bar config (find bar is created per-pane via WorkspaceView).
747785 a.findBarCfg = component.FindBarConfig{
748786 GetFindController: func(paneID entity.PaneID) port.FindController {
@@ -767,6 +805,27 @@ func (a *App) initFindBarConfig() {
767805 a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget())
768806 }
769807 },
808+ OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool {
809+ if a.insertAccentUC == nil {
810+ return false
811+ }
812+ if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 {
813+ return false
814+ }
815+ shiftHeld := state&gdk.ShiftMaskValue != 0
816+ if char := input.KeyvalToRune(keyval); char != 0 {
817+ return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld)
818+ }
819+ return false
820+ },
821+ OnAccentKeyRelease: func(keyval uint) {
822+ if a.insertAccentUC == nil {
823+ return
824+ }
825+ if char := input.KeyvalToRune(keyval); char != 0 {
826+ a.insertAccentUC.OnKeyReleased(ctx, char)
827+ }
828+ },
770829 }
771830 }
772831
@@ -197,6 +197,7 @@ func New(deps *Dependencies) (*App, error) {
197 FavoritesUC: deps.FavoritesUC,197 FavoritesUC: deps.FavoritesUC,
198 Clipboard: deps.Clipboard,198 Clipboard: deps.Clipboard,
199 ConfigGetter: config.Get,199 ConfigGetter: config.Get,
200+ // AccentHandler is registered after initAccentPicker in onActivate
200 OnClipboardCopied: func(textLen int) {201 OnClipboardCopied: func(textLen int) {
201 // Show brief toast on auto-copy (similar to zellij footer notification)202 // Show brief toast on auto-copy (similar to zellij footer notification)
202 // Must schedule on GTK main thread since this is called from WebKit handler203 // Must schedule on GTK main thread since this is called from WebKit handler
@@ -291,6 +292,7 @@ func (a *App) onActivate(ctx context.Context) {
291 a.installCrashReportNotifier(ctx)292 a.installCrashReportNotifier(ctx)
292 a.initFocusAndBorderOverlay()293 a.initFocusAndBorderOverlay()
293 a.initAccentPicker(ctx)294 a.initAccentPicker(ctx)
295+ a.registerAccentHandlers(ctx)
294 a.initDownloadHandler(ctx)296 a.initDownloadHandler(ctx)
295 297
296 a.initCoordinators(ctx)298 a.initCoordinators(ctx)
@@ -298,7 +300,7 @@ func (a *App) onActivate(ctx context.Context) {
298 logging.Trace().Mark("coordinators_init")300 logging.Trace().Mark("coordinators_init")
299 a.initKeyboardHandler(ctx)301 a.initKeyboardHandler(ctx)
300 a.initOmniboxConfig(ctx)302 a.initOmniboxConfig(ctx)
301- a.initFindBarConfig()303+ a.initFindBarConfig(ctx)
302 a.initSessionManager(ctx)304 a.initSessionManager(ctx)
303 a.initTabPicker(ctx)305 a.initTabPicker(ctx)
304 a.initSnapshotService(ctx)306 a.initSnapshotService(ctx)
@@ -541,6 +543,21 @@ func (a *App) initAccentPicker(ctx context.Context) {
541 log.Debug().Msg("accent picker initialized")543 log.Debug().Msg("accent picker initialized")
542 }544 }
543 545
546+// registerAccentHandlers registers the accent key press/release message handlers
547+// with the router. Must be called after initAccentPicker so insertAccentUC is non-nil.
548+func (a *App) registerAccentHandlers(ctx context.Context) {
549+ log := logging.FromContext(ctx)
550+
551+ if a.router == nil || a.insertAccentUC == nil {
552+ log.Debug().Msg("router or insertAccentUC not available, skipping accent handler registration")
553+ return
554+ }
555+
556+ if err := handlers.RegisterAccentHandlers(ctx, a.router, a.insertAccentUC); err != nil {
557+ log.Error().Err(err).Msg("failed to register accent handlers")
558+ }
559+}
560+
544 func (a *App) initDownloadHandler(ctx context.Context) {561 func (a *App) initDownloadHandler(ctx context.Context) {
545 log := logging.FromContext(ctx)562 log := logging.FromContext(ctx)
546 563
@@ -737,12 +754,33 @@ func (a *App) initOmniboxConfig(ctx context.Context) {
737 a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget())754 a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget())
738 }755 }
739 },756 },
757+ OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool {
758+ if a.insertAccentUC == nil {
759+ return false
760+ }
761+ if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 {
762+ return false
763+ }
764+ shiftHeld := state&gdk.ShiftMaskValue != 0
765+ if char := input.KeyvalToRune(keyval); char != 0 {
766+ return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld)
767+ }
768+ return false
769+ },
770+ OnAccentKeyRelease: func(keyval uint) {
771+ if a.insertAccentUC == nil {
772+ return
773+ }
774+ if char := input.KeyvalToRune(keyval); char != 0 {
775+ a.insertAccentUC.OnKeyReleased(ctx, char)
776+ }
777+ },
740 }778 }
741 a.navCoord.SetOmniboxProvider(a)779 a.navCoord.SetOmniboxProvider(a)
742 log.Debug().Msg("omnibox config stored, provider set")780 log.Debug().Msg("omnibox config stored, provider set")
743 }781 }
744 782
745-func (a *App) initFindBarConfig() {783+func (a *App) initFindBarConfig(ctx context.Context) {
746 // Store find bar config (find bar is created per-pane via WorkspaceView).784 // Store find bar config (find bar is created per-pane via WorkspaceView).
747 a.findBarCfg = component.FindBarConfig{785 a.findBarCfg = component.FindBarConfig{
748 GetFindController: func(paneID entity.PaneID) port.FindController {786 GetFindController: func(paneID entity.PaneID) port.FindController {
@@ -767,6 +805,27 @@ func (a *App) initFindBarConfig() {
767 a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget())805 a.accentFocusProvider.SetFocusedInput(a.getActiveWebViewTarget())
768 }806 }
769 },807 },
808+ OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool {
809+ if a.insertAccentUC == nil {
810+ return false
811+ }
812+ if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 {
813+ return false
814+ }
815+ shiftHeld := state&gdk.ShiftMaskValue != 0
816+ if char := input.KeyvalToRune(keyval); char != 0 {
817+ return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld)
818+ }
819+ return false
820+ },
821+ OnAccentKeyRelease: func(keyval uint) {
822+ if a.insertAccentUC == nil {
823+ return
824+ }
825+ if char := input.KeyvalToRune(keyval); char != 0 {
826+ a.insertAccentUC.OnKeyReleased(ctx, char)
827+ }
828+ },
770 }829 }
771 }830 }
772 831
M internal/ui/component/find_bar.go +32 -12
@@ -39,19 +39,23 @@ type FindBar struct {
3939
4040 uc *usecase.FindInPageUseCase
4141
42- visible bool
43- mu sync.RWMutex
44- ctx context.Context
45- onClose func()
46- retainedCallbacks []interface{} // Keep callbacks alive for GC
42+ visible bool
43+ mu sync.RWMutex
44+ ctx context.Context
45+ onClose func()
46+ onAccentKeyPress func(keyval uint, state gdk.ModifierType) bool
47+ onAccentKeyRelease func(keyval uint)
48+ retainedCallbacks []interface{} // Keep callbacks alive for GC
4749 }
4850
4951 // FindBarConfig holds configuration for creating a FindBar.
5052 type FindBarConfig struct {
51- OnClose func()
52- GetFindController func(paneID entity.PaneID) port.FindController
53- OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)
54- OnFocusOut func() // Callback when entry loses focus
53+ OnClose func()
54+ GetFindController func(paneID entity.PaneID) port.FindController
55+ OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)
56+ OnFocusOut func() // Callback when entry loses focus
57+ OnAccentKeyPress func(keyval uint, state gdk.ModifierType) bool // Long-press accent detection
58+ OnAccentKeyRelease func(keyval uint) // Key release for accent cancel
5559 }
5660
5761 // NewFindBar creates a new FindBar component.
@@ -59,9 +63,11 @@ func NewFindBar(ctx context.Context, cfg FindBarConfig) *FindBar {
5963 log := logging.FromContext(ctx)
6064
6165 fb := &FindBar{
62- ctx: ctx,
63- onClose: cfg.OnClose,
64- uc: usecase.NewFindInPageUseCase(ctx),
66+ ctx: ctx,
67+ onClose: cfg.OnClose,
68+ onAccentKeyPress: cfg.OnAccentKeyPress,
69+ onAccentKeyRelease: cfg.OnAccentKeyRelease,
70+ uc: usecase.NewFindInPageUseCase(ctx),
6571 }
6672
6773 if err := fb.createWidgets(); err != nil {
@@ -370,9 +376,23 @@ func (fb *FindBar) attachKeyController() {
370376 }
371377 return true
372378 }
379+ // Try long-press accent detection for text keys
380+ if fb.onAccentKeyPress != nil {
381+ return fb.onAccentKeyPress(keyval, state)
382+ }
373383 return false
374384 }
385+ fb.retainedCallbacks = append(fb.retainedCallbacks, keyPressedCb)
375386 controller.ConnectKeyPressed(&keyPressedCb)
387+
388+ keyReleasedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) {
389+ if fb.onAccentKeyRelease != nil {
390+ fb.onAccentKeyRelease(keyval)
391+ }
392+ }
393+ fb.retainedCallbacks = append(fb.retainedCallbacks, keyReleasedCb)
394+ controller.ConnectKeyReleased(&keyReleasedCb)
395+
376396 fb.outerBox.AddController(&controller.EventController)
377397 }
378398
@@ -39,19 +39,23 @@ type FindBar struct {
39 39
40 uc *usecase.FindInPageUseCase40 uc *usecase.FindInPageUseCase
41 41
42- visible bool42+ visible bool
43- mu sync.RWMutex43+ mu sync.RWMutex
44- ctx context.Context44+ ctx context.Context
45- onClose func()45+ onClose func()
46- retainedCallbacks []interface{} // Keep callbacks alive for GC46+ onAccentKeyPress func(keyval uint, state gdk.ModifierType) bool
47+ onAccentKeyRelease func(keyval uint)
48+ retainedCallbacks []interface{} // Keep callbacks alive for GC
47 }49 }
48 50
49 // FindBarConfig holds configuration for creating a FindBar.51 // FindBarConfig holds configuration for creating a FindBar.
50 type FindBarConfig struct {52 type FindBarConfig struct {
51- OnClose func()53+ OnClose func()
52- GetFindController func(paneID entity.PaneID) port.FindController54+ GetFindController func(paneID entity.PaneID) port.FindController
53- OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)55+ OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)
54- OnFocusOut func() // Callback when entry loses focus56+ OnFocusOut func() // Callback when entry loses focus
57+ OnAccentKeyPress func(keyval uint, state gdk.ModifierType) bool // Long-press accent detection
58+ OnAccentKeyRelease func(keyval uint) // Key release for accent cancel
55 }59 }
56 60
57 // NewFindBar creates a new FindBar component.61 // NewFindBar creates a new FindBar component.
@@ -59,9 +63,11 @@ func NewFindBar(ctx context.Context, cfg FindBarConfig) *FindBar {
59 log := logging.FromContext(ctx)63 log := logging.FromContext(ctx)
60 64
61 fb := &FindBar{65 fb := &FindBar{
62- ctx: ctx,66+ ctx: ctx,
63- onClose: cfg.OnClose,67+ onClose: cfg.OnClose,
64- uc: usecase.NewFindInPageUseCase(ctx),68+ onAccentKeyPress: cfg.OnAccentKeyPress,
69+ onAccentKeyRelease: cfg.OnAccentKeyRelease,
70+ uc: usecase.NewFindInPageUseCase(ctx),
65 }71 }
66 72
67 if err := fb.createWidgets(); err != nil {73 if err := fb.createWidgets(); err != nil {
@@ -370,9 +376,23 @@ func (fb *FindBar) attachKeyController() {
370 }376 }
371 return true377 return true
372 }378 }
379+ // Try long-press accent detection for text keys
380+ if fb.onAccentKeyPress != nil {
381+ return fb.onAccentKeyPress(keyval, state)
382+ }
373 return false383 return false
374 }384 }
385+ fb.retainedCallbacks = append(fb.retainedCallbacks, keyPressedCb)
375 controller.ConnectKeyPressed(&keyPressedCb)386 controller.ConnectKeyPressed(&keyPressedCb)
387+
388+ keyReleasedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) {
389+ if fb.onAccentKeyRelease != nil {
390+ fb.onAccentKeyRelease(keyval)
391+ }
392+ }
393+ fb.retainedCallbacks = append(fb.retainedCallbacks, keyReleasedCb)
394+ controller.ConnectKeyReleased(&keyReleasedCb)
395+
376 fb.outerBox.AddController(&controller.EventController)396 fb.outerBox.AddController(&controller.EventController)
377 }397 }
378 398
M internal/ui/component/omnibox.go +49 -28
@@ -111,9 +111,11 @@ type Omnibox struct {
111111 ctx context.Context
112112
113113 // Callbacks
114- onNavigate func(url string)
115- onClose func()
116- onToast func(ctx context.Context, message string, level ToastLevel)
114+ onNavigate func(url string)
115+ onClose func()
116+ onToast func(ctx context.Context, message string, level ToastLevel)
117+ onAccentKeyPress func(keyval uint, state gdk.ModifierType) bool
118+ onAccentKeyRelease func(keyval uint)
117119
118120 // Debouncing
119121 debounceTimer *time.Timer
@@ -144,18 +146,20 @@ type Omnibox struct {
144146
145147 // OmniboxConfig holds configuration for creating an Omnibox.
146148 type OmniboxConfig struct {
147- HistoryUC *usecase.SearchHistoryUseCase
148- FavoritesUC *usecase.ManageFavoritesUseCase
149- FaviconAdapter *adapter.FaviconAdapter
150- CopyURLUC *usecase.CopyURLUseCase
151- ShortcutsUC *usecase.SearchShortcutsUseCase
152- DefaultSearch string
153- InitialBehavior string
154- UIScale float64 // UI scale for favicon sizing
155- OnNavigate func(url string) // Callback when user navigates via omnibox
156- OnToast func(ctx context.Context, message string, level ToastLevel) // Callback to show toast notification
157- OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)
158- OnFocusOut func() // Callback when entry loses focus
149+ HistoryUC *usecase.SearchHistoryUseCase
150+ FavoritesUC *usecase.ManageFavoritesUseCase
151+ FaviconAdapter *adapter.FaviconAdapter
152+ CopyURLUC *usecase.CopyURLUseCase
153+ ShortcutsUC *usecase.SearchShortcutsUseCase
154+ DefaultSearch string
155+ InitialBehavior string
156+ UIScale float64 // UI scale for favicon sizing
157+ OnNavigate func(url string) // Callback when user navigates via omnibox
158+ OnToast func(ctx context.Context, message string, level ToastLevel) // Callback to show toast notification
159+ OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)
160+ OnFocusOut func() // Callback when entry loses focus
161+ OnAccentKeyPress func(keyval uint, state gdk.ModifierType) bool // Long-press accent detection
162+ OnAccentKeyRelease func(keyval uint) // Key release for accent cancel
159163 }
160164
161165 // NewOmnibox creates a new native GTK4 omnibox widget.
@@ -169,18 +173,20 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox {
169173 }
170174
171175 o := &Omnibox{
172- viewMode: ViewModeHistory,
173- selectedIndex: -1,
174- historyUC: cfg.HistoryUC,
175- favoritesUC: cfg.FavoritesUC,
176- faviconAdapter: cfg.FaviconAdapter,
177- copyURLUC: cfg.CopyURLUC,
178- shortcutsUC: cfg.ShortcutsUC,
179- defaultSearch: cfg.DefaultSearch,
180- initialBehavior: cfg.InitialBehavior,
181- onToast: cfg.OnToast,
182- ctx: ctx,
183- uiScale: uiScale,
176+ viewMode: ViewModeHistory,
177+ selectedIndex: -1,
178+ historyUC: cfg.HistoryUC,
179+ favoritesUC: cfg.FavoritesUC,
180+ faviconAdapter: cfg.FaviconAdapter,
181+ copyURLUC: cfg.CopyURLUC,
182+ shortcutsUC: cfg.ShortcutsUC,
183+ defaultSearch: cfg.DefaultSearch,
184+ initialBehavior: cfg.InitialBehavior,
185+ onToast: cfg.OnToast,
186+ onAccentKeyPress: cfg.OnAccentKeyPress,
187+ onAccentKeyRelease: cfg.OnAccentKeyRelease,
188+ ctx: ctx,
189+ uiScale: uiScale,
184190 }
185191 o.idleCoalescer = mainloop.NewCoalescer(func(fn func()) {
186192 var cb glib.SourceFunc = func(uintptr) bool {
@@ -427,11 +433,26 @@ func (o *Omnibox) setupKeyboardHandling() {
427433 controller.SetPropagationPhase(gtk.PhaseCaptureValue)
428434
429435 keyPressedCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool {
430- return o.handleKeyPress(keyval, keycode, state)
436+ if o.handleKeyPress(keyval, keycode, state) {
437+ return true
438+ }
439+ // Try long-press accent detection for text keys
440+ if o.onAccentKeyPress != nil {
441+ return o.onAccentKeyPress(keyval, state)
442+ }
443+ return false
431444 }
432445 o.retainedCallbacks = append(o.retainedCallbacks, keyPressedCb)
433446 controller.ConnectKeyPressed(&keyPressedCb)
434447
448+ keyReleasedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) {
449+ if o.onAccentKeyRelease != nil {
450+ o.onAccentKeyRelease(keyval)
451+ }
452+ }
453+ o.retainedCallbacks = append(o.retainedCallbacks, keyReleasedCb)
454+ controller.ConnectKeyReleased(&keyReleasedCb)
455+
435456 o.outerBox.AddController(&controller.EventController)
436457 }
437458
@@ -111,9 +111,11 @@ type Omnibox struct {
111 ctx context.Context111 ctx context.Context
112 112
113 // Callbacks113 // Callbacks
114- onNavigate func(url string)114+ onNavigate func(url string)
115- onClose func()115+ onClose func()
116- onToast func(ctx context.Context, message string, level ToastLevel)116+ onToast func(ctx context.Context, message string, level ToastLevel)
117+ onAccentKeyPress func(keyval uint, state gdk.ModifierType) bool
118+ onAccentKeyRelease func(keyval uint)
117 119
118 // Debouncing120 // Debouncing
119 debounceTimer *time.Timer121 debounceTimer *time.Timer
@@ -144,18 +146,20 @@ type Omnibox struct {
144 146
145 // OmniboxConfig holds configuration for creating an Omnibox.147 // OmniboxConfig holds configuration for creating an Omnibox.
146 type OmniboxConfig struct {148 type OmniboxConfig struct {
147- HistoryUC *usecase.SearchHistoryUseCase149+ HistoryUC *usecase.SearchHistoryUseCase
148- FavoritesUC *usecase.ManageFavoritesUseCase150+ FavoritesUC *usecase.ManageFavoritesUseCase
149- FaviconAdapter *adapter.FaviconAdapter151+ FaviconAdapter *adapter.FaviconAdapter
150- CopyURLUC *usecase.CopyURLUseCase152+ CopyURLUC *usecase.CopyURLUseCase
151- ShortcutsUC *usecase.SearchShortcutsUseCase153+ ShortcutsUC *usecase.SearchShortcutsUseCase
152- DefaultSearch string154+ DefaultSearch string
153- InitialBehavior string155+ InitialBehavior string
154- UIScale float64 // UI scale for favicon sizing156+ UIScale float64 // UI scale for favicon sizing
155- OnNavigate func(url string) // Callback when user navigates via omnibox157+ OnNavigate func(url string) // Callback when user navigates via omnibox
156- OnToast func(ctx context.Context, message string, level ToastLevel) // Callback to show toast notification158+ OnToast func(ctx context.Context, message string, level ToastLevel) // Callback to show toast notification
157- OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)159+ OnFocusIn func(entry *gtk.SearchEntry) // Callback when entry gains focus (for accent picker)
158- OnFocusOut func() // Callback when entry loses focus160+ OnFocusOut func() // Callback when entry loses focus
161+ OnAccentKeyPress func(keyval uint, state gdk.ModifierType) bool // Long-press accent detection
162+ OnAccentKeyRelease func(keyval uint) // Key release for accent cancel
159 }163 }
160 164
161 // NewOmnibox creates a new native GTK4 omnibox widget.165 // NewOmnibox creates a new native GTK4 omnibox widget.
@@ -169,18 +173,20 @@ func NewOmnibox(ctx context.Context, cfg OmniboxConfig) *Omnibox {
169 }173 }
170 174
171 o := &Omnibox{175 o := &Omnibox{
172- viewMode: ViewModeHistory,176+ viewMode: ViewModeHistory,
173- selectedIndex: -1,177+ selectedIndex: -1,
174- historyUC: cfg.HistoryUC,178+ historyUC: cfg.HistoryUC,
175- favoritesUC: cfg.FavoritesUC,179+ favoritesUC: cfg.FavoritesUC,
176- faviconAdapter: cfg.FaviconAdapter,180+ faviconAdapter: cfg.FaviconAdapter,
177- copyURLUC: cfg.CopyURLUC,181+ copyURLUC: cfg.CopyURLUC,
178- shortcutsUC: cfg.ShortcutsUC,182+ shortcutsUC: cfg.ShortcutsUC,
179- defaultSearch: cfg.DefaultSearch,183+ defaultSearch: cfg.DefaultSearch,
180- initialBehavior: cfg.InitialBehavior,184+ initialBehavior: cfg.InitialBehavior,
181- onToast: cfg.OnToast,185+ onToast: cfg.OnToast,
182- ctx: ctx,186+ onAccentKeyPress: cfg.OnAccentKeyPress,
183- uiScale: uiScale,187+ onAccentKeyRelease: cfg.OnAccentKeyRelease,
188+ ctx: ctx,
189+ uiScale: uiScale,
184 }190 }
185 o.idleCoalescer = mainloop.NewCoalescer(func(fn func()) {191 o.idleCoalescer = mainloop.NewCoalescer(func(fn func()) {
186 var cb glib.SourceFunc = func(uintptr) bool {192 var cb glib.SourceFunc = func(uintptr) bool {
@@ -427,11 +433,26 @@ func (o *Omnibox) setupKeyboardHandling() {
427 controller.SetPropagationPhase(gtk.PhaseCaptureValue)433 controller.SetPropagationPhase(gtk.PhaseCaptureValue)
428 434
429 keyPressedCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool {435 keyPressedCb := func(_ gtk.EventControllerKey, keyval, keycode uint, state gdk.ModifierType) bool {
430- return o.handleKeyPress(keyval, keycode, state)436+ if o.handleKeyPress(keyval, keycode, state) {
437+ return true
438+ }
439+ // Try long-press accent detection for text keys
440+ if o.onAccentKeyPress != nil {
441+ return o.onAccentKeyPress(keyval, state)
442+ }
443+ return false
431 }444 }
432 o.retainedCallbacks = append(o.retainedCallbacks, keyPressedCb)445 o.retainedCallbacks = append(o.retainedCallbacks, keyPressedCb)
433 controller.ConnectKeyPressed(&keyPressedCb)446 controller.ConnectKeyPressed(&keyPressedCb)
434 447
448+ keyReleasedCb := func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) {
449+ if o.onAccentKeyRelease != nil {
450+ o.onAccentKeyRelease(keyval)
451+ }
452+ }
453+ o.retainedCallbacks = append(o.retainedCallbacks, keyReleasedCb)
454+ controller.ConnectKeyReleased(&keyReleasedCb)
455+
435 o.outerBox.AddController(&controller.EventController)456 o.outerBox.AddController(&controller.EventController)
436 }457 }
437 458
M internal/ui/input/keyboard.go +12 -7
@@ -152,7 +152,10 @@ func (h *KeyboardHandler) Mode() Mode {
152152 }
153153
154154 // AttachTo attaches the keyboard handler to a GTK window.
155-// The handler will intercept key events in the capture phase.
155+// The handler runs in the bubble phase so that WebKit's IM pipeline (including
156+// dead-key / compose sequences) processes key events first. App-level shortcuts
157+// are still intercepted because WebKit only consumes events it recognises as
158+// text input; unhandled events bubble up to this controller.
156159 func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
157160 log := logging.FromContext(h.ctx)
158161
@@ -167,8 +170,10 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
167170 return
168171 }
169172
170- // Set capture phase to intercept events before WebView gets them
171- h.controller.SetPropagationPhase(gtk.PhaseCaptureValue)
173+ // Bubble phase: let WebKit IM handle key events first (dead keys, compose
174+ // sequences, input methods). Events not consumed by WebKit bubble up here
175+ // so app shortcuts still work.
176+ h.controller.SetPropagationPhase(gtk.PhaseBubbleValue)
172177
173178 // Connect key pressed handler (retain callback to prevent GC).
174179 // The callback receives: keyval (translated key), keycode (hardware key position), state (modifiers)
@@ -277,7 +282,7 @@ func (h *KeyboardHandler) tryAccentDetection(accentHandler AccentHandler, keyval
277282 if modifiers != 0 && modifiers != ModShift {
278283 return false
279284 }
280- if char := keyvalToRune(keyval); char != 0 {
285+ if char := KeyvalToRune(keyval); char != 0 {
281286 return accentHandler.OnKeyPressed(h.ctx, char, modifiers == ModShift)
282287 }
283288 return false
@@ -448,14 +453,14 @@ func (h *KeyboardHandler) handleKeyRelease(keyval uint) {
448453 }
449454
450455 // Convert keyval to rune for the accent handler
451- if char := keyvalToRune(keyval); char != 0 {
456+ if char := KeyvalToRune(keyval); char != 0 {
452457 accentHandler.OnKeyReleased(h.ctx, char)
453458 }
454459 }
455460
456-// keyvalToRune converts a GTK keyval to a lowercase rune.
461+// KeyvalToRune converts a GTK keyval to a lowercase rune.
457462 // Returns 0 if the keyval is not a printable character with potential accents.
458-func keyvalToRune(keyval uint) rune {
463+func KeyvalToRune(keyval uint) rune {
459464 // Handle lowercase a-z
460465 if keyval >= uint('a') && keyval <= uint('z') {
461466 return rune(keyval)
@@ -152,7 +152,10 @@ func (h *KeyboardHandler) Mode() Mode {
152 }152 }
153 153
154 // AttachTo attaches the keyboard handler to a GTK window.154 // AttachTo attaches the keyboard handler to a GTK window.
155-// The handler will intercept key events in the capture phase.155+// The handler runs in the bubble phase so that WebKit's IM pipeline (including
156+// dead-key / compose sequences) processes key events first. App-level shortcuts
157+// are still intercepted because WebKit only consumes events it recognises as
158+// text input; unhandled events bubble up to this controller.
156 func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {159 func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
157 log := logging.FromContext(h.ctx)160 log := logging.FromContext(h.ctx)
158 161
@@ -167,8 +170,10 @@ func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
167 return170 return
168 }171 }
169 172
170- // Set capture phase to intercept events before WebView gets them173+ // Bubble phase: let WebKit IM handle key events first (dead keys, compose
171- h.controller.SetPropagationPhase(gtk.PhaseCaptureValue)174+ // sequences, input methods). Events not consumed by WebKit bubble up here
175+ // so app shortcuts still work.
176+ h.controller.SetPropagationPhase(gtk.PhaseBubbleValue)
172 177
173 // Connect key pressed handler (retain callback to prevent GC).178 // Connect key pressed handler (retain callback to prevent GC).
174 // The callback receives: keyval (translated key), keycode (hardware key position), state (modifiers)179 // The callback receives: keyval (translated key), keycode (hardware key position), state (modifiers)
@@ -277,7 +282,7 @@ func (h *KeyboardHandler) tryAccentDetection(accentHandler AccentHandler, keyval
277 if modifiers != 0 && modifiers != ModShift {282 if modifiers != 0 && modifiers != ModShift {
278 return false283 return false
279 }284 }
280- if char := keyvalToRune(keyval); char != 0 {285+ if char := KeyvalToRune(keyval); char != 0 {
281 return accentHandler.OnKeyPressed(h.ctx, char, modifiers == ModShift)286 return accentHandler.OnKeyPressed(h.ctx, char, modifiers == ModShift)
282 }287 }
283 return false288 return false
@@ -448,14 +453,14 @@ func (h *KeyboardHandler) handleKeyRelease(keyval uint) {
448 }453 }
449 454
450 // Convert keyval to rune for the accent handler455 // Convert keyval to rune for the accent handler
451- if char := keyvalToRune(keyval); char != 0 {456+ if char := KeyvalToRune(keyval); char != 0 {
452 accentHandler.OnKeyReleased(h.ctx, char)457 accentHandler.OnKeyReleased(h.ctx, char)
453 }458 }
454 }459 }
455 460
456-// keyvalToRune converts a GTK keyval to a lowercase rune.461+// KeyvalToRune converts a GTK keyval to a lowercase rune.
457 // Returns 0 if the keyval is not a printable character with potential accents.462 // Returns 0 if the keyval is not a printable character with potential accents.
458-func keyvalToRune(keyval uint) rune {463+func KeyvalToRune(keyval uint) rune {
459 // Handle lowercase a-z464 // Handle lowercase a-z
460 if keyval >= uint('a') && keyval <= uint('z') {465 if keyval >= uint('a') && keyval <= uint('z') {
461 return rune(keyval)466 return rune(keyval)

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