refactor(keybindings): address PR review feedback
- commit
- 1960dafff5ab5b50ac859218678186fa61b11277
- author
- brice · · 2026-01-24 08:35 UTC
- parent
- e36b143
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +113 -113
M docs/CONFIG.md +24 -9
| @@ -307,16 +307,31 @@ Notes: | |||
| 307 | 307 | ||
| 308 | 308 | ### Global Shortcuts | |
| 309 | 309 | ||
| 310 | - | | Key | Type | Default | Description | | |
| 311 | - | |-----|------|---------|-------------| | |
| 312 | - | | `workspace.shortcuts.close_pane` | string | `"ctrl+w"` | Close active pane (closes tab if last pane) | | |
| 313 | - | | `workspace.shortcuts.next_tab` | string | `"ctrl+tab"` | Next tab shortcut | | |
| 314 | - | | `workspace.shortcuts.previous_tab` | string | `"ctrl+shift+tab"` | Previous tab shortcut | | |
| 315 | - | | `workspace.shortcuts.consume_or_expel_left` | string | `"alt+["` | Consume into left sibling stack, or expel left if stacked (very alpha) | | |
| 316 | - | | `workspace.shortcuts.consume_or_expel_right` | string | `"alt+]"` | Consume into right sibling stack, or expel right if stacked (very alpha) | | |
| 317 | - | | `workspace.shortcuts.consume_or_expel_up` | string | `"alt+{"` | Consume into upper sibling stack, or expel up if stacked (very alpha) | | |
| 318 | - | | `workspace.shortcuts.consume_or_expel_down` | string | `"alt+}"` | Consume into lower sibling stack, or expel down if stacked (very alpha) | | |
| 310 | + | Global shortcuts are configured under `workspace.shortcuts.actions` using the same `ActionBinding` structure as modal keybindings: | |
| 311 | + | ||
| 312 | + | | Action | Default Keys | Description | | |
| 313 | + | |--------|--------------|-------------| | |
| 314 | + | | `close_pane` | `ctrl+w` | Close active pane (closes tab if last pane) | | |
| 315 | + | | `next_tab` | `ctrl+tab` | Switch to next tab | | |
| 316 | + | | `previous_tab` | `ctrl+shift+tab` | Switch to previous tab | | |
| 317 | + | | `consume_or_expel_left` | `alt+[` | Consume into left sibling stack, or expel left if stacked | | |
| 318 | + | | `consume_or_expel_right` | `alt+]` | Consume into right sibling stack, or expel right if stacked | | |
| 319 | + | | `consume_or_expel_up` | `alt+{` | Consume into upper sibling stack, or expel up if stacked | | |
| 320 | + | | `consume_or_expel_down` | `alt+}` | Consume into lower sibling stack, or expel down if stacked | | |
| 321 | + | ||
| 322 | + | **Example:** | |
| 323 | + | ```toml | |
| 324 | + | [workspace.shortcuts.actions.close_pane] | |
| 325 | + | keys = ["ctrl+w"] | |
| 326 | + | desc = "Close active pane" | |
| 327 | + | ||
| 328 | + | [workspace.shortcuts.actions.next_tab] | |
| 329 | + | keys = ["ctrl+tab", "alt+l"] | |
| 330 | + | desc = "Switch to next tab" | |
| 331 | + | ``` | |
| 319 | 332 | ||
| 333 | + | > **Tip:** Use `dumb://config` → Keybindings tab to edit shortcuts visually. | |
| 334 | + | > | |
| 320 | 335 | > **Note:** New tab creation uses modal tab mode (Ctrl+T then n/c). This follows the Zellij-style modal keyboard interface. | |
| 321 | 336 | > | |
| 322 | 337 | > **Note:** Consume-or-expel is experimental and may change behavior between releases. | |
| @@ -307,16 +307,31 @@ Notes: | |||||
| 307 | 307 | ||||
| 308 | ### Global Shortcuts | 308 | ### Global Shortcuts | ||
| 309 | 309 | ||||
| 310 | - | | Key | Type | Default | Description | | 310 | + | Global shortcuts are configured under `workspace.shortcuts.actions` using the same `ActionBinding` structure as modal keybindings: |
| 311 | - | |-----|------|---------|-------------| | 311 | + | |
| 312 | - | | `workspace.shortcuts.close_pane` | string | `"ctrl+w"` | Close active pane (closes tab if last pane) | | 312 | + | | Action | Default Keys | Description | |
| 313 | - | | `workspace.shortcuts.next_tab` | string | `"ctrl+tab"` | Next tab shortcut | | 313 | + | |--------|--------------|-------------| |
| 314 | - | | `workspace.shortcuts.previous_tab` | string | `"ctrl+shift+tab"` | Previous tab shortcut | | 314 | + | | `close_pane` | `ctrl+w` | Close active pane (closes tab if last pane) | |
| 315 | - | | `workspace.shortcuts.consume_or_expel_left` | string | `"alt+["` | Consume into left sibling stack, or expel left if stacked (very alpha) | | 315 | + | | `next_tab` | `ctrl+tab` | Switch to next tab | |
| 316 | - | | `workspace.shortcuts.consume_or_expel_right` | string | `"alt+]"` | Consume into right sibling stack, or expel right if stacked (very alpha) | | 316 | + | | `previous_tab` | `ctrl+shift+tab` | Switch to previous tab | |
| 317 | - | | `workspace.shortcuts.consume_or_expel_up` | string | `"alt+{"` | Consume into upper sibling stack, or expel up if stacked (very alpha) | | 317 | + | | `consume_or_expel_left` | `alt+[` | Consume into left sibling stack, or expel left if stacked | |
| 318 | - | | `workspace.shortcuts.consume_or_expel_down` | string | `"alt+}"` | Consume into lower sibling stack, or expel down if stacked (very alpha) | | 318 | + | | `consume_or_expel_right` | `alt+]` | Consume into right sibling stack, or expel right if stacked | |
| 319 | + | | `consume_or_expel_up` | `alt+{` | Consume into upper sibling stack, or expel up if stacked | | |||
| 320 | + | | `consume_or_expel_down` | `alt+}` | Consume into lower sibling stack, or expel down if stacked | | |||
| 321 | + | ||||
| 322 | + | **Example:** | |||
| 323 | + | ```toml | |||
| 324 | + | [workspace.shortcuts.actions.close_pane] | |||
| 325 | + | keys = ["ctrl+w"] | |||
| 326 | + | desc = "Close active pane" | |||
| 327 | + | ||||
| 328 | + | [workspace.shortcuts.actions.next_tab] | |||
| 329 | + | keys = ["ctrl+tab", "alt+l"] | |||
| 330 | + | desc = "Switch to next tab" | |||
| 331 | + | ``` | |||
| 319 | 332 | ||||
| 333 | + | > **Tip:** Use `dumb://config` → Keybindings tab to edit shortcuts visually. | |||
| 334 | + | > | |||
| 320 | > **Note:** New tab creation uses modal tab mode (Ctrl+T then n/c). This follows the Zellij-style modal keyboard interface. | 335 | > **Note:** New tab creation uses modal tab mode (Ctrl+T then n/c). This follows the Zellij-style modal keyboard interface. | ||
| 321 | > | 336 | > | ||
| 322 | > **Note:** Consume-or-expel is experimental and may change behavior between releases. | 337 | > **Note:** Consume-or-expel is experimental and may change behavior between releases. | ||
M internal/application/usecase/keybindings.go +9 -2
| @@ -7,6 +7,15 @@ import ( | |||
| 7 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | + | // validModes defines the valid keybinding modes. | |
| 11 | + | var validModes = map[string]bool{ | |
| 12 | + | "global": true, | |
| 13 | + | "pane": true, | |
| 14 | + | "tab": true, | |
| 15 | + | "resize": true, | |
| 16 | + | "session": true, | |
| 17 | + | } | |
| 18 | + | ||
| 10 | 19 | // GetKeybindingsUseCase retrieves all keybindings. | |
| 11 | 20 | type GetKeybindingsUseCase struct { | |
| 12 | 21 | provider port.KeybindingsProvider | |
| @@ -96,7 +105,6 @@ func validateSetKeybindingRequest(req port.SetKeybindingRequest) error { | |||
| 96 | 105 | if req.Action == "" { | |
| 97 | 106 | return fmt.Errorf("action is required") | |
| 98 | 107 | } | |
| 99 | - | validModes := map[string]bool{"global": true, "pane": true, "tab": true, "resize": true, "session": true} | |
| 100 | 108 | if !validModes[req.Mode] { | |
| 101 | 109 | return fmt.Errorf("invalid mode: %s", req.Mode) | |
| 102 | 110 | } | |
| @@ -110,7 +118,6 @@ func validateResetKeybindingRequest(req port.ResetKeybindingRequest) error { | |||
| 110 | 118 | if req.Action == "" { | |
| 111 | 119 | return fmt.Errorf("action is required") | |
| 112 | 120 | } | |
| 113 | - | validModes := map[string]bool{"global": true, "pane": true, "tab": true, "resize": true, "session": true} | |
| 114 | 121 | if !validModes[req.Mode] { | |
| 115 | 122 | return fmt.Errorf("invalid mode: %s", req.Mode) | |
| 116 | 123 | } | |
| @@ -7,6 +7,15 @@ import ( | |||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 7 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| 10 | + | // validModes defines the valid keybinding modes. | |||
| 11 | + | var validModes = map[string]bool{ | |||
| 12 | + | "global": true, | |||
| 13 | + | "pane": true, | |||
| 14 | + | "tab": true, | |||
| 15 | + | "resize": true, | |||
| 16 | + | "session": true, | |||
| 17 | + | } | |||
| 18 | + | ||||
| 10 | // GetKeybindingsUseCase retrieves all keybindings. | 19 | // GetKeybindingsUseCase retrieves all keybindings. | ||
| 11 | type GetKeybindingsUseCase struct { | 20 | type GetKeybindingsUseCase struct { | ||
| 12 | provider port.KeybindingsProvider | 21 | provider port.KeybindingsProvider | ||
| @@ -96,7 +105,6 @@ func validateSetKeybindingRequest(req port.SetKeybindingRequest) error { | |||||
| 96 | if req.Action == "" { | 105 | if req.Action == "" { | ||
| 97 | return fmt.Errorf("action is required") | 106 | return fmt.Errorf("action is required") | ||
| 98 | } | 107 | } | ||
| 99 | - | validModes := map[string]bool{"global": true, "pane": true, "tab": true, "resize": true, "session": true} | |||
| 100 | if !validModes[req.Mode] { | 108 | if !validModes[req.Mode] { | ||
| 101 | return fmt.Errorf("invalid mode: %s", req.Mode) | 109 | return fmt.Errorf("invalid mode: %s", req.Mode) | ||
| 102 | } | 110 | } | ||
| @@ -110,7 +118,6 @@ func validateResetKeybindingRequest(req port.ResetKeybindingRequest) error { | |||||
| 110 | if req.Action == "" { | 118 | if req.Action == "" { | ||
| 111 | return fmt.Errorf("action is required") | 119 | return fmt.Errorf("action is required") | ||
| 112 | } | 120 | } | ||
| 113 | - | validModes := map[string]bool{"global": true, "pane": true, "tab": true, "resize": true, "session": true} | |||
| 114 | if !validModes[req.Mode] { | 121 | if !validModes[req.Mode] { | ||
| 115 | return fmt.Errorf("invalid mode: %s", req.Mode) | 122 | return fmt.Errorf("invalid mode: %s", req.Mode) | ||
| 116 | } | 123 | } | ||
M internal/infrastructure/config/loader.go +0 -33
| @@ -297,39 +297,6 @@ func (m *Manager) Save(cfg *Config) error { | |||
| 297 | 297 | return nil | |
| 298 | 298 | } | |
| 299 | 299 | ||
| 300 | - | // SaveKeybindings saves keybinding-related configuration to disk with ordered sections. | |
| 301 | - | func (m *Manager) SaveKeybindings(cfg *Config) error { | |
| 302 | - | m.mu.Lock() | |
| 303 | - | defer m.mu.Unlock() | |
| 304 | - | ||
| 305 | - | if cfg == nil { | |
| 306 | - | return fmt.Errorf("config is nil") | |
| 307 | - | } | |
| 308 | - | ||
| 309 | - | // Write full config with ordered sections | |
| 310 | - | configFile := m.viper.ConfigFileUsed() | |
| 311 | - | if configFile == "" { | |
| 312 | - | var err error | |
| 313 | - | configFile, err = GetConfigFile() | |
| 314 | - | if err != nil { | |
| 315 | - | return fmt.Errorf("failed to determine config file path: %w", err) | |
| 316 | - | } | |
| 317 | - | } | |
| 318 | - | ||
| 319 | - | if err := WriteConfigOrdered(cfg, configFile); err != nil { | |
| 320 | - | return fmt.Errorf("failed to write config: %w", err) | |
| 321 | - | } | |
| 322 | - | ||
| 323 | - | // Reload if not watching | |
| 324 | - | if !m.watching { | |
| 325 | - | if err := m.reload(); err != nil { | |
| 326 | - | return err | |
| 327 | - | } | |
| 328 | - | } | |
| 329 | - | ||
| 330 | - | return nil | |
| 331 | - | } | |
| 332 | - | ||
| 333 | 300 | // GetConfigFile returns the path to the configuration file being used. | |
| 334 | 301 | func (m *Manager) GetConfigFile() string { | |
| 335 | 302 | return m.viper.ConfigFileUsed() | |
| @@ -297,39 +297,6 @@ func (m *Manager) Save(cfg *Config) error { | |||||
| 297 | return nil | 297 | return nil | ||
| 298 | } | 298 | } | ||
| 299 | 299 | ||||
| 300 | - | // SaveKeybindings saves keybinding-related configuration to disk with ordered sections. | |||
| 301 | - | func (m *Manager) SaveKeybindings(cfg *Config) error { | |||
| 302 | - | m.mu.Lock() | |||
| 303 | - | defer m.mu.Unlock() | |||
| 304 | - | ||||
| 305 | - | if cfg == nil { | |||
| 306 | - | return fmt.Errorf("config is nil") | |||
| 307 | - | } | |||
| 308 | - | ||||
| 309 | - | // Write full config with ordered sections | |||
| 310 | - | configFile := m.viper.ConfigFileUsed() | |||
| 311 | - | if configFile == "" { | |||
| 312 | - | var err error | |||
| 313 | - | configFile, err = GetConfigFile() | |||
| 314 | - | if err != nil { | |||
| 315 | - | return fmt.Errorf("failed to determine config file path: %w", err) | |||
| 316 | - | } | |||
| 317 | - | } | |||
| 318 | - | ||||
| 319 | - | if err := WriteConfigOrdered(cfg, configFile); err != nil { | |||
| 320 | - | return fmt.Errorf("failed to write config: %w", err) | |||
| 321 | - | } | |||
| 322 | - | ||||
| 323 | - | // Reload if not watching | |||
| 324 | - | if !m.watching { | |||
| 325 | - | if err := m.reload(); err != nil { | |||
| 326 | - | return err | |||
| 327 | - | } | |||
| 328 | - | } | |||
| 329 | - | ||||
| 330 | - | return nil | |||
| 331 | - | } | |||
| 332 | - | ||||
| 333 | // GetConfigFile returns the path to the configuration file being used. | 300 | // GetConfigFile returns the path to the configuration file being used. | ||
| 334 | func (m *Manager) GetConfigFile() string { | 301 | func (m *Manager) GetConfigFile() string { | ||
| 335 | return m.viper.ConfigFileUsed() | 302 | return m.viper.ConfigFileUsed() | ||
M internal/infrastructure/webkit/handlers/keybindings.go +6 -6
| @@ -75,7 +75,7 @@ func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.Web | |||
| 75 | 75 | ||
| 76 | 76 | updateKeybinding(cfg, req) | |
| 77 | 77 | ||
| 78 | - | if err := mgr.SaveKeybindings(cfg); err != nil { | |
| 78 | + | if err := mgr.Save(cfg); err != nil { | |
| 79 | 79 | return nil, fmt.Errorf("failed to save: %w", err) | |
| 80 | 80 | } | |
| 81 | 81 | ||
| @@ -113,7 +113,7 @@ func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.W | |||
| 113 | 113 | cfg := mgr.Get() | |
| 114 | 114 | updateKeybinding(cfg, setReq) | |
| 115 | 115 | ||
| 116 | - | if err := mgr.SaveKeybindings(cfg); err != nil { | |
| 116 | + | if err := mgr.Save(cfg); err != nil { | |
| 117 | 117 | return nil, fmt.Errorf("failed to save: %w", err) | |
| 118 | 118 | } | |
| 119 | 119 | ||
| @@ -140,7 +140,7 @@ func (*KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webk | |||
| 140 | 140 | cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts | |
| 141 | 141 | cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions | |
| 142 | 142 | ||
| 143 | - | if err := mgr.SaveKeybindings(cfg); err != nil { | |
| 143 | + | if err := mgr.Save(cfg); err != nil { | |
| 144 | 144 | return nil, fmt.Errorf("failed to save: %w", err) | |
| 145 | 145 | } | |
| 146 | 146 | ||
| @@ -356,7 +356,7 @@ func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRout | |||
| 356 | 356 | if err := router.RegisterHandlerWithCallbacks( | |
| 357 | 357 | "set_keybinding", | |
| 358 | 358 | "__dumber_keybinding_set", | |
| 359 | - | "__dumber_keybinding_error", | |
| 359 | + | "__dumber_keybinding_set_error", | |
| 360 | 360 | "", | |
| 361 | 361 | webkit.MessageHandlerFunc(handler.HandleSetKeybinding), | |
| 362 | 362 | ); err != nil { | |
| @@ -367,7 +367,7 @@ func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRout | |||
| 367 | 367 | if err := router.RegisterHandlerWithCallbacks( | |
| 368 | 368 | "reset_keybinding", | |
| 369 | 369 | "__dumber_keybinding_reset", | |
| 370 | - | "__dumber_keybinding_error", | |
| 370 | + | "__dumber_keybinding_reset_error", | |
| 371 | 371 | "", | |
| 372 | 372 | webkit.MessageHandlerFunc(handler.HandleResetKeybinding), | |
| 373 | 373 | ); err != nil { | |
| @@ -378,7 +378,7 @@ func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRout | |||
| 378 | 378 | if err := router.RegisterHandlerWithCallbacks( | |
| 379 | 379 | "reset_all_keybindings", | |
| 380 | 380 | "__dumber_keybindings_reset_all", | |
| 381 | - | "__dumber_keybindings_error", | |
| 381 | + | "__dumber_keybindings_reset_all_error", | |
| 382 | 382 | "", | |
| 383 | 383 | webkit.MessageHandlerFunc(handler.HandleResetAllKeybindings), | |
| 384 | 384 | ); err != nil { | |
| @@ -75,7 +75,7 @@ func (*KeybindingsHandler) HandleSetKeybinding(ctx context.Context, _ webkit.Web | |||||
| 75 | 75 | ||||
| 76 | updateKeybinding(cfg, req) | 76 | updateKeybinding(cfg, req) | ||
| 77 | 77 | ||||
| 78 | - | if err := mgr.SaveKeybindings(cfg); err != nil { | 78 | + | if err := mgr.Save(cfg); err != nil { |
| 79 | return nil, fmt.Errorf("failed to save: %w", err) | 79 | return nil, fmt.Errorf("failed to save: %w", err) | ||
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| @@ -113,7 +113,7 @@ func (*KeybindingsHandler) HandleResetKeybinding(ctx context.Context, _ webkit.W | |||||
| 113 | cfg := mgr.Get() | 113 | cfg := mgr.Get() | ||
| 114 | updateKeybinding(cfg, setReq) | 114 | updateKeybinding(cfg, setReq) | ||
| 115 | 115 | ||||
| 116 | - | if err := mgr.SaveKeybindings(cfg); err != nil { | 116 | + | if err := mgr.Save(cfg); err != nil { |
| 117 | return nil, fmt.Errorf("failed to save: %w", err) | 117 | return nil, fmt.Errorf("failed to save: %w", err) | ||
| 118 | } | 118 | } | ||
| 119 | 119 | ||||
| @@ -140,7 +140,7 @@ func (*KeybindingsHandler) HandleResetAllKeybindings(ctx context.Context, _ webk | |||||
| 140 | cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts | 140 | cfg.Workspace.Shortcuts = defaults.Workspace.Shortcuts | ||
| 141 | cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions | 141 | cfg.Session.SessionMode.Actions = defaults.Session.SessionMode.Actions | ||
| 142 | 142 | ||||
| 143 | - | if err := mgr.SaveKeybindings(cfg); err != nil { | 143 | + | if err := mgr.Save(cfg); err != nil { |
| 144 | return nil, fmt.Errorf("failed to save: %w", err) | 144 | return nil, fmt.Errorf("failed to save: %w", err) | ||
| 145 | } | 145 | } | ||
| 146 | 146 | ||||
| @@ -356,7 +356,7 @@ func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRout | |||||
| 356 | if err := router.RegisterHandlerWithCallbacks( | 356 | if err := router.RegisterHandlerWithCallbacks( | ||
| 357 | "set_keybinding", | 357 | "set_keybinding", | ||
| 358 | "__dumber_keybinding_set", | 358 | "__dumber_keybinding_set", | ||
| 359 | - | "__dumber_keybinding_error", | 359 | + | "__dumber_keybinding_set_error", |
| 360 | "", | 360 | "", | ||
| 361 | webkit.MessageHandlerFunc(handler.HandleSetKeybinding), | 361 | webkit.MessageHandlerFunc(handler.HandleSetKeybinding), | ||
| 362 | ); err != nil { | 362 | ); err != nil { | ||
| @@ -367,7 +367,7 @@ func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRout | |||||
| 367 | if err := router.RegisterHandlerWithCallbacks( | 367 | if err := router.RegisterHandlerWithCallbacks( | ||
| 368 | "reset_keybinding", | 368 | "reset_keybinding", | ||
| 369 | "__dumber_keybinding_reset", | 369 | "__dumber_keybinding_reset", | ||
| 370 | - | "__dumber_keybinding_error", | 370 | + | "__dumber_keybinding_reset_error", |
| 371 | "", | 371 | "", | ||
| 372 | webkit.MessageHandlerFunc(handler.HandleResetKeybinding), | 372 | webkit.MessageHandlerFunc(handler.HandleResetKeybinding), | ||
| 373 | ); err != nil { | 373 | ); err != nil { | ||
| @@ -378,7 +378,7 @@ func RegisterKeybindingsHandlers(ctx context.Context, router *webkit.MessageRout | |||||
| 378 | if err := router.RegisterHandlerWithCallbacks( | 378 | if err := router.RegisterHandlerWithCallbacks( | ||
| 379 | "reset_all_keybindings", | 379 | "reset_all_keybindings", | ||
| 380 | "__dumber_keybindings_reset_all", | 380 | "__dumber_keybindings_reset_all", | ||
| 381 | - | "__dumber_keybindings_error", | 381 | + | "__dumber_keybindings_reset_all_error", |
| 382 | "", | 382 | "", | ||
| 383 | webkit.MessageHandlerFunc(handler.HandleResetAllKeybindings), | 383 | webkit.MessageHandlerFunc(handler.HandleResetAllKeybindings), | ||
| 384 | ); err != nil { | 384 | ); err != nil { | ||
M webui/src/lib/bridge.ts +14 -15
| @@ -60,8 +60,6 @@ export function toPlainObject<T>(obj: T): T { | |||
| 60 | 60 | * @throws Error if bridge is not available or serialization fails | |
| 61 | 61 | */ | |
| 62 | 62 | export function postMessage(msg: BridgeMessage): void { | |
| 63 | - | console.log("[bridge] postMessage called with:", msg); | |
| 64 | - | ||
| 65 | 63 | const bridge = getWebKitBridge(); | |
| 66 | 64 | if (!bridge) { | |
| 67 | 65 | throw new Error("WebKit bridge not available"); | |
| @@ -74,22 +72,12 @@ export function postMessage(msg: BridgeMessage): void { | |||
| 74 | 72 | ||
| 75 | 73 | // Convert to plain object to strip Svelte 5 Proxies | |
| 76 | 74 | // This is critical - WebKit silently drops messages with Proxy objects | |
| 77 | - | let plainMsg: BridgeMessage; | |
| 78 | - | try { | |
| 79 | - | plainMsg = toPlainObject(msg); | |
| 80 | - | console.log("[bridge] Serialized message (Proxy stripped):", plainMsg); | |
| 81 | - | } catch (err) { | |
| 82 | - | console.error("[bridge] Failed to serialize message:", err, msg); | |
| 83 | - | throw err; | |
| 84 | - | } | |
| 85 | - | ||
| 86 | - | console.log("[bridge] Sending to WebKit bridge..."); | |
| 75 | + | const plainMsg = toPlainObject(msg); | |
| 87 | 76 | bridge.postMessage(plainMsg); | |
| 88 | - | console.log("[bridge] Message sent successfully"); | |
| 89 | 77 | } | |
| 90 | 78 | ||
| 91 | 79 | /** | |
| 92 | - | * Post a message and set up success/error callbacks. | |
| 80 | + | * Post a message and set up success/error callbacks with automatic cleanup. | |
| 93 | 81 | * | |
| 94 | 82 | * @param type - Message type (e.g., "get_keybindings", "set_keybinding") | |
| 95 | 83 | * @param payload - Message payload (will be serialized to strip Proxies) | |
| @@ -106,13 +94,23 @@ export function postMessageWithCallbacks<TResponse, TPayload extends Record<stri | |||
| 106 | 94 | onSuccess: (response: TResponse) => void, | |
| 107 | 95 | onError: (error: string) => void, | |
| 108 | 96 | ): void { | |
| 109 | - | // Set up callbacks on window | |
| 97 | + | // Cleanup function to remove callbacks from window | |
| 98 | + | const cleanup = () => { | |
| 99 | + | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 100 | + | delete (window as any)[successCallback]; | |
| 101 | + | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 102 | + | delete (window as any)[errorCallback]; | |
| 103 | + | }; | |
| 104 | + | ||
| 105 | + | // Set up callbacks on window with automatic cleanup | |
| 110 | 106 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 111 | 107 | (window as any)[successCallback] = (response: TResponse) => { | |
| 108 | + | cleanup(); | |
| 112 | 109 | onSuccess(response); | |
| 113 | 110 | }; | |
| 114 | 111 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 115 | 112 | (window as any)[errorCallback] = (msg: string) => { | |
| 113 | + | cleanup(); | |
| 116 | 114 | onError(typeof msg === "string" ? msg : "Unknown error"); | |
| 117 | 115 | }; | |
| 118 | 116 | ||
| @@ -123,6 +121,7 @@ export function postMessageWithCallbacks<TResponse, TPayload extends Record<stri | |||
| 123 | 121 | payload, | |
| 124 | 122 | }); | |
| 125 | 123 | } catch (err) { | |
| 124 | + | cleanup(); | |
| 126 | 125 | const message = err instanceof Error ? err.message : String(err); | |
| 127 | 126 | onError(message); | |
| 128 | 127 | } | |
| @@ -60,8 +60,6 @@ export function toPlainObject<T>(obj: T): T { | |||||
| 60 | * @throws Error if bridge is not available or serialization fails | 60 | * @throws Error if bridge is not available or serialization fails | ||
| 61 | */ | 61 | */ | ||
| 62 | export function postMessage(msg: BridgeMessage): void { | 62 | export function postMessage(msg: BridgeMessage): void { | ||
| 63 | - | console.log("[bridge] postMessage called with:", msg); | |||
| 64 | - | ||||
| 65 | const bridge = getWebKitBridge(); | 63 | const bridge = getWebKitBridge(); | ||
| 66 | if (!bridge) { | 64 | if (!bridge) { | ||
| 67 | throw new Error("WebKit bridge not available"); | 65 | throw new Error("WebKit bridge not available"); | ||
| @@ -74,22 +72,12 @@ export function postMessage(msg: BridgeMessage): void { | |||||
| 74 | 72 | ||||
| 75 | // Convert to plain object to strip Svelte 5 Proxies | 73 | // Convert to plain object to strip Svelte 5 Proxies | ||
| 76 | // This is critical - WebKit silently drops messages with Proxy objects | 74 | // This is critical - WebKit silently drops messages with Proxy objects | ||
| 77 | - | let plainMsg: BridgeMessage; | 75 | + | const plainMsg = toPlainObject(msg); |
| 78 | - | try { | |||
| 79 | - | plainMsg = toPlainObject(msg); | |||
| 80 | - | console.log("[bridge] Serialized message (Proxy stripped):", plainMsg); | |||
| 81 | - | } catch (err) { | |||
| 82 | - | console.error("[bridge] Failed to serialize message:", err, msg); | |||
| 83 | - | throw err; | |||
| 84 | - | } | |||
| 85 | - | ||||
| 86 | - | console.log("[bridge] Sending to WebKit bridge..."); | |||
| 87 | bridge.postMessage(plainMsg); | 76 | bridge.postMessage(plainMsg); | ||
| 88 | - | console.log("[bridge] Message sent successfully"); | |||
| 89 | } | 77 | } | ||
| 90 | 78 | ||||
| 91 | /** | 79 | /** | ||
| 92 | - | * Post a message and set up success/error callbacks. | 80 | + | * Post a message and set up success/error callbacks with automatic cleanup. |
| 93 | * | 81 | * | ||
| 94 | * @param type - Message type (e.g., "get_keybindings", "set_keybinding") | 82 | * @param type - Message type (e.g., "get_keybindings", "set_keybinding") | ||
| 95 | * @param payload - Message payload (will be serialized to strip Proxies) | 83 | * @param payload - Message payload (will be serialized to strip Proxies) | ||
| @@ -106,13 +94,23 @@ export function postMessageWithCallbacks<TResponse, TPayload extends Record<stri | |||||
| 106 | onSuccess: (response: TResponse) => void, | 94 | onSuccess: (response: TResponse) => void, | ||
| 107 | onError: (error: string) => void, | 95 | onError: (error: string) => void, | ||
| 108 | ): void { | 96 | ): void { | ||
| 109 | - | // Set up callbacks on window | 97 | + | // Cleanup function to remove callbacks from window |
| 98 | + | const cleanup = () => { | |||
| 99 | + | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
| 100 | + | delete (window as any)[successCallback]; | |||
| 101 | + | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
| 102 | + | delete (window as any)[errorCallback]; | |||
| 103 | + | }; | |||
| 104 | + | ||||
| 105 | + | // Set up callbacks on window with automatic cleanup | |||
| 110 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | 106 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| 111 | (window as any)[successCallback] = (response: TResponse) => { | 107 | (window as any)[successCallback] = (response: TResponse) => { | ||
| 108 | + | cleanup(); | |||
| 112 | onSuccess(response); | 109 | onSuccess(response); | ||
| 113 | }; | 110 | }; | ||
| 114 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | 111 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| 115 | (window as any)[errorCallback] = (msg: string) => { | 112 | (window as any)[errorCallback] = (msg: string) => { | ||
| 113 | + | cleanup(); | |||
| 116 | onError(typeof msg === "string" ? msg : "Unknown error"); | 114 | onError(typeof msg === "string" ? msg : "Unknown error"); | ||
| 117 | }; | 115 | }; | ||
| 118 | 116 | ||||
| @@ -123,6 +121,7 @@ export function postMessageWithCallbacks<TResponse, TPayload extends Record<stri | |||||
| 123 | payload, | 121 | payload, | ||
| 124 | }); | 122 | }); | ||
| 125 | } catch (err) { | 123 | } catch (err) { | ||
| 124 | + | cleanup(); | |||
| 126 | const message = err instanceof Error ? err.message : String(err); | 125 | const message = err instanceof Error ? err.message : String(err); | ||
| 127 | onError(message); | 126 | onError(message); | ||
| 128 | } | 127 | } | ||
A webui/src/lib/utils/keys.ts +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | /** | |
| 2 | + | * Format a keybinding string for display. | |
| 3 | + | * Converts internal key names to user-friendly display format. | |
| 4 | + | * | |
| 5 | + | * Examples: | |
| 6 | + | * "ctrl+shift+a" -> "Ctrl + Shift + A" | |
| 7 | + | * "arrowleft" -> "Left" | |
| 8 | + | * "alt+[" -> "Alt + [" | |
| 9 | + | */ | |
| 10 | + | export function formatKey(key: string): string { | |
| 11 | + | // Split by '+' separator first, then format each part | |
| 12 | + | // This avoids replacing '+' characters that are actual key names | |
| 13 | + | const parts = key.split("+"); | |
| 14 | + | ||
| 15 | + | const formattedParts = parts.map((part) => { | |
| 16 | + | const lower = part.toLowerCase(); | |
| 17 | + | switch (lower) { | |
| 18 | + | case "arrowleft": | |
| 19 | + | return "Left"; | |
| 20 | + | case "arrowright": | |
| 21 | + | return "Right"; | |
| 22 | + | case "arrowup": | |
| 23 | + | return "Up"; | |
| 24 | + | case "arrowdown": | |
| 25 | + | return "Down"; | |
| 26 | + | case "ctrl": | |
| 27 | + | return "Ctrl"; | |
| 28 | + | case "alt": | |
| 29 | + | return "Alt"; | |
| 30 | + | case "shift": | |
| 31 | + | return "Shift"; | |
| 32 | + | case "space": | |
| 33 | + | return "Space"; | |
| 34 | + | case "enter": | |
| 35 | + | return "Enter"; | |
| 36 | + | case "tab": | |
| 37 | + | return "Tab"; | |
| 38 | + | case "backspace": | |
| 39 | + | return "Backspace"; | |
| 40 | + | case "escape": | |
| 41 | + | return "Esc"; | |
| 42 | + | default: | |
| 43 | + | // Capitalize single letters, leave symbols as-is | |
| 44 | + | return part.length === 1 && /[a-z]/i.test(part) | |
| 45 | + | ? part.toUpperCase() | |
| 46 | + | : part; | |
| 47 | + | } | |
| 48 | + | }); | |
| 49 | + | ||
| 50 | + | return formattedParts.join(" + "); | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | /** | |||
| 2 | + | * Format a keybinding string for display. | |||
| 3 | + | * Converts internal key names to user-friendly display format. | |||
| 4 | + | * | |||
| 5 | + | * Examples: | |||
| 6 | + | * "ctrl+shift+a" -> "Ctrl + Shift + A" | |||
| 7 | + | * "arrowleft" -> "Left" | |||
| 8 | + | * "alt+[" -> "Alt + [" | |||
| 9 | + | */ | |||
| 10 | + | export function formatKey(key: string): string { | |||
| 11 | + | // Split by '+' separator first, then format each part | |||
| 12 | + | // This avoids replacing '+' characters that are actual key names | |||
| 13 | + | const parts = key.split("+"); | |||
| 14 | + | ||||
| 15 | + | const formattedParts = parts.map((part) => { | |||
| 16 | + | const lower = part.toLowerCase(); | |||
| 17 | + | switch (lower) { | |||
| 18 | + | case "arrowleft": | |||
| 19 | + | return "Left"; | |||
| 20 | + | case "arrowright": | |||
| 21 | + | return "Right"; | |||
| 22 | + | case "arrowup": | |||
| 23 | + | return "Up"; | |||
| 24 | + | case "arrowdown": | |||
| 25 | + | return "Down"; | |||
| 26 | + | case "ctrl": | |||
| 27 | + | return "Ctrl"; | |||
| 28 | + | case "alt": | |||
| 29 | + | return "Alt"; | |||
| 30 | + | case "shift": | |||
| 31 | + | return "Shift"; | |||
| 32 | + | case "space": | |||
| 33 | + | return "Space"; | |||
| 34 | + | case "enter": | |||
| 35 | + | return "Enter"; | |||
| 36 | + | case "tab": | |||
| 37 | + | return "Tab"; | |||
| 38 | + | case "backspace": | |||
| 39 | + | return "Backspace"; | |||
| 40 | + | case "escape": | |||
| 41 | + | return "Esc"; | |||
| 42 | + | default: | |||
| 43 | + | // Capitalize single letters, leave symbols as-is | |||
| 44 | + | return part.length === 1 && /[a-z]/i.test(part) | |||
| 45 | + | ? part.toUpperCase() | |||
| 46 | + | : part; | |||
| 47 | + | } | |||
| 48 | + | }); | |||
| 49 | + | ||||
| 50 | + | return formattedParts.join(" + "); | |||
| 51 | + | } | |||
M webui/src/pages/config/KeyCaptureModal.svelte +1 -22
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | import { Button } from "$lib/components/ui/button"; | |
| 4 | 4 | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | |
| 5 | 5 | import { Keyboard, X, Plus } from "@lucide/svelte"; | |
| 6 | + | import { formatKey } from "$lib/utils/keys"; | |
| 6 | 7 | ||
| 7 | 8 | type KeybindingEntry = { | |
| 8 | 9 | action: string; | |
| @@ -29,10 +30,6 @@ | |||
| 29 | 30 | let capturedKey = $state<string | null>(null); | |
| 30 | 31 | let isSaving = $state(false); | |
| 31 | 32 | ||
| 32 | - | $effect(() => { | |
| 33 | - | console.log("[KeyCaptureModal] State - newKeys:", newKeys, "capturedKey:", capturedKey); | |
| 34 | - | }); | |
| 35 | - | ||
| 36 | 33 | function handleKeyDown(e: KeyboardEvent) { | |
| 37 | 34 | if (!isCapturing) return; | |
| 38 | 35 | e.preventDefault(); | |
| @@ -86,12 +83,8 @@ | |||
| 86 | 83 | } | |
| 87 | 84 | ||
| 88 | 85 | function addCapturedKey() { | |
| 89 | - | console.log("[KeyCaptureModal] addCapturedKey called, capturedKey:", capturedKey, "newKeys before:", newKeys); | |
| 90 | 86 | if (capturedKey && !newKeys.includes(capturedKey)) { | |
| 91 | 87 | newKeys = [...newKeys, capturedKey]; | |
| 92 | - | console.log("[KeyCaptureModal] Added key, newKeys after:", newKeys); | |
| 93 | - | } else { | |
| 94 | - | console.log("[KeyCaptureModal] Key not added (null or duplicate)"); | |
| 95 | 88 | } | |
| 96 | 89 | capturedKey = null; | |
| 97 | 90 | } | |
| @@ -100,18 +93,6 @@ | |||
| 100 | 93 | newKeys = newKeys.filter((_, i) => i !== index); | |
| 101 | 94 | } | |
| 102 | 95 | ||
| 103 | - | function formatKey(key: string): string { | |
| 104 | - | return key | |
| 105 | - | .replace(/arrowleft/gi, "Left") | |
| 106 | - | .replace(/arrowright/gi, "Right") | |
| 107 | - | .replace(/arrowup/gi, "Up") | |
| 108 | - | .replace(/arrowdown/gi, "Down") | |
| 109 | - | .replace(/\+/g, " + ") | |
| 110 | - | .replace(/ctrl/gi, "Ctrl") | |
| 111 | - | .replace(/alt/gi, "Alt") | |
| 112 | - | .replace(/shift/gi, "Shift"); | |
| 113 | - | } | |
| 114 | - | ||
| 115 | 96 | onMount(() => { | |
| 116 | 97 | const handler = (e: KeyboardEvent) => handleKeyDown(e); | |
| 117 | 98 | window.addEventListener("keydown", handler, true); | |
| @@ -197,9 +178,7 @@ | |||
| 197 | 178 | let keysToSave = [...newKeys]; | |
| 198 | 179 | if (capturedKey && !keysToSave.includes(capturedKey)) { | |
| 199 | 180 | keysToSave.push(capturedKey); | |
| 200 | - | console.log("[KeyCaptureModal] Auto-added pending captured key:", capturedKey); | |
| 201 | 181 | } | |
| 202 | - | console.log("[KeyCaptureModal] Save clicked, keysToSave:", keysToSave); | |
| 203 | 182 | isSaving = true; | |
| 204 | 183 | onSave(keysToSave); | |
| 205 | 184 | }}>Save</AlertDialog.Action> | |
| @@ -3,6 +3,7 @@ | |||||
| 3 | import { Button } from "$lib/components/ui/button"; | 3 | import { Button } from "$lib/components/ui/button"; | ||
| 4 | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | 4 | import * as AlertDialog from "$lib/components/ui/alert-dialog"; | ||
| 5 | import { Keyboard, X, Plus } from "@lucide/svelte"; | 5 | import { Keyboard, X, Plus } from "@lucide/svelte"; | ||
| 6 | + | import { formatKey } from "$lib/utils/keys"; | |||
| 6 | 7 | ||||
| 7 | type KeybindingEntry = { | 8 | type KeybindingEntry = { | ||
| 8 | action: string; | 9 | action: string; | ||
| @@ -29,10 +30,6 @@ | |||||
| 29 | let capturedKey = $state<string | null>(null); | 30 | let capturedKey = $state<string | null>(null); | ||
| 30 | let isSaving = $state(false); | 31 | let isSaving = $state(false); | ||
| 31 | 32 | ||||
| 32 | - | $effect(() => { | |||
| 33 | - | console.log("[KeyCaptureModal] State - newKeys:", newKeys, "capturedKey:", capturedKey); | |||
| 34 | - | }); | |||
| 35 | - | ||||
| 36 | function handleKeyDown(e: KeyboardEvent) { | 33 | function handleKeyDown(e: KeyboardEvent) { | ||
| 37 | if (!isCapturing) return; | 34 | if (!isCapturing) return; | ||
| 38 | e.preventDefault(); | 35 | e.preventDefault(); | ||
| @@ -86,12 +83,8 @@ | |||||
| 86 | } | 83 | } | ||
| 87 | 84 | ||||
| 88 | function addCapturedKey() { | 85 | function addCapturedKey() { | ||
| 89 | - | console.log("[KeyCaptureModal] addCapturedKey called, capturedKey:", capturedKey, "newKeys before:", newKeys); | |||
| 90 | if (capturedKey && !newKeys.includes(capturedKey)) { | 86 | if (capturedKey && !newKeys.includes(capturedKey)) { | ||
| 91 | newKeys = [...newKeys, capturedKey]; | 87 | newKeys = [...newKeys, capturedKey]; | ||
| 92 | - | console.log("[KeyCaptureModal] Added key, newKeys after:", newKeys); | |||
| 93 | - | } else { | |||
| 94 | - | console.log("[KeyCaptureModal] Key not added (null or duplicate)"); | |||
| 95 | } | 88 | } | ||
| 96 | capturedKey = null; | 89 | capturedKey = null; | ||
| 97 | } | 90 | } | ||
| @@ -100,18 +93,6 @@ | |||||
| 100 | newKeys = newKeys.filter((_, i) => i !== index); | 93 | newKeys = newKeys.filter((_, i) => i !== index); | ||
| 101 | } | 94 | } | ||
| 102 | 95 | ||||
| 103 | - | function formatKey(key: string): string { | |||
| 104 | - | return key | |||
| 105 | - | .replace(/arrowleft/gi, "Left") | |||
| 106 | - | .replace(/arrowright/gi, "Right") | |||
| 107 | - | .replace(/arrowup/gi, "Up") | |||
| 108 | - | .replace(/arrowdown/gi, "Down") | |||
| 109 | - | .replace(/\+/g, " + ") | |||
| 110 | - | .replace(/ctrl/gi, "Ctrl") | |||
| 111 | - | .replace(/alt/gi, "Alt") | |||
| 112 | - | .replace(/shift/gi, "Shift"); | |||
| 113 | - | } | |||
| 114 | - | ||||
| 115 | onMount(() => { | 96 | onMount(() => { | ||
| 116 | const handler = (e: KeyboardEvent) => handleKeyDown(e); | 97 | const handler = (e: KeyboardEvent) => handleKeyDown(e); | ||
| 117 | window.addEventListener("keydown", handler, true); | 98 | window.addEventListener("keydown", handler, true); | ||
| @@ -197,9 +178,7 @@ | |||||
| 197 | let keysToSave = [...newKeys]; | 178 | let keysToSave = [...newKeys]; | ||
| 198 | if (capturedKey && !keysToSave.includes(capturedKey)) { | 179 | if (capturedKey && !keysToSave.includes(capturedKey)) { | ||
| 199 | keysToSave.push(capturedKey); | 180 | keysToSave.push(capturedKey); | ||
| 200 | - | console.log("[KeyCaptureModal] Auto-added pending captured key:", capturedKey); | |||
| 201 | } | 181 | } | ||
| 202 | - | console.log("[KeyCaptureModal] Save clicked, keysToSave:", keysToSave); | |||
| 203 | isSaving = true; | 182 | isSaving = true; | ||
| 204 | onSave(keysToSave); | 183 | onSave(keysToSave); | ||
| 205 | }}>Save</AlertDialog.Action> | 184 | }}>Save</AlertDialog.Action> | ||
M webui/src/pages/config/KeybindingGroup.svelte +1 -12
| @@ -2,6 +2,7 @@ | |||
| 2 | 2 | import { Button } from "$lib/components/ui/button"; | |
| 3 | 3 | import * as Table from "$lib/components/ui/table"; | |
| 4 | 4 | import { RotateCcw, Edit3 } from "@lucide/svelte"; | |
| 5 | + | import { formatKey } from "$lib/utils/keys"; | |
| 5 | 6 | ||
| 6 | 7 | type KeybindingEntry = { | |
| 7 | 8 | action: string; | |
| @@ -25,18 +26,6 @@ | |||
| 25 | 26 | } | |
| 26 | 27 | ||
| 27 | 28 | let { group, onEdit, onReset }: Props = $props(); | |
| 28 | - | ||
| 29 | - | function formatKey(key: string): string { | |
| 30 | - | return key | |
| 31 | - | .replace(/arrowleft/gi, "Left") | |
| 32 | - | .replace(/arrowright/gi, "Right") | |
| 33 | - | .replace(/arrowup/gi, "Up") | |
| 34 | - | .replace(/arrowdown/gi, "Down") | |
| 35 | - | .replace(/\+/g, " + ") | |
| 36 | - | .replace(/ctrl/gi, "Ctrl") | |
| 37 | - | .replace(/alt/gi, "Alt") | |
| 38 | - | .replace(/shift/gi, "Shift"); | |
| 39 | - | } | |
| 40 | 29 | </script> | |
| 41 | 30 | ||
| 42 | 31 | <div class="space-y-3"> | |
| @@ -2,6 +2,7 @@ | |||||
| 2 | import { Button } from "$lib/components/ui/button"; | 2 | import { Button } from "$lib/components/ui/button"; | ||
| 3 | import * as Table from "$lib/components/ui/table"; | 3 | import * as Table from "$lib/components/ui/table"; | ||
| 4 | import { RotateCcw, Edit3 } from "@lucide/svelte"; | 4 | import { RotateCcw, Edit3 } from "@lucide/svelte"; | ||
| 5 | + | import { formatKey } from "$lib/utils/keys"; | |||
| 5 | 6 | ||||
| 6 | type KeybindingEntry = { | 7 | type KeybindingEntry = { | ||
| 7 | action: string; | 8 | action: string; | ||
| @@ -25,18 +26,6 @@ | |||||
| 25 | } | 26 | } | ||
| 26 | 27 | ||||
| 27 | let { group, onEdit, onReset }: Props = $props(); | 28 | let { group, onEdit, onReset }: Props = $props(); | ||
| 28 | - | ||||
| 29 | - | function formatKey(key: string): string { | |||
| 30 | - | return key | |||
| 31 | - | .replace(/arrowleft/gi, "Left") | |||
| 32 | - | .replace(/arrowright/gi, "Right") | |||
| 33 | - | .replace(/arrowup/gi, "Up") | |||
| 34 | - | .replace(/arrowdown/gi, "Down") | |||
| 35 | - | .replace(/\+/g, " + ") | |||
| 36 | - | .replace(/ctrl/gi, "Ctrl") | |||
| 37 | - | .replace(/alt/gi, "Alt") | |||
| 38 | - | .replace(/shift/gi, "Shift"); | |||
| 39 | - | } | |||
| 40 | </script> | 29 | </script> | ||
| 41 | 30 | ||||
| 42 | <div class="space-y-3"> | 31 | <div class="space-y-3"> | ||
M webui/src/pages/config/KeybindingsTab.svelte +7 -14
| @@ -93,11 +93,9 @@ | |||
| 93 | 93 | } | |
| 94 | 94 | ||
| 95 | 95 | (window as any).__dumber_keybinding_set = (response: { status: string; conflicts?: KeybindingConflict[] }) => { | |
| 96 | - | console.log("[KeybindingsTab] Received set_keybinding response:", response); | |
| 97 | 96 | // Verify we got a valid success response from backend | |
| 98 | 97 | if (!response || response.status !== "success") { | |
| 99 | 98 | error = "Failed to save keybinding: invalid response"; | |
| 100 | - | console.error("[KeybindingsTab] Invalid response:", response); | |
| 101 | 99 | return; | |
| 102 | 100 | } | |
| 103 | 101 | if (response.conflicts && response.conflicts.length > 0) { | |
| @@ -109,18 +107,11 @@ | |||
| 109 | 107 | editingBinding = null; | |
| 110 | 108 | showSuccess("Keybinding saved successfully"); | |
| 111 | 109 | }; | |
| 112 | - | (window as any).__dumber_keybinding_error = (msg: string) => { | |
| 113 | - | console.error("[KeybindingsTab] set_keybinding error:", msg); | |
| 110 | + | (window as any).__dumber_keybinding_set_error = (msg: string) => { | |
| 114 | 111 | error = typeof msg === "string" ? msg : "Failed to save keybinding"; | |
| 115 | 112 | }; | |
| 116 | 113 | ||
| 117 | 114 | try { | |
| 118 | - | console.log("[KeybindingsTab] Sending set_keybinding:", { | |
| 119 | - | mode: editingBinding.mode, | |
| 120 | - | action: editingBinding.action, | |
| 121 | - | keys: keys, | |
| 122 | - | }); | |
| 123 | - | // postMessage handles Svelte 5 Proxy serialization automatically | |
| 124 | 115 | postMessage({ | |
| 125 | 116 | type: "set_keybinding", | |
| 126 | 117 | webviewId: getWebViewId(), | |
| @@ -130,9 +121,7 @@ | |||
| 130 | 121 | keys: keys, | |
| 131 | 122 | }, | |
| 132 | 123 | }); | |
| 133 | - | console.log("[KeybindingsTab] Message posted, waiting for callback..."); | |
| 134 | 124 | } catch (err) { | |
| 135 | - | console.error("[KeybindingsTab] postMessage failed:", err); | |
| 136 | 125 | error = err instanceof Error ? err.message : "Failed to save keybinding"; | |
| 137 | 126 | } | |
| 138 | 127 | } | |
| @@ -141,9 +130,11 @@ | |||
| 141 | 130 | if (!getWebKitBridge()) return; | |
| 142 | 131 | ||
| 143 | 132 | (window as any).__dumber_keybinding_reset = () => { | |
| 133 | + | conflicts = []; // Clear conflicts after reset | |
| 144 | 134 | loadKeybindings(); | |
| 135 | + | showSuccess("Keybinding reset to default"); | |
| 145 | 136 | }; | |
| 146 | - | (window as any).__dumber_keybinding_error = (msg: string) => { | |
| 137 | + | (window as any).__dumber_keybinding_reset_error = (msg: string) => { | |
| 147 | 138 | error = typeof msg === "string" ? msg : "Failed to reset keybinding"; | |
| 148 | 139 | }; | |
| 149 | 140 | ||
| @@ -162,10 +153,12 @@ | |||
| 162 | 153 | if (!getWebKitBridge()) return; | |
| 163 | 154 | ||
| 164 | 155 | (window as any).__dumber_keybindings_reset_all = () => { | |
| 156 | + | conflicts = []; // Clear conflicts after reset all | |
| 165 | 157 | loadKeybindings(); | |
| 166 | 158 | resetAllDialogOpen = false; | |
| 159 | + | showSuccess("All keybindings reset to defaults"); | |
| 167 | 160 | }; | |
| 168 | - | (window as any).__dumber_keybinding_error = (msg: string) => { | |
| 161 | + | (window as any).__dumber_keybindings_reset_all_error = (msg: string) => { | |
| 169 | 162 | error = typeof msg === "string" ? msg : "Failed to reset keybindings"; | |
| 170 | 163 | }; | |
| 171 | 164 | ||
| @@ -93,11 +93,9 @@ | |||||
| 93 | } | 93 | } | ||
| 94 | 94 | ||||
| 95 | (window as any).__dumber_keybinding_set = (response: { status: string; conflicts?: KeybindingConflict[] }) => { | 95 | (window as any).__dumber_keybinding_set = (response: { status: string; conflicts?: KeybindingConflict[] }) => { | ||
| 96 | - | console.log("[KeybindingsTab] Received set_keybinding response:", response); | |||
| 97 | // Verify we got a valid success response from backend | 96 | // Verify we got a valid success response from backend | ||
| 98 | if (!response || response.status !== "success") { | 97 | if (!response || response.status !== "success") { | ||
| 99 | error = "Failed to save keybinding: invalid response"; | 98 | error = "Failed to save keybinding: invalid response"; | ||
| 100 | - | console.error("[KeybindingsTab] Invalid response:", response); | |||
| 101 | return; | 99 | return; | ||
| 102 | } | 100 | } | ||
| 103 | if (response.conflicts && response.conflicts.length > 0) { | 101 | if (response.conflicts && response.conflicts.length > 0) { | ||
| @@ -109,18 +107,11 @@ | |||||
| 109 | editingBinding = null; | 107 | editingBinding = null; | ||
| 110 | showSuccess("Keybinding saved successfully"); | 108 | showSuccess("Keybinding saved successfully"); | ||
| 111 | }; | 109 | }; | ||
| 112 | - | (window as any).__dumber_keybinding_error = (msg: string) => { | 110 | + | (window as any).__dumber_keybinding_set_error = (msg: string) => { |
| 113 | - | console.error("[KeybindingsTab] set_keybinding error:", msg); | |||
| 114 | error = typeof msg === "string" ? msg : "Failed to save keybinding"; | 111 | error = typeof msg === "string" ? msg : "Failed to save keybinding"; | ||
| 115 | }; | 112 | }; | ||
| 116 | 113 | ||||
| 117 | try { | 114 | try { | ||
| 118 | - | console.log("[KeybindingsTab] Sending set_keybinding:", { | |||
| 119 | - | mode: editingBinding.mode, | |||
| 120 | - | action: editingBinding.action, | |||
| 121 | - | keys: keys, | |||
| 122 | - | }); | |||
| 123 | - | // postMessage handles Svelte 5 Proxy serialization automatically | |||
| 124 | postMessage({ | 115 | postMessage({ | ||
| 125 | type: "set_keybinding", | 116 | type: "set_keybinding", | ||
| 126 | webviewId: getWebViewId(), | 117 | webviewId: getWebViewId(), | ||
| @@ -130,9 +121,7 @@ | |||||
| 130 | keys: keys, | 121 | keys: keys, | ||
| 131 | }, | 122 | }, | ||
| 132 | }); | 123 | }); | ||
| 133 | - | console.log("[KeybindingsTab] Message posted, waiting for callback..."); | |||
| 134 | } catch (err) { | 124 | } catch (err) { | ||
| 135 | - | console.error("[KeybindingsTab] postMessage failed:", err); | |||
| 136 | error = err instanceof Error ? err.message : "Failed to save keybinding"; | 125 | error = err instanceof Error ? err.message : "Failed to save keybinding"; | ||
| 137 | } | 126 | } | ||
| 138 | } | 127 | } | ||
| @@ -141,9 +130,11 @@ | |||||
| 141 | if (!getWebKitBridge()) return; | 130 | if (!getWebKitBridge()) return; | ||
| 142 | 131 | ||||
| 143 | (window as any).__dumber_keybinding_reset = () => { | 132 | (window as any).__dumber_keybinding_reset = () => { | ||
| 133 | + | conflicts = []; // Clear conflicts after reset | |||
| 144 | loadKeybindings(); | 134 | loadKeybindings(); | ||
| 135 | + | showSuccess("Keybinding reset to default"); | |||
| 145 | }; | 136 | }; | ||
| 146 | - | (window as any).__dumber_keybinding_error = (msg: string) => { | 137 | + | (window as any).__dumber_keybinding_reset_error = (msg: string) => { |
| 147 | error = typeof msg === "string" ? msg : "Failed to reset keybinding"; | 138 | error = typeof msg === "string" ? msg : "Failed to reset keybinding"; | ||
| 148 | }; | 139 | }; | ||
| 149 | 140 | ||||
| @@ -162,10 +153,12 @@ | |||||
| 162 | if (!getWebKitBridge()) return; | 153 | if (!getWebKitBridge()) return; | ||
| 163 | 154 | ||||
| 164 | (window as any).__dumber_keybindings_reset_all = () => { | 155 | (window as any).__dumber_keybindings_reset_all = () => { | ||
| 156 | + | conflicts = []; // Clear conflicts after reset all | |||
| 165 | loadKeybindings(); | 157 | loadKeybindings(); | ||
| 166 | resetAllDialogOpen = false; | 158 | resetAllDialogOpen = false; | ||
| 159 | + | showSuccess("All keybindings reset to defaults"); | |||
| 167 | }; | 160 | }; | ||
| 168 | - | (window as any).__dumber_keybinding_error = (msg: string) => { | 161 | + | (window as any).__dumber_keybindings_reset_all_error = (msg: string) => { |
| 169 | error = typeof msg === "string" ? msg : "Failed to reset keybindings"; | 162 | error = typeof msg === "string" ? msg : "Failed to reset keybindings"; | ||
| 170 | }; | 163 | }; | ||
| 171 | 164 | ||||