fix: address thread-safety and nil-guard issues in keyboard routing
- Dispatch setControllerPhase via glib.IdleAdd since modal timeouts fire from non-GTK goroutines - Snapshot h.cfg under RLock in handleModeAction and Enter*Mode methods to prevent race with ReloadShortcuts - Log warning and fall through when kbHandler is nil for mode actions - Add nil check for NewGlobalShortcutHandler return value
- commit
- 9c71032b8b6d8b148999773ee0fbb381f6c367b1
- author
- brice · · 2026-03-13 06:44 UTC
- parent
- 7dee585
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +7 -5
M internal/ui/app.go +3 -1
| @@ -630,6 +630,8 @@ func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.D | |||
| 630 | 630 | } | |
| 631 | 631 | ||
| 632 | 632 | func (a *App) initKeyboardHandler(ctx context.Context) { | |
| 633 | + | log := logging.FromContext(ctx) | |
| 634 | + | ||
| 633 | 635 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | |
| 634 | 636 | return | |
| 635 | 637 | } | |
| @@ -717,7 +719,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 717 | 719 | }, | |
| 718 | 720 | ) | |
| 719 | 721 | if a.globalShortcutHandler == nil { | |
| 720 | - | logging.FromContext(ctx).Warn().Msg("failed to create global shortcut handler, some shortcuts may not work when WebView has focus") | |
| 722 | + | log.Warn().Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus") | |
| 721 | 723 | } | |
| 722 | 724 | } | |
| 723 | 725 | ||
| @@ -630,6 +630,8 @@ func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.D | |||||
| 630 | } | 630 | } | ||
| 631 | 631 | ||||
| 632 | func (a *App) initKeyboardHandler(ctx context.Context) { | 632 | func (a *App) initKeyboardHandler(ctx context.Context) { | ||
| 633 | + | log := logging.FromContext(ctx) | |||
| 634 | + | ||||
| 633 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | 635 | if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil { | ||
| 634 | return | 636 | return | ||
| 635 | } | 637 | } | ||
| @@ -717,7 +719,7 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||||
| 717 | }, | 719 | }, | ||
| 718 | ) | 720 | ) | ||
| 719 | if a.globalShortcutHandler == nil { | 721 | if a.globalShortcutHandler == nil { | ||
| 720 | - | logging.FromContext(ctx).Warn().Msg("failed to create global shortcut handler, some shortcuts may not work when WebView has focus") | 722 | + | log.Warn().Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus") |
| 721 | } | 723 | } | ||
| 722 | } | 724 | } | ||
| 723 | 725 | ||||
M internal/ui/input/global_shortcuts.go +1 -1
| @@ -203,7 +203,7 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi | |||
| 203 | 203 | } | |
| 204 | 204 | log.Warn(). | |
| 205 | 205 | Str("action", string(actionToDispatch)). | |
| 206 | - | Msg("mode action triggered but no keyboard handler available") | |
| 206 | + | Msg("mode action triggered but keyboard handler not set, falling through to default handler") | |
| 207 | 207 | } | |
| 208 | 208 | ||
| 209 | 209 | if h.onAction != nil { | |
| @@ -203,7 +203,7 @@ func (h *GlobalShortcutHandler) registerShortcut(keyval uint, modifiers gdk.Modi | |||||
| 203 | } | 203 | } | ||
| 204 | log.Warn(). | 204 | log.Warn(). | ||
| 205 | Str("action", string(actionToDispatch)). | 205 | Str("action", string(actionToDispatch)). | ||
| 206 | - | Msg("mode action triggered but no keyboard handler available") | 206 | + | Msg("mode action triggered but keyboard handler not set, falling through to default handler") |
| 207 | } | 207 | } | ||
| 208 | 208 | ||||
| 209 | if h.onAction != nil { | 209 | if h.onAction != nil { | ||
M internal/ui/input/keyboard.go +3 -3
| @@ -134,13 +134,13 @@ func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) { | |||
| 134 | 134 | } | |
| 135 | 135 | ||
| 136 | 136 | // setControllerPhase changes the propagation phase of the key controller. | |
| 137 | - | // Uses glib.IdleAdd to ensure the GTK call runs on the main thread, | |
| 138 | - | // since this may be called from modal timeout goroutines. | |
| 137 | + | // Uses glib.IdleAdd because this may be called from timer goroutines | |
| 138 | + | // (modal timeout) which are not on the GTK main thread. | |
| 139 | 139 | func (h *KeyboardHandler) setControllerPhase(phase gtk.PropagationPhase) { | |
| 140 | 140 | if h.controller == nil { | |
| 141 | 141 | return | |
| 142 | 142 | } | |
| 143 | - | cb := glib.SourceFunc(func(uintptr) bool { | |
| 143 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 144 | 144 | if h.controller != nil { | |
| 145 | 145 | h.controller.SetPropagationPhase(phase) | |
| 146 | 146 | } | |
| @@ -134,13 +134,13 @@ func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) { | |||||
| 134 | } | 134 | } | ||
| 135 | 135 | ||||
| 136 | // setControllerPhase changes the propagation phase of the key controller. | 136 | // setControllerPhase changes the propagation phase of the key controller. | ||
| 137 | - | // Uses glib.IdleAdd to ensure the GTK call runs on the main thread, | 137 | + | // Uses glib.IdleAdd because this may be called from timer goroutines |
| 138 | - | // since this may be called from modal timeout goroutines. | 138 | + | // (modal timeout) which are not on the GTK main thread. |
| 139 | func (h *KeyboardHandler) setControllerPhase(phase gtk.PropagationPhase) { | 139 | func (h *KeyboardHandler) setControllerPhase(phase gtk.PropagationPhase) { | ||
| 140 | if h.controller == nil { | 140 | if h.controller == nil { | ||
| 141 | return | 141 | return | ||
| 142 | } | 142 | } | ||
| 143 | - | cb := glib.SourceFunc(func(uintptr) bool { | 143 | + | cb := glib.SourceFunc(func(_ uintptr) bool { |
| 144 | if h.controller != nil { | 144 | if h.controller != nil { | ||
| 145 | h.controller.SetPropagationPhase(phase) | 145 | h.controller.SetPropagationPhase(phase) | ||
| 146 | } | 146 | } | ||