refactor(config): use ActionBinding for all keybinding modes
- commit
- d701001ed53869b49614ce7c7129438541f09919
- author
- bnema · · 2026-01-11 14:21 UTC
- committer
- brice · · 2026-01-24 06:22 UTC
- parent
- 7ec6282
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +186 -171
M internal/infrastructure/config/defaults.go +53 -52
| @@ -161,40 +161,40 @@ func DefaultConfig() *Config { | |||
| 161 | 161 | PaneMode: PaneModeConfig{ | |
| 162 | 162 | ActivationShortcut: defaultPaneActivationShortcut, | |
| 163 | 163 | TimeoutMilliseconds: defaultPaneTimeoutMilliseconds, | |
| 164 | - | Actions: map[string][]string{ | |
| 165 | - | "split-right": {"arrowright", "r"}, | |
| 166 | - | "split-left": {"arrowleft", "l"}, | |
| 167 | - | "split-up": {"arrowup", "u"}, | |
| 168 | - | "split-down": {"arrowdown", "d"}, | |
| 169 | - | "stack-pane": {"s"}, | |
| 170 | - | "close-pane": {"x"}, | |
| 171 | - | "move-pane-to-tab": {"m"}, | |
| 172 | - | "move-pane-to-next-tab": {"M", "shift+m"}, | |
| 164 | + | Actions: map[string]ActionBinding{ | |
| 165 | + | "split-right": {Keys: []string{"arrowright", "r"}, Desc: "Split pane to the right"}, | |
| 166 | + | "split-left": {Keys: []string{"arrowleft", "l"}, Desc: "Split pane to the left"}, | |
| 167 | + | "split-up": {Keys: []string{"arrowup", "u"}, Desc: "Split pane upward"}, | |
| 168 | + | "split-down": {Keys: []string{"arrowdown", "d"}, Desc: "Split pane downward"}, | |
| 169 | + | "stack-pane": {Keys: []string{"s"}, Desc: "Stack pane with sibling"}, | |
| 170 | + | "close-pane": {Keys: []string{"x"}, Desc: "Close current pane"}, | |
| 171 | + | "move-pane-to-tab": {Keys: []string{"m"}, Desc: "Move pane to different tab"}, | |
| 172 | + | "move-pane-to-next-tab": {Keys: []string{"M", "shift+m"}, Desc: "Move pane to next tab"}, | |
| 173 | 173 | ||
| 174 | - | "consume-or-expel-left": {"bracketleft", "["}, | |
| 175 | - | "consume-or-expel-right": {"bracketright", "]"}, | |
| 176 | - | "consume-or-expel-up": {"shift+bracketleft", "braceleft", "{"}, | |
| 177 | - | "consume-or-expel-down": {"shift+bracketright", "braceright", "}"}, | |
| 174 | + | "consume-or-expel-left": {Keys: []string{"["}, Desc: "Consume/expel pane left"}, | |
| 175 | + | "consume-or-expel-right": {Keys: []string{"]"}, Desc: "Consume/expel pane right"}, | |
| 176 | + | "consume-or-expel-up": {Keys: []string{"{"}, Desc: "Consume/expel pane up"}, | |
| 177 | + | "consume-or-expel-down": {Keys: []string{"}"}, Desc: "Consume/expel pane down"}, | |
| 178 | 178 | ||
| 179 | - | "focus-right": {"shift+arrowright", "shift+l"}, | |
| 180 | - | "focus-left": {"shift+arrowleft", "shift+h"}, | |
| 181 | - | "focus-up": {"shift+arrowup", "shift+k"}, | |
| 182 | - | "focus-down": {"shift+arrowdown", "shift+j"}, | |
| 183 | - | "confirm": {"enter"}, | |
| 184 | - | "cancel": {"escape"}, | |
| 179 | + | "focus-right": {Keys: []string{"shift+arrowright", "shift+l"}, Desc: "Focus pane to the right"}, | |
| 180 | + | "focus-left": {Keys: []string{"shift+arrowleft", "shift+h"}, Desc: "Focus pane to the left"}, | |
| 181 | + | "focus-up": {Keys: []string{"shift+arrowup", "shift+k"}, Desc: "Focus pane above"}, | |
| 182 | + | "focus-down": {Keys: []string{"shift+arrowdown", "shift+j"}, Desc: "Focus pane below"}, | |
| 183 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, | |
| 184 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, | |
| 185 | 185 | }, | |
| 186 | 186 | }, | |
| 187 | 187 | TabMode: TabModeConfig{ | |
| 188 | 188 | ActivationShortcut: defaultTabActivationShortcut, | |
| 189 | 189 | TimeoutMilliseconds: defaultTabTimeoutMilliseconds, | |
| 190 | - | Actions: map[string][]string{ | |
| 191 | - | "new-tab": {"n", "c"}, | |
| 192 | - | "close-tab": {"x"}, | |
| 193 | - | "next-tab": {"l", "tab"}, | |
| 194 | - | "previous-tab": {"h", "shift+tab"}, | |
| 195 | - | "rename-tab": {"r"}, | |
| 196 | - | "confirm": {"enter"}, | |
| 197 | - | "cancel": {"escape"}, | |
| 190 | + | Actions: map[string]ActionBinding{ | |
| 191 | + | "new-tab": {Keys: []string{"n", "c"}, Desc: "Create new tab"}, | |
| 192 | + | "close-tab": {Keys: []string{"x"}, Desc: "Close current tab"}, | |
| 193 | + | "next-tab": {Keys: []string{"l", "tab"}, Desc: "Switch to next tab"}, | |
| 194 | + | "previous-tab": {Keys: []string{"h", "shift+tab"}, Desc: "Switch to previous tab"}, | |
| 195 | + | "rename-tab": {Keys: []string{"r"}, Desc: "Rename current tab"}, | |
| 196 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, | |
| 197 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, | |
| 198 | 198 | }, | |
| 199 | 199 | }, | |
| 200 | 200 | ResizeMode: ResizeModeConfig{ | |
| @@ -202,30 +202,31 @@ func DefaultConfig() *Config { | |||
| 202 | 202 | TimeoutMilliseconds: defaultResizeTimeoutMilliseconds, | |
| 203 | 203 | StepPercent: defaultResizeStepPercent, | |
| 204 | 204 | MinPanePercent: defaultResizeMinPanePercent, | |
| 205 | - | Actions: map[string][]string{ | |
| 206 | - | "resize-increase-left": {"h", "arrowleft"}, | |
| 207 | - | "resize-increase-down": {"j", "arrowdown"}, | |
| 208 | - | "resize-increase-up": {"k", "arrowup"}, | |
| 209 | - | "resize-increase-right": {"l", "arrowright"}, | |
| 210 | - | "resize-decrease-left": {"H"}, | |
| 211 | - | "resize-decrease-down": {"J"}, | |
| 212 | - | "resize-decrease-up": {"K"}, | |
| 213 | - | "resize-decrease-right": {"L"}, | |
| 214 | - | "resize-increase": {"+", "="}, | |
| 215 | - | "resize-decrease": {"-"}, | |
| 216 | - | "confirm": {"enter"}, | |
| 217 | - | "cancel": {"escape"}, | |
| 205 | + | Actions: map[string]ActionBinding{ | |
| 206 | + | "resize-increase-left": {Keys: []string{"h", "arrowleft"}, Desc: "Increase size leftward"}, | |
| 207 | + | "resize-increase-down": {Keys: []string{"j", "arrowdown"}, Desc: "Increase size downward"}, | |
| 208 | + | "resize-increase-up": {Keys: []string{"k", "arrowup"}, Desc: "Increase size upward"}, | |
| 209 | + | "resize-increase-right": {Keys: []string{"l", "arrowright"}, Desc: "Increase size rightward"}, | |
| 210 | + | "resize-decrease-left": {Keys: []string{"H"}, Desc: "Decrease size leftward"}, | |
| 211 | + | "resize-decrease-down": {Keys: []string{"J"}, Desc: "Decrease size downward"}, | |
| 212 | + | "resize-decrease-up": {Keys: []string{"K"}, Desc: "Decrease size upward"}, | |
| 213 | + | "resize-decrease-right": {Keys: []string{"L"}, Desc: "Decrease size rightward"}, | |
| 214 | + | "resize-increase": {Keys: []string{"+", "="}, Desc: "Increase pane size"}, | |
| 215 | + | "resize-decrease": {Keys: []string{"-"}, Desc: "Decrease pane size"}, | |
| 216 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, | |
| 217 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, | |
| 218 | 218 | }, | |
| 219 | 219 | }, | |
| 220 | 220 | Shortcuts: GlobalShortcutsConfig{ | |
| 221 | - | ClosePane: "ctrl+w", | |
| 222 | - | NextTab: "ctrl+tab", | |
| 223 | - | PreviousTab: "ctrl+shift+tab", | |
| 224 | - | ||
| 225 | - | ConsumeOrExpelLeft: "alt+bracketleft", | |
| 226 | - | ConsumeOrExpelRight: "alt+bracketright", | |
| 227 | - | ConsumeOrExpelUp: "alt+shift+bracketleft", | |
| 228 | - | ConsumeOrExpelDown: "alt+shift+bracketright", | |
| 221 | + | Actions: map[string]ActionBinding{ | |
| 222 | + | "close_pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"}, | |
| 223 | + | "next_tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"}, | |
| 224 | + | "previous_tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"}, | |
| 225 | + | "consume_or_expel_left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"}, | |
| 226 | + | "consume_or_expel_right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"}, | |
| 227 | + | "consume_or_expel_up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"}, | |
| 228 | + | "consume_or_expel_down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"}, | |
| 229 | + | }, | |
| 229 | 230 | }, | |
| 230 | 231 | TabBarPosition: defaultTabBarPosition, | |
| 231 | 232 | HideTabBarWhenSingleTab: true, | |
| @@ -269,10 +270,10 @@ func DefaultConfig() *Config { | |||
| 269 | 270 | SessionMode: SessionModeConfig{ | |
| 270 | 271 | ActivationShortcut: defaultSessionActivationShortcut, | |
| 271 | 272 | TimeoutMilliseconds: defaultSessionTimeoutMilliseconds, | |
| 272 | - | Actions: map[string][]string{ | |
| 273 | - | "session-manager": {"s", "w"}, | |
| 274 | - | "confirm": {"enter"}, | |
| 275 | - | "cancel": {"escape"}, | |
| 273 | + | Actions: map[string]ActionBinding{ | |
| 274 | + | "session-manager": {Keys: []string{"s", "w"}, Desc: "Open session manager"}, | |
| 275 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, | |
| 276 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, | |
| 276 | 277 | }, | |
| 277 | 278 | }, | |
| 278 | 279 | }, | |
| @@ -161,40 +161,40 @@ func DefaultConfig() *Config { | |||||
| 161 | PaneMode: PaneModeConfig{ | 161 | PaneMode: PaneModeConfig{ | ||
| 162 | ActivationShortcut: defaultPaneActivationShortcut, | 162 | ActivationShortcut: defaultPaneActivationShortcut, | ||
| 163 | TimeoutMilliseconds: defaultPaneTimeoutMilliseconds, | 163 | TimeoutMilliseconds: defaultPaneTimeoutMilliseconds, | ||
| 164 | - | Actions: map[string][]string{ | 164 | + | Actions: map[string]ActionBinding{ |
| 165 | - | "split-right": {"arrowright", "r"}, | 165 | + | "split-right": {Keys: []string{"arrowright", "r"}, Desc: "Split pane to the right"}, |
| 166 | - | "split-left": {"arrowleft", "l"}, | 166 | + | "split-left": {Keys: []string{"arrowleft", "l"}, Desc: "Split pane to the left"}, |
| 167 | - | "split-up": {"arrowup", "u"}, | 167 | + | "split-up": {Keys: []string{"arrowup", "u"}, Desc: "Split pane upward"}, |
| 168 | - | "split-down": {"arrowdown", "d"}, | 168 | + | "split-down": {Keys: []string{"arrowdown", "d"}, Desc: "Split pane downward"}, |
| 169 | - | "stack-pane": {"s"}, | 169 | + | "stack-pane": {Keys: []string{"s"}, Desc: "Stack pane with sibling"}, |
| 170 | - | "close-pane": {"x"}, | 170 | + | "close-pane": {Keys: []string{"x"}, Desc: "Close current pane"}, |
| 171 | - | "move-pane-to-tab": {"m"}, | 171 | + | "move-pane-to-tab": {Keys: []string{"m"}, Desc: "Move pane to different tab"}, |
| 172 | - | "move-pane-to-next-tab": {"M", "shift+m"}, | 172 | + | "move-pane-to-next-tab": {Keys: []string{"M", "shift+m"}, Desc: "Move pane to next tab"}, |
| 173 | 173 | ||||
| 174 | - | "consume-or-expel-left": {"bracketleft", "["}, | 174 | + | "consume-or-expel-left": {Keys: []string{"["}, Desc: "Consume/expel pane left"}, |
| 175 | - | "consume-or-expel-right": {"bracketright", "]"}, | 175 | + | "consume-or-expel-right": {Keys: []string{"]"}, Desc: "Consume/expel pane right"}, |
| 176 | - | "consume-or-expel-up": {"shift+bracketleft", "braceleft", "{"}, | 176 | + | "consume-or-expel-up": {Keys: []string{"{"}, Desc: "Consume/expel pane up"}, |
| 177 | - | "consume-or-expel-down": {"shift+bracketright", "braceright", "}"}, | 177 | + | "consume-or-expel-down": {Keys: []string{"}"}, Desc: "Consume/expel pane down"}, |
| 178 | 178 | ||||
| 179 | - | "focus-right": {"shift+arrowright", "shift+l"}, | 179 | + | "focus-right": {Keys: []string{"shift+arrowright", "shift+l"}, Desc: "Focus pane to the right"}, |
| 180 | - | "focus-left": {"shift+arrowleft", "shift+h"}, | 180 | + | "focus-left": {Keys: []string{"shift+arrowleft", "shift+h"}, Desc: "Focus pane to the left"}, |
| 181 | - | "focus-up": {"shift+arrowup", "shift+k"}, | 181 | + | "focus-up": {Keys: []string{"shift+arrowup", "shift+k"}, Desc: "Focus pane above"}, |
| 182 | - | "focus-down": {"shift+arrowdown", "shift+j"}, | 182 | + | "focus-down": {Keys: []string{"shift+arrowdown", "shift+j"}, Desc: "Focus pane below"}, |
| 183 | - | "confirm": {"enter"}, | 183 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, |
| 184 | - | "cancel": {"escape"}, | 184 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, |
| 185 | }, | 185 | }, | ||
| 186 | }, | 186 | }, | ||
| 187 | TabMode: TabModeConfig{ | 187 | TabMode: TabModeConfig{ | ||
| 188 | ActivationShortcut: defaultTabActivationShortcut, | 188 | ActivationShortcut: defaultTabActivationShortcut, | ||
| 189 | TimeoutMilliseconds: defaultTabTimeoutMilliseconds, | 189 | TimeoutMilliseconds: defaultTabTimeoutMilliseconds, | ||
| 190 | - | Actions: map[string][]string{ | 190 | + | Actions: map[string]ActionBinding{ |
| 191 | - | "new-tab": {"n", "c"}, | 191 | + | "new-tab": {Keys: []string{"n", "c"}, Desc: "Create new tab"}, |
| 192 | - | "close-tab": {"x"}, | 192 | + | "close-tab": {Keys: []string{"x"}, Desc: "Close current tab"}, |
| 193 | - | "next-tab": {"l", "tab"}, | 193 | + | "next-tab": {Keys: []string{"l", "tab"}, Desc: "Switch to next tab"}, |
| 194 | - | "previous-tab": {"h", "shift+tab"}, | 194 | + | "previous-tab": {Keys: []string{"h", "shift+tab"}, Desc: "Switch to previous tab"}, |
| 195 | - | "rename-tab": {"r"}, | 195 | + | "rename-tab": {Keys: []string{"r"}, Desc: "Rename current tab"}, |
| 196 | - | "confirm": {"enter"}, | 196 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, |
| 197 | - | "cancel": {"escape"}, | 197 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, |
| 198 | }, | 198 | }, | ||
| 199 | }, | 199 | }, | ||
| 200 | ResizeMode: ResizeModeConfig{ | 200 | ResizeMode: ResizeModeConfig{ | ||
| @@ -202,30 +202,31 @@ func DefaultConfig() *Config { | |||||
| 202 | TimeoutMilliseconds: defaultResizeTimeoutMilliseconds, | 202 | TimeoutMilliseconds: defaultResizeTimeoutMilliseconds, | ||
| 203 | StepPercent: defaultResizeStepPercent, | 203 | StepPercent: defaultResizeStepPercent, | ||
| 204 | MinPanePercent: defaultResizeMinPanePercent, | 204 | MinPanePercent: defaultResizeMinPanePercent, | ||
| 205 | - | Actions: map[string][]string{ | 205 | + | Actions: map[string]ActionBinding{ |
| 206 | - | "resize-increase-left": {"h", "arrowleft"}, | 206 | + | "resize-increase-left": {Keys: []string{"h", "arrowleft"}, Desc: "Increase size leftward"}, |
| 207 | - | "resize-increase-down": {"j", "arrowdown"}, | 207 | + | "resize-increase-down": {Keys: []string{"j", "arrowdown"}, Desc: "Increase size downward"}, |
| 208 | - | "resize-increase-up": {"k", "arrowup"}, | 208 | + | "resize-increase-up": {Keys: []string{"k", "arrowup"}, Desc: "Increase size upward"}, |
| 209 | - | "resize-increase-right": {"l", "arrowright"}, | 209 | + | "resize-increase-right": {Keys: []string{"l", "arrowright"}, Desc: "Increase size rightward"}, |
| 210 | - | "resize-decrease-left": {"H"}, | 210 | + | "resize-decrease-left": {Keys: []string{"H"}, Desc: "Decrease size leftward"}, |
| 211 | - | "resize-decrease-down": {"J"}, | 211 | + | "resize-decrease-down": {Keys: []string{"J"}, Desc: "Decrease size downward"}, |
| 212 | - | "resize-decrease-up": {"K"}, | 212 | + | "resize-decrease-up": {Keys: []string{"K"}, Desc: "Decrease size upward"}, |
| 213 | - | "resize-decrease-right": {"L"}, | 213 | + | "resize-decrease-right": {Keys: []string{"L"}, Desc: "Decrease size rightward"}, |
| 214 | - | "resize-increase": {"+", "="}, | 214 | + | "resize-increase": {Keys: []string{"+", "="}, Desc: "Increase pane size"}, |
| 215 | - | "resize-decrease": {"-"}, | 215 | + | "resize-decrease": {Keys: []string{"-"}, Desc: "Decrease pane size"}, |
| 216 | - | "confirm": {"enter"}, | 216 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, |
| 217 | - | "cancel": {"escape"}, | 217 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, |
| 218 | }, | 218 | }, | ||
| 219 | }, | 219 | }, | ||
| 220 | Shortcuts: GlobalShortcutsConfig{ | 220 | Shortcuts: GlobalShortcutsConfig{ | ||
| 221 | - | ClosePane: "ctrl+w", | 221 | + | Actions: map[string]ActionBinding{ |
| 222 | - | NextTab: "ctrl+tab", | 222 | + | "close_pane": {Keys: []string{"ctrl+w"}, Desc: "Close active pane"}, |
| 223 | - | PreviousTab: "ctrl+shift+tab", | 223 | + | "next_tab": {Keys: []string{"ctrl+tab"}, Desc: "Switch to next tab"}, |
| 224 | - | 224 | + | "previous_tab": {Keys: []string{"ctrl+shift+tab"}, Desc: "Switch to previous tab"}, | |
| 225 | - | ConsumeOrExpelLeft: "alt+bracketleft", | 225 | + | "consume_or_expel_left": {Keys: []string{"alt+["}, Desc: "Consume/expel pane left"}, |
| 226 | - | ConsumeOrExpelRight: "alt+bracketright", | 226 | + | "consume_or_expel_right": {Keys: []string{"alt+]"}, Desc: "Consume/expel pane right"}, |
| 227 | - | ConsumeOrExpelUp: "alt+shift+bracketleft", | 227 | + | "consume_or_expel_up": {Keys: []string{"alt+{"}, Desc: "Consume/expel pane up"}, |
| 228 | - | ConsumeOrExpelDown: "alt+shift+bracketright", | 228 | + | "consume_or_expel_down": {Keys: []string{"alt+}"}, Desc: "Consume/expel pane down"}, |
| 229 | + | }, | |||
| 229 | }, | 230 | }, | ||
| 230 | TabBarPosition: defaultTabBarPosition, | 231 | TabBarPosition: defaultTabBarPosition, | ||
| 231 | HideTabBarWhenSingleTab: true, | 232 | HideTabBarWhenSingleTab: true, | ||
| @@ -269,10 +270,10 @@ func DefaultConfig() *Config { | |||||
| 269 | SessionMode: SessionModeConfig{ | 270 | SessionMode: SessionModeConfig{ | ||
| 270 | ActivationShortcut: defaultSessionActivationShortcut, | 271 | ActivationShortcut: defaultSessionActivationShortcut, | ||
| 271 | TimeoutMilliseconds: defaultSessionTimeoutMilliseconds, | 272 | TimeoutMilliseconds: defaultSessionTimeoutMilliseconds, | ||
| 272 | - | Actions: map[string][]string{ | 273 | + | Actions: map[string]ActionBinding{ |
| 273 | - | "session-manager": {"s", "w"}, | 274 | + | "session-manager": {Keys: []string{"s", "w"}, Desc: "Open session manager"}, |
| 274 | - | "confirm": {"enter"}, | 275 | + | "confirm": {Keys: []string{"enter"}, Desc: "Confirm action"}, |
| 275 | - | "cancel": {"escape"}, | 276 | + | "cancel": {Keys: []string{"escape"}, Desc: "Cancel/exit mode"}, |
| 276 | }, | 277 | }, | ||
| 277 | }, | 278 | }, | ||
| 278 | }, | 279 | }, | ||
M internal/infrastructure/config/loader.go +44 -7
| @@ -289,6 +289,49 @@ func (m *Manager) Save(cfg *Config) error { | |||
| 289 | 289 | return nil | |
| 290 | 290 | } | |
| 291 | 291 | ||
| 292 | + | // SaveKeybindings saves keybinding-related configuration to disk. | |
| 293 | + | func (m *Manager) SaveKeybindings(cfg *Config) error { | |
| 294 | + | m.mu.Lock() | |
| 295 | + | defer m.mu.Unlock() | |
| 296 | + | ||
| 297 | + | if cfg == nil { | |
| 298 | + | return fmt.Errorf("config is nil") | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | // Update Viper with keybinding values | |
| 302 | + | // Pane mode | |
| 303 | + | m.viper.Set("workspace.pane_mode.actions", cfg.Workspace.PaneMode.Actions) | |
| 304 | + | m.viper.Set("workspace.pane_mode.activation_shortcut", cfg.Workspace.PaneMode.ActivationShortcut) | |
| 305 | + | ||
| 306 | + | // Tab mode | |
| 307 | + | m.viper.Set("workspace.tab_mode.actions", cfg.Workspace.TabMode.Actions) | |
| 308 | + | m.viper.Set("workspace.tab_mode.activation_shortcut", cfg.Workspace.TabMode.ActivationShortcut) | |
| 309 | + | ||
| 310 | + | // Resize mode | |
| 311 | + | m.viper.Set("workspace.resize_mode.actions", cfg.Workspace.ResizeMode.Actions) | |
| 312 | + | m.viper.Set("workspace.resize_mode.activation_shortcut", cfg.Workspace.ResizeMode.ActivationShortcut) | |
| 313 | + | ||
| 314 | + | // Session mode | |
| 315 | + | m.viper.Set("session.session_mode.actions", cfg.Session.SessionMode.Actions) | |
| 316 | + | m.viper.Set("session.session_mode.activation_shortcut", cfg.Session.SessionMode.ActivationShortcut) | |
| 317 | + | ||
| 318 | + | // Global shortcuts | |
| 319 | + | m.viper.Set("workspace.shortcuts.actions", cfg.Workspace.Shortcuts.Actions) | |
| 320 | + | ||
| 321 | + | if err := m.viper.WriteConfig(); err != nil { | |
| 322 | + | return fmt.Errorf("failed to write config: %w", err) | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | // Reload if not watching | |
| 326 | + | if !m.watching { | |
| 327 | + | if err := m.reload(); err != nil { | |
| 328 | + | return err | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | return nil | |
| 333 | + | } | |
| 334 | + | ||
| 292 | 335 | // GetConfigFile returns the path to the configuration file being used. | |
| 293 | 336 | func (m *Manager) GetConfigFile() string { | |
| 294 | 337 | return m.viper.ConfigFileUsed() | |
| @@ -443,13 +486,7 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) { | |||
| 443 | 486 | m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent) | |
| 444 | 487 | m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent) | |
| 445 | 488 | m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions) | |
| 446 | - | m.viper.SetDefault("workspace.shortcuts.close_pane", defaults.Workspace.Shortcuts.ClosePane) | |
| 447 | - | m.viper.SetDefault("workspace.shortcuts.next_tab", defaults.Workspace.Shortcuts.NextTab) | |
| 448 | - | m.viper.SetDefault("workspace.shortcuts.previous_tab", defaults.Workspace.Shortcuts.PreviousTab) | |
| 449 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_left", defaults.Workspace.Shortcuts.ConsumeOrExpelLeft) | |
| 450 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_right", defaults.Workspace.Shortcuts.ConsumeOrExpelRight) | |
| 451 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_up", defaults.Workspace.Shortcuts.ConsumeOrExpelUp) | |
| 452 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_down", defaults.Workspace.Shortcuts.ConsumeOrExpelDown) | |
| 489 | + | m.viper.SetDefault("workspace.shortcuts.actions", defaults.Workspace.Shortcuts.Actions) | |
| 453 | 490 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | |
| 454 | 491 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | |
| 455 | 492 | m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove) | |
| @@ -289,6 +289,49 @@ func (m *Manager) Save(cfg *Config) error { | |||||
| 289 | return nil | 289 | return nil | ||
| 290 | } | 290 | } | ||
| 291 | 291 | ||||
| 292 | + | // SaveKeybindings saves keybinding-related configuration to disk. | |||
| 293 | + | func (m *Manager) SaveKeybindings(cfg *Config) error { | |||
| 294 | + | m.mu.Lock() | |||
| 295 | + | defer m.mu.Unlock() | |||
| 296 | + | ||||
| 297 | + | if cfg == nil { | |||
| 298 | + | return fmt.Errorf("config is nil") | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | // Update Viper with keybinding values | |||
| 302 | + | // Pane mode | |||
| 303 | + | m.viper.Set("workspace.pane_mode.actions", cfg.Workspace.PaneMode.Actions) | |||
| 304 | + | m.viper.Set("workspace.pane_mode.activation_shortcut", cfg.Workspace.PaneMode.ActivationShortcut) | |||
| 305 | + | ||||
| 306 | + | // Tab mode | |||
| 307 | + | m.viper.Set("workspace.tab_mode.actions", cfg.Workspace.TabMode.Actions) | |||
| 308 | + | m.viper.Set("workspace.tab_mode.activation_shortcut", cfg.Workspace.TabMode.ActivationShortcut) | |||
| 309 | + | ||||
| 310 | + | // Resize mode | |||
| 311 | + | m.viper.Set("workspace.resize_mode.actions", cfg.Workspace.ResizeMode.Actions) | |||
| 312 | + | m.viper.Set("workspace.resize_mode.activation_shortcut", cfg.Workspace.ResizeMode.ActivationShortcut) | |||
| 313 | + | ||||
| 314 | + | // Session mode | |||
| 315 | + | m.viper.Set("session.session_mode.actions", cfg.Session.SessionMode.Actions) | |||
| 316 | + | m.viper.Set("session.session_mode.activation_shortcut", cfg.Session.SessionMode.ActivationShortcut) | |||
| 317 | + | ||||
| 318 | + | // Global shortcuts | |||
| 319 | + | m.viper.Set("workspace.shortcuts.actions", cfg.Workspace.Shortcuts.Actions) | |||
| 320 | + | ||||
| 321 | + | if err := m.viper.WriteConfig(); err != nil { | |||
| 322 | + | return fmt.Errorf("failed to write config: %w", err) | |||
| 323 | + | } | |||
| 324 | + | ||||
| 325 | + | // Reload if not watching | |||
| 326 | + | if !m.watching { | |||
| 327 | + | if err := m.reload(); err != nil { | |||
| 328 | + | return err | |||
| 329 | + | } | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | return nil | |||
| 333 | + | } | |||
| 334 | + | ||||
| 292 | // GetConfigFile returns the path to the configuration file being used. | 335 | // GetConfigFile returns the path to the configuration file being used. | ||
| 293 | func (m *Manager) GetConfigFile() string { | 336 | func (m *Manager) GetConfigFile() string { | ||
| 294 | return m.viper.ConfigFileUsed() | 337 | return m.viper.ConfigFileUsed() | ||
| @@ -443,13 +486,7 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) { | |||||
| 443 | m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent) | 486 | m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent) | ||
| 444 | m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent) | 487 | m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent) | ||
| 445 | m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions) | 488 | m.viper.SetDefault("workspace.resize_mode.actions", defaults.Workspace.ResizeMode.Actions) | ||
| 446 | - | m.viper.SetDefault("workspace.shortcuts.close_pane", defaults.Workspace.Shortcuts.ClosePane) | 489 | + | m.viper.SetDefault("workspace.shortcuts.actions", defaults.Workspace.Shortcuts.Actions) |
| 447 | - | m.viper.SetDefault("workspace.shortcuts.next_tab", defaults.Workspace.Shortcuts.NextTab) | |||
| 448 | - | m.viper.SetDefault("workspace.shortcuts.previous_tab", defaults.Workspace.Shortcuts.PreviousTab) | |||
| 449 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_left", defaults.Workspace.Shortcuts.ConsumeOrExpelLeft) | |||
| 450 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_right", defaults.Workspace.Shortcuts.ConsumeOrExpelRight) | |||
| 451 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_up", defaults.Workspace.Shortcuts.ConsumeOrExpelUp) | |||
| 452 | - | m.viper.SetDefault("workspace.shortcuts.consume_or_expel_down", defaults.Workspace.Shortcuts.ConsumeOrExpelDown) | |||
| 453 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | 490 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | ||
| 454 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | 491 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | ||
| 455 | m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove) | 492 | m.viper.SetDefault("workspace.switch_to_tab_on_move", defaults.Workspace.SwitchToTabOnMove) | ||
M internal/infrastructure/config/schema.go +39 -29
| @@ -410,19 +410,25 @@ type WorkspaceConfig struct { | |||
| 410 | 410 | Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" toml:"styling" json:"styling"` | |
| 411 | 411 | } | |
| 412 | 412 | ||
| 413 | + | // ActionBinding defines a keybinding with optional description. | |
| 414 | + | type ActionBinding struct { | |
| 415 | + | Keys []string `mapstructure:"keys" yaml:"keys" toml:"keys" json:"keys"` | |
| 416 | + | Desc string `mapstructure:"desc" yaml:"desc" toml:"desc" json:"desc,omitempty"` | |
| 417 | + | } | |
| 418 | + | ||
| 413 | 419 | // PaneModeConfig defines modal behavior for pane management. | |
| 414 | 420 | type PaneModeConfig struct { | |
| 415 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 416 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 417 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 421 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 422 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 423 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 418 | 424 | } | |
| 419 | 425 | ||
| 420 | 426 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | |
| 421 | 427 | // This is built from the action→keys structure in the config. | |
| 422 | 428 | func (p *PaneModeConfig) GetKeyBindings() map[string]string { | |
| 423 | 429 | keyToAction := make(map[string]string) | |
| 424 | - | for action, keys := range p.Actions { | |
| 425 | - | for _, key := range keys { | |
| 430 | + | for action, binding := range p.Actions { | |
| 431 | + | for _, key := range binding.Keys { | |
| 426 | 432 | keyToAction[key] = action | |
| 427 | 433 | } | |
| 428 | 434 | } | |
| @@ -431,17 +437,17 @@ func (p *PaneModeConfig) GetKeyBindings() map[string]string { | |||
| 431 | 437 | ||
| 432 | 438 | // TabModeConfig defines modal behavior for tab management (Zellij-style). | |
| 433 | 439 | type TabModeConfig struct { | |
| 434 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 435 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 436 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 440 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 441 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 442 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 437 | 443 | } | |
| 438 | 444 | ||
| 439 | 445 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | |
| 440 | 446 | // This is built from the action→keys structure in the config. | |
| 441 | 447 | func (t *TabModeConfig) GetKeyBindings() map[string]string { | |
| 442 | 448 | keyToAction := make(map[string]string) | |
| 443 | - | for action, keys := range t.Actions { | |
| 444 | - | for _, key := range keys { | |
| 449 | + | for action, binding := range t.Actions { | |
| 450 | + | for _, key := range binding.Keys { | |
| 445 | 451 | keyToAction[key] = action | |
| 446 | 452 | } | |
| 447 | 453 | } | |
| @@ -450,19 +456,19 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string { | |||
| 450 | 456 | ||
| 451 | 457 | // ResizeModeConfig defines modal behavior for resizing panes (Zellij-style). | |
| 452 | 458 | type ResizeModeConfig struct { | |
| 453 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 454 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 455 | - | StepPercent float64 `mapstructure:"step_percent" yaml:"step_percent" toml:"step_percent" json:"step_percent"` | |
| 456 | - | MinPanePercent float64 `mapstructure:"min_pane_percent" yaml:"min_pane_percent" toml:"min_pane_percent" json:"min_pane_percent"` //nolint:lll // struct tags must stay on one line | |
| 457 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 459 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 460 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 461 | + | StepPercent float64 `mapstructure:"step_percent" yaml:"step_percent" toml:"step_percent" json:"step_percent"` | |
| 462 | + | MinPanePercent float64 `mapstructure:"min_pane_percent" yaml:"min_pane_percent" toml:"min_pane_percent" json:"min_pane_percent"` //nolint:lll // struct tags must stay on one line | |
| 463 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 458 | 464 | } | |
| 459 | 465 | ||
| 460 | 466 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | |
| 461 | 467 | // This is built from the action→keys structure in the config. | |
| 462 | 468 | func (r *ResizeModeConfig) GetKeyBindings() map[string]string { | |
| 463 | 469 | keyToAction := make(map[string]string) | |
| 464 | - | for action, keys := range r.Actions { | |
| 465 | - | for _, key := range keys { | |
| 470 | + | for action, binding := range r.Actions { | |
| 471 | + | for _, key := range binding.Keys { | |
| 466 | 472 | keyToAction[key] = action | |
| 467 | 473 | } | |
| 468 | 474 | } | |
| @@ -471,14 +477,18 @@ func (r *ResizeModeConfig) GetKeyBindings() map[string]string { | |||
| 471 | 477 | ||
| 472 | 478 | // GlobalShortcutsConfig defines global shortcuts (always active, not modal). | |
| 473 | 479 | type GlobalShortcutsConfig struct { | |
| 474 | - | ClosePane string `mapstructure:"close_pane" yaml:"close_pane" toml:"close_pane" json:"close_pane"` | |
| 475 | - | NextTab string `mapstructure:"next_tab" yaml:"next_tab" toml:"next_tab" json:"next_tab"` | |
| 476 | - | PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" toml:"previous_tab" json:"previous_tab"` | |
| 480 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 481 | + | } | |
| 477 | 482 | ||
| 478 | - | ConsumeOrExpelLeft string `mapstructure:"consume_or_expel_left" yaml:"consume_or_expel_left" toml:"consume_or_expel_left" json:"consume_or_expel_left"` //nolint:lll // struct tags must stay on one line | |
| 479 | - | ConsumeOrExpelRight string `mapstructure:"consume_or_expel_right" yaml:"consume_or_expel_right" toml:"consume_or_expel_right" json:"consume_or_expel_right"` //nolint:lll // struct tags must stay on one line | |
| 480 | - | ConsumeOrExpelUp string `mapstructure:"consume_or_expel_up" yaml:"consume_or_expel_up" toml:"consume_or_expel_up" json:"consume_or_expel_up"` //nolint:lll // struct tags must stay on one line | |
| 481 | - | ConsumeOrExpelDown string `mapstructure:"consume_or_expel_down" yaml:"consume_or_expel_down" toml:"consume_or_expel_down" json:"consume_or_expel_down"` //nolint:lll // struct tags must stay on one line | |
| 483 | + | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | |
| 484 | + | func (g *GlobalShortcutsConfig) GetKeyBindings() map[string]string { | |
| 485 | + | keyToAction := make(map[string]string) | |
| 486 | + | for action, binding := range g.Actions { | |
| 487 | + | for _, key := range binding.Keys { | |
| 488 | + | keyToAction[key] = action | |
| 489 | + | } | |
| 490 | + | } | |
| 491 | + | return keyToAction | |
| 482 | 492 | } | |
| 483 | 493 | ||
| 484 | 494 | // PopupBehavior defines how popup windows should be opened | |
| @@ -574,17 +584,17 @@ type SessionConfig struct { | |||
| 574 | 584 | ||
| 575 | 585 | // SessionModeConfig defines modal behavior for session management. | |
| 576 | 586 | type SessionModeConfig struct { | |
| 577 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 578 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 579 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 587 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | |
| 588 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | |
| 589 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | |
| 580 | 590 | } | |
| 581 | 591 | ||
| 582 | 592 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | |
| 583 | 593 | // This is built from the action→keys structure in the config. | |
| 584 | 594 | func (s *SessionModeConfig) GetKeyBindings() map[string]string { | |
| 585 | 595 | keyToAction := make(map[string]string) | |
| 586 | - | for action, keys := range s.Actions { | |
| 587 | - | for _, key := range keys { | |
| 596 | + | for action, binding := range s.Actions { | |
| 597 | + | for _, key := range binding.Keys { | |
| 588 | 598 | keyToAction[key] = action | |
| 589 | 599 | } | |
| 590 | 600 | } | |
| @@ -410,19 +410,25 @@ type WorkspaceConfig struct { | |||||
| 410 | Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" toml:"styling" json:"styling"` | 410 | Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" toml:"styling" json:"styling"` | ||
| 411 | } | 411 | } | ||
| 412 | 412 | ||||
| 413 | + | // ActionBinding defines a keybinding with optional description. | |||
| 414 | + | type ActionBinding struct { | |||
| 415 | + | Keys []string `mapstructure:"keys" yaml:"keys" toml:"keys" json:"keys"` | |||
| 416 | + | Desc string `mapstructure:"desc" yaml:"desc" toml:"desc" json:"desc,omitempty"` | |||
| 417 | + | } | |||
| 418 | + | ||||
| 413 | // PaneModeConfig defines modal behavior for pane management. | 419 | // PaneModeConfig defines modal behavior for pane management. | ||
| 414 | type PaneModeConfig struct { | 420 | type PaneModeConfig struct { | ||
| 415 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | 421 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line |
| 416 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | 422 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` |
| 417 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | 423 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` |
| 418 | } | 424 | } | ||
| 419 | 425 | ||||
| 420 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | 426 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | ||
| 421 | // This is built from the action→keys structure in the config. | 427 | // This is built from the action→keys structure in the config. | ||
| 422 | func (p *PaneModeConfig) GetKeyBindings() map[string]string { | 428 | func (p *PaneModeConfig) GetKeyBindings() map[string]string { | ||
| 423 | keyToAction := make(map[string]string) | 429 | keyToAction := make(map[string]string) | ||
| 424 | - | for action, keys := range p.Actions { | 430 | + | for action, binding := range p.Actions { |
| 425 | - | for _, key := range keys { | 431 | + | for _, key := range binding.Keys { |
| 426 | keyToAction[key] = action | 432 | keyToAction[key] = action | ||
| 427 | } | 433 | } | ||
| 428 | } | 434 | } | ||
| @@ -431,17 +437,17 @@ func (p *PaneModeConfig) GetKeyBindings() map[string]string { | |||||
| 431 | 437 | ||||
| 432 | // TabModeConfig defines modal behavior for tab management (Zellij-style). | 438 | // TabModeConfig defines modal behavior for tab management (Zellij-style). | ||
| 433 | type TabModeConfig struct { | 439 | type TabModeConfig struct { | ||
| 434 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | 440 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line |
| 435 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | 441 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` |
| 436 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | 442 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` |
| 437 | } | 443 | } | ||
| 438 | 444 | ||||
| 439 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | 445 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | ||
| 440 | // This is built from the action→keys structure in the config. | 446 | // This is built from the action→keys structure in the config. | ||
| 441 | func (t *TabModeConfig) GetKeyBindings() map[string]string { | 447 | func (t *TabModeConfig) GetKeyBindings() map[string]string { | ||
| 442 | keyToAction := make(map[string]string) | 448 | keyToAction := make(map[string]string) | ||
| 443 | - | for action, keys := range t.Actions { | 449 | + | for action, binding := range t.Actions { |
| 444 | - | for _, key := range keys { | 450 | + | for _, key := range binding.Keys { |
| 445 | keyToAction[key] = action | 451 | keyToAction[key] = action | ||
| 446 | } | 452 | } | ||
| 447 | } | 453 | } | ||
| @@ -450,19 +456,19 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string { | |||||
| 450 | 456 | ||||
| 451 | // ResizeModeConfig defines modal behavior for resizing panes (Zellij-style). | 457 | // ResizeModeConfig defines modal behavior for resizing panes (Zellij-style). | ||
| 452 | type ResizeModeConfig struct { | 458 | type ResizeModeConfig struct { | ||
| 453 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | 459 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line |
| 454 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | 460 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` |
| 455 | - | StepPercent float64 `mapstructure:"step_percent" yaml:"step_percent" toml:"step_percent" json:"step_percent"` | 461 | + | StepPercent float64 `mapstructure:"step_percent" yaml:"step_percent" toml:"step_percent" json:"step_percent"` |
| 456 | - | MinPanePercent float64 `mapstructure:"min_pane_percent" yaml:"min_pane_percent" toml:"min_pane_percent" json:"min_pane_percent"` //nolint:lll // struct tags must stay on one line | 462 | + | MinPanePercent float64 `mapstructure:"min_pane_percent" yaml:"min_pane_percent" toml:"min_pane_percent" json:"min_pane_percent"` //nolint:lll // struct tags must stay on one line |
| 457 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | 463 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` |
| 458 | } | 464 | } | ||
| 459 | 465 | ||||
| 460 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | 466 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | ||
| 461 | // This is built from the action→keys structure in the config. | 467 | // This is built from the action→keys structure in the config. | ||
| 462 | func (r *ResizeModeConfig) GetKeyBindings() map[string]string { | 468 | func (r *ResizeModeConfig) GetKeyBindings() map[string]string { | ||
| 463 | keyToAction := make(map[string]string) | 469 | keyToAction := make(map[string]string) | ||
| 464 | - | for action, keys := range r.Actions { | 470 | + | for action, binding := range r.Actions { |
| 465 | - | for _, key := range keys { | 471 | + | for _, key := range binding.Keys { |
| 466 | keyToAction[key] = action | 472 | keyToAction[key] = action | ||
| 467 | } | 473 | } | ||
| 468 | } | 474 | } | ||
| @@ -471,14 +477,18 @@ func (r *ResizeModeConfig) GetKeyBindings() map[string]string { | |||||
| 471 | 477 | ||||
| 472 | // GlobalShortcutsConfig defines global shortcuts (always active, not modal). | 478 | // GlobalShortcutsConfig defines global shortcuts (always active, not modal). | ||
| 473 | type GlobalShortcutsConfig struct { | 479 | type GlobalShortcutsConfig struct { | ||
| 474 | - | ClosePane string `mapstructure:"close_pane" yaml:"close_pane" toml:"close_pane" json:"close_pane"` | 480 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` |
| 475 | - | NextTab string `mapstructure:"next_tab" yaml:"next_tab" toml:"next_tab" json:"next_tab"` | 481 | + | } |
| 476 | - | PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" toml:"previous_tab" json:"previous_tab"` | |||
| 477 | 482 | ||||
| 478 | - | ConsumeOrExpelLeft string `mapstructure:"consume_or_expel_left" yaml:"consume_or_expel_left" toml:"consume_or_expel_left" json:"consume_or_expel_left"` //nolint:lll // struct tags must stay on one line | 483 | + | // GetKeyBindings returns an inverted map for O(1) key→action lookup. |
| 479 | - | ConsumeOrExpelRight string `mapstructure:"consume_or_expel_right" yaml:"consume_or_expel_right" toml:"consume_or_expel_right" json:"consume_or_expel_right"` //nolint:lll // struct tags must stay on one line | 484 | + | func (g *GlobalShortcutsConfig) GetKeyBindings() map[string]string { |
| 480 | - | ConsumeOrExpelUp string `mapstructure:"consume_or_expel_up" yaml:"consume_or_expel_up" toml:"consume_or_expel_up" json:"consume_or_expel_up"` //nolint:lll // struct tags must stay on one line | 485 | + | keyToAction := make(map[string]string) |
| 481 | - | ConsumeOrExpelDown string `mapstructure:"consume_or_expel_down" yaml:"consume_or_expel_down" toml:"consume_or_expel_down" json:"consume_or_expel_down"` //nolint:lll // struct tags must stay on one line | 486 | + | for action, binding := range g.Actions { |
| 487 | + | for _, key := range binding.Keys { | |||
| 488 | + | keyToAction[key] = action | |||
| 489 | + | } | |||
| 490 | + | } | |||
| 491 | + | return keyToAction | |||
| 482 | } | 492 | } | ||
| 483 | 493 | ||||
| 484 | // PopupBehavior defines how popup windows should be opened | 494 | // PopupBehavior defines how popup windows should be opened | ||
| @@ -574,17 +584,17 @@ type SessionConfig struct { | |||||
| 574 | 584 | ||||
| 575 | // SessionModeConfig defines modal behavior for session management. | 585 | // SessionModeConfig defines modal behavior for session management. | ||
| 576 | type SessionModeConfig struct { | 586 | type SessionModeConfig struct { | ||
| 577 | - | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line | 587 | + | ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" toml:"activation_shortcut" json:"activation_shortcut"` //nolint:lll // struct tags must stay on one line |
| 578 | - | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` | 588 | + | TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" toml:"timeout_ms" json:"timeout_ms"` |
| 579 | - | Actions map[string][]string `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` | 589 | + | Actions map[string]ActionBinding `mapstructure:"actions" yaml:"actions" toml:"actions" json:"actions"` |
| 580 | } | 590 | } | ||
| 581 | 591 | ||||
| 582 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | 592 | // GetKeyBindings returns an inverted map for O(1) key→action lookup. | ||
| 583 | // This is built from the action→keys structure in the config. | 593 | // This is built from the action→keys structure in the config. | ||
| 584 | func (s *SessionModeConfig) GetKeyBindings() map[string]string { | 594 | func (s *SessionModeConfig) GetKeyBindings() map[string]string { | ||
| 585 | keyToAction := make(map[string]string) | 595 | keyToAction := make(map[string]string) | ||
| 586 | - | for action, keys := range s.Actions { | 596 | + | for action, binding := range s.Actions { |
| 587 | - | for _, key := range keys { | 597 | + | for _, key := range binding.Keys { |
| 588 | keyToAction[key] = action | 598 | keyToAction[key] = action | ||
| 589 | } | 599 | } | ||
| 590 | } | 600 | } | ||
M internal/infrastructure/config/schema_provider.go +5 -47
| @@ -445,54 +445,12 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||
| 445 | 445 | Description: "Key bindings for resize mode actions", | |
| 446 | 446 | Section: SectionWorkspace, | |
| 447 | 447 | }, | |
| 448 | - | // Global shortcuts | |
| 448 | + | // Global shortcuts - managed via dedicated keybindings UI | |
| 449 | 449 | { | |
| 450 | - | Key: "workspace.shortcuts.close_pane", | |
| 451 | - | Type: "string", | |
| 452 | - | Default: defaults.Workspace.Shortcuts.ClosePane, | |
| 453 | - | Description: "Global shortcut to close current pane", | |
| 454 | - | Section: SectionWorkspace, | |
| 455 | - | }, | |
| 456 | - | { | |
| 457 | - | Key: "workspace.shortcuts.next_tab", | |
| 458 | - | Type: "string", | |
| 459 | - | Default: defaults.Workspace.Shortcuts.NextTab, | |
| 460 | - | Description: "Global shortcut to switch to next tab", | |
| 461 | - | Section: SectionWorkspace, | |
| 462 | - | }, | |
| 463 | - | { | |
| 464 | - | Key: "workspace.shortcuts.previous_tab", | |
| 465 | - | Type: "string", | |
| 466 | - | Default: defaults.Workspace.Shortcuts.PreviousTab, | |
| 467 | - | Description: "Global shortcut to switch to previous tab", | |
| 468 | - | Section: SectionWorkspace, | |
| 469 | - | }, | |
| 470 | - | { | |
| 471 | - | Key: "workspace.shortcuts.consume_or_expel_left", | |
| 472 | - | Type: "string", | |
| 473 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelLeft, | |
| 474 | - | Description: "Consume into left sibling stack or expel left (alpha)", | |
| 475 | - | Section: SectionWorkspace, | |
| 476 | - | }, | |
| 477 | - | { | |
| 478 | - | Key: "workspace.shortcuts.consume_or_expel_right", | |
| 479 | - | Type: "string", | |
| 480 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelRight, | |
| 481 | - | Description: "Consume into right sibling stack or expel right (alpha)", | |
| 482 | - | Section: SectionWorkspace, | |
| 483 | - | }, | |
| 484 | - | { | |
| 485 | - | Key: "workspace.shortcuts.consume_or_expel_up", | |
| 486 | - | Type: "string", | |
| 487 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelUp, | |
| 488 | - | Description: "Consume into upper sibling stack or expel up (alpha)", | |
| 489 | - | Section: SectionWorkspace, | |
| 490 | - | }, | |
| 491 | - | { | |
| 492 | - | Key: "workspace.shortcuts.consume_or_expel_down", | |
| 493 | - | Type: "string", | |
| 494 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelDown, | |
| 495 | - | Description: "Consume into lower sibling stack or expel down (alpha)", | |
| 450 | + | Key: "workspace.shortcuts.actions", | |
| 451 | + | Type: "object", | |
| 452 | + | Default: "(see defaults)", | |
| 453 | + | Description: "Global keyboard shortcuts (use Keybindings tab to edit)", | |
| 496 | 454 | Section: SectionWorkspace, | |
| 497 | 455 | }, | |
| 498 | 456 | // Popups | |
| @@ -445,54 +445,12 @@ func (*SchemaProvider) getWorkspaceKeys(defaults *Config) []entity.ConfigKeyInfo | |||||
| 445 | Description: "Key bindings for resize mode actions", | 445 | Description: "Key bindings for resize mode actions", | ||
| 446 | Section: SectionWorkspace, | 446 | Section: SectionWorkspace, | ||
| 447 | }, | 447 | }, | ||
| 448 | - | // Global shortcuts | 448 | + | // Global shortcuts - managed via dedicated keybindings UI |
| 449 | { | 449 | { | ||
| 450 | - | Key: "workspace.shortcuts.close_pane", | 450 | + | Key: "workspace.shortcuts.actions", |
| 451 | - | Type: "string", | 451 | + | Type: "object", |
| 452 | - | Default: defaults.Workspace.Shortcuts.ClosePane, | 452 | + | Default: "(see defaults)", |
| 453 | - | Description: "Global shortcut to close current pane", | 453 | + | Description: "Global keyboard shortcuts (use Keybindings tab to edit)", |
| 454 | - | Section: SectionWorkspace, | |||
| 455 | - | }, | |||
| 456 | - | { | |||
| 457 | - | Key: "workspace.shortcuts.next_tab", | |||
| 458 | - | Type: "string", | |||
| 459 | - | Default: defaults.Workspace.Shortcuts.NextTab, | |||
| 460 | - | Description: "Global shortcut to switch to next tab", | |||
| 461 | - | Section: SectionWorkspace, | |||
| 462 | - | }, | |||
| 463 | - | { | |||
| 464 | - | Key: "workspace.shortcuts.previous_tab", | |||
| 465 | - | Type: "string", | |||
| 466 | - | Default: defaults.Workspace.Shortcuts.PreviousTab, | |||
| 467 | - | Description: "Global shortcut to switch to previous tab", | |||
| 468 | - | Section: SectionWorkspace, | |||
| 469 | - | }, | |||
| 470 | - | { | |||
| 471 | - | Key: "workspace.shortcuts.consume_or_expel_left", | |||
| 472 | - | Type: "string", | |||
| 473 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelLeft, | |||
| 474 | - | Description: "Consume into left sibling stack or expel left (alpha)", | |||
| 475 | - | Section: SectionWorkspace, | |||
| 476 | - | }, | |||
| 477 | - | { | |||
| 478 | - | Key: "workspace.shortcuts.consume_or_expel_right", | |||
| 479 | - | Type: "string", | |||
| 480 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelRight, | |||
| 481 | - | Description: "Consume into right sibling stack or expel right (alpha)", | |||
| 482 | - | Section: SectionWorkspace, | |||
| 483 | - | }, | |||
| 484 | - | { | |||
| 485 | - | Key: "workspace.shortcuts.consume_or_expel_up", | |||
| 486 | - | Type: "string", | |||
| 487 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelUp, | |||
| 488 | - | Description: "Consume into upper sibling stack or expel up (alpha)", | |||
| 489 | - | Section: SectionWorkspace, | |||
| 490 | - | }, | |||
| 491 | - | { | |||
| 492 | - | Key: "workspace.shortcuts.consume_or_expel_down", | |||
| 493 | - | Type: "string", | |||
| 494 | - | Default: defaults.Workspace.Shortcuts.ConsumeOrExpelDown, | |||
| 495 | - | Description: "Consume into lower sibling stack or expel down (alpha)", | |||
| 496 | Section: SectionWorkspace, | 454 | Section: SectionWorkspace, | ||
| 497 | }, | 455 | }, | ||
| 498 | // Popups | 456 | // Popups | ||
M internal/infrastructure/config/validation.go +6 -6
| @@ -166,11 +166,11 @@ func validatePaneMode(config *Config) []string { | |||
| 166 | 166 | } | |
| 167 | 167 | ||
| 168 | 168 | seenKeys := make(map[string]string) | |
| 169 | - | for action, keys := range config.Workspace.PaneMode.Actions { | |
| 170 | - | if len(keys) == 0 { | |
| 169 | + | for action, binding := range config.Workspace.PaneMode.Actions { | |
| 170 | + | if len(binding.Keys) == 0 { | |
| 171 | 171 | validationErrors = append(validationErrors, fmt.Sprintf("workspace.pane_mode.actions.%s must have at least one key binding", action)) | |
| 172 | 172 | } | |
| 173 | - | for _, key := range keys { | |
| 173 | + | for _, key := range binding.Keys { | |
| 174 | 174 | if existingAction, exists := seenKeys[key]; exists { | |
| 175 | 175 | validationErrors = append(validationErrors, fmt.Sprintf( | |
| 176 | 176 | "duplicate key binding '%s' found in pane_mode actions '%s' and '%s'", | |
| @@ -204,11 +204,11 @@ func validateTabMode(config *Config) []string { | |||
| 204 | 204 | } | |
| 205 | 205 | ||
| 206 | 206 | tabSeenKeys := make(map[string]string) | |
| 207 | - | for action, keys := range config.Workspace.TabMode.Actions { | |
| 208 | - | if len(keys) == 0 { | |
| 207 | + | for action, binding := range config.Workspace.TabMode.Actions { | |
| 208 | + | if len(binding.Keys) == 0 { | |
| 209 | 209 | validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action)) | |
| 210 | 210 | } | |
| 211 | - | for _, key := range keys { | |
| 211 | + | for _, key := range binding.Keys { | |
| 212 | 212 | if existingAction, exists := tabSeenKeys[key]; exists { | |
| 213 | 213 | validationErrors = append(validationErrors, fmt.Sprintf( | |
| 214 | 214 | "duplicate key binding '%s' found in tab_mode actions '%s' and '%s'", | |
| @@ -166,11 +166,11 @@ func validatePaneMode(config *Config) []string { | |||||
| 166 | } | 166 | } | ||
| 167 | 167 | ||||
| 168 | seenKeys := make(map[string]string) | 168 | seenKeys := make(map[string]string) | ||
| 169 | - | for action, keys := range config.Workspace.PaneMode.Actions { | 169 | + | for action, binding := range config.Workspace.PaneMode.Actions { |
| 170 | - | if len(keys) == 0 { | 170 | + | if len(binding.Keys) == 0 { |
| 171 | validationErrors = append(validationErrors, fmt.Sprintf("workspace.pane_mode.actions.%s must have at least one key binding", action)) | 171 | validationErrors = append(validationErrors, fmt.Sprintf("workspace.pane_mode.actions.%s must have at least one key binding", action)) | ||
| 172 | } | 172 | } | ||
| 173 | - | for _, key := range keys { | 173 | + | for _, key := range binding.Keys { |
| 174 | if existingAction, exists := seenKeys[key]; exists { | 174 | if existingAction, exists := seenKeys[key]; exists { | ||
| 175 | validationErrors = append(validationErrors, fmt.Sprintf( | 175 | validationErrors = append(validationErrors, fmt.Sprintf( | ||
| 176 | "duplicate key binding '%s' found in pane_mode actions '%s' and '%s'", | 176 | "duplicate key binding '%s' found in pane_mode actions '%s' and '%s'", | ||
| @@ -204,11 +204,11 @@ func validateTabMode(config *Config) []string { | |||||
| 204 | } | 204 | } | ||
| 205 | 205 | ||||
| 206 | tabSeenKeys := make(map[string]string) | 206 | tabSeenKeys := make(map[string]string) | ||
| 207 | - | for action, keys := range config.Workspace.TabMode.Actions { | 207 | + | for action, binding := range config.Workspace.TabMode.Actions { |
| 208 | - | if len(keys) == 0 { | 208 | + | if len(binding.Keys) == 0 { |
| 209 | validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action)) | 209 | validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action)) | ||
| 210 | } | 210 | } | ||
| 211 | - | for _, key := range keys { | 211 | + | for _, key := range binding.Keys { |
| 212 | if existingAction, exists := tabSeenKeys[key]; exists { | 212 | if existingAction, exists := tabSeenKeys[key]; exists { | ||
| 213 | validationErrors = append(validationErrors, fmt.Sprintf( | 213 | validationErrors = append(validationErrors, fmt.Sprintf( | ||
| 214 | "duplicate key binding '%s' found in tab_mode actions '%s' and '%s'", | 214 | "duplicate key binding '%s' found in tab_mode actions '%s' and '%s'", | ||
M internal/ui/input/global_shortcuts.go +20 -11
| @@ -94,20 +94,29 @@ func NewGlobalShortcutHandler( | |||
| 94 | 94 | Msg("registered global shortcut") | |
| 95 | 95 | ||
| 96 | 96 | if cfg != nil { | |
| 97 | - | registerFromConfig := func(key string, action Action) { | |
| 98 | - | binding, ok := ParseKeyString(key) | |
| 97 | + | // Map action names to action constants for global shortcuts | |
| 98 | + | actionMap := map[string]Action{ | |
| 99 | + | "consume_or_expel_left": ActionConsumeOrExpelLeft, | |
| 100 | + | "consume_or_expel_right": ActionConsumeOrExpelRight, | |
| 101 | + | "consume_or_expel_up": ActionConsumeOrExpelUp, | |
| 102 | + | "consume_or_expel_down": ActionConsumeOrExpelDown, | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | for actionName, actionBinding := range cfg.Workspace.Shortcuts.Actions { | |
| 106 | + | action, ok := actionMap[actionName] | |
| 99 | 107 | if !ok { | |
| 100 | - | log.Warn().Str("shortcut", key).Str("action", string(action)).Msg("failed to parse global shortcut") | |
| 101 | - | return | |
| 108 | + | continue | |
| 109 | + | } | |
| 110 | + | for _, keyStr := range actionBinding.Keys { | |
| 111 | + | binding, ok := ParseKeyString(keyStr) | |
| 112 | + | if !ok { | |
| 113 | + | log.Warn().Str("shortcut", keyStr).Str("action", string(action)).Msg("failed to parse global shortcut") | |
| 114 | + | continue | |
| 115 | + | } | |
| 116 | + | h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action) | |
| 117 | + | log.Trace().Str("shortcut", keyStr).Str("action", string(action)).Msg("registered global shortcut") | |
| 102 | 118 | } | |
| 103 | - | h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action) | |
| 104 | - | log.Trace().Str("shortcut", key).Str("action", string(action)).Msg("registered global shortcut") | |
| 105 | 119 | } | |
| 106 | - | ||
| 107 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelLeft, ActionConsumeOrExpelLeft) | |
| 108 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelRight, ActionConsumeOrExpelRight) | |
| 109 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelUp, ActionConsumeOrExpelUp) | |
| 110 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelDown, ActionConsumeOrExpelDown) | |
| 111 | 120 | } | |
| 112 | 121 | ||
| 113 | 122 | // Attach to window | |
| @@ -94,20 +94,29 @@ func NewGlobalShortcutHandler( | |||||
| 94 | Msg("registered global shortcut") | 94 | Msg("registered global shortcut") | ||
| 95 | 95 | ||||
| 96 | if cfg != nil { | 96 | if cfg != nil { | ||
| 97 | - | registerFromConfig := func(key string, action Action) { | 97 | + | // Map action names to action constants for global shortcuts |
| 98 | - | binding, ok := ParseKeyString(key) | 98 | + | actionMap := map[string]Action{ |
| 99 | + | "consume_or_expel_left": ActionConsumeOrExpelLeft, | |||
| 100 | + | "consume_or_expel_right": ActionConsumeOrExpelRight, | |||
| 101 | + | "consume_or_expel_up": ActionConsumeOrExpelUp, | |||
| 102 | + | "consume_or_expel_down": ActionConsumeOrExpelDown, | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | for actionName, actionBinding := range cfg.Workspace.Shortcuts.Actions { | |||
| 106 | + | action, ok := actionMap[actionName] | |||
| 99 | if !ok { | 107 | if !ok { | ||
| 100 | - | log.Warn().Str("shortcut", key).Str("action", string(action)).Msg("failed to parse global shortcut") | 108 | + | continue |
| 101 | - | return | 109 | + | } |
| 110 | + | for _, keyStr := range actionBinding.Keys { | |||
| 111 | + | binding, ok := ParseKeyString(keyStr) | |||
| 112 | + | if !ok { | |||
| 113 | + | log.Warn().Str("shortcut", keyStr).Str("action", string(action)).Msg("failed to parse global shortcut") | |||
| 114 | + | continue | |||
| 115 | + | } | |||
| 116 | + | h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action) | |||
| 117 | + | log.Trace().Str("shortcut", keyStr).Str("action", string(action)).Msg("registered global shortcut") | |||
| 102 | } | 118 | } | ||
| 103 | - | h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action) | |||
| 104 | - | log.Trace().Str("shortcut", key).Str("action", string(action)).Msg("registered global shortcut") | |||
| 105 | } | 119 | } | ||
| 106 | - | ||||
| 107 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelLeft, ActionConsumeOrExpelLeft) | |||
| 108 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelRight, ActionConsumeOrExpelRight) | |||
| 109 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelUp, ActionConsumeOrExpelUp) | |||
| 110 | - | registerFromConfig(cfg.Workspace.Shortcuts.ConsumeOrExpelDown, ActionConsumeOrExpelDown) | |||
| 111 | } | 120 | } | ||
| 112 | 121 | ||||
| 113 | // Attach to window | 122 | // Attach to window | ||
M internal/ui/input/shortcuts.go +19 -19
| @@ -325,27 +325,27 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *config.WorkspaceConfig) { | |||
| 325 | 325 | // This follows Zellij-style modal keyboard interface. | |
| 326 | 326 | // | |
| 327 | 327 | // However, these standard browser shortcuts ARE global: | |
| 328 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ClosePane); ok { | |
| 329 | - | s.Global[binding] = ActionClosePane // Ctrl+W closes active pane | |
| 330 | - | } | |
| 331 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.NextTab); ok { | |
| 332 | - | s.Global[binding] = ActionNextTab // Ctrl+Tab | |
| 333 | - | } | |
| 334 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.PreviousTab); ok { | |
| 335 | - | s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab | |
| 328 | + | // Map action names to action constants | |
| 329 | + | actionMap := map[string]Action{ | |
| 330 | + | "close_pane": ActionClosePane, | |
| 331 | + | "next_tab": ActionNextTab, | |
| 332 | + | "previous_tab": ActionPreviousTab, | |
| 333 | + | "consume_or_expel_left": ActionConsumeOrExpelLeft, | |
| 334 | + | "consume_or_expel_right": ActionConsumeOrExpelRight, | |
| 335 | + | "consume_or_expel_up": ActionConsumeOrExpelUp, | |
| 336 | + | "consume_or_expel_down": ActionConsumeOrExpelDown, | |
| 336 | 337 | } | |
| 337 | 338 | ||
| 338 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelLeft); ok { | |
| 339 | - | s.Global[binding] = ActionConsumeOrExpelLeft | |
| 340 | - | } | |
| 341 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelRight); ok { | |
| 342 | - | s.Global[binding] = ActionConsumeOrExpelRight | |
| 343 | - | } | |
| 344 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelUp); ok { | |
| 345 | - | s.Global[binding] = ActionConsumeOrExpelUp | |
| 346 | - | } | |
| 347 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelDown); ok { | |
| 348 | - | s.Global[binding] = ActionConsumeOrExpelDown | |
| 339 | + | for actionName, actionBinding := range cfg.Shortcuts.Actions { | |
| 340 | + | action, ok := actionMap[actionName] | |
| 341 | + | if !ok { | |
| 342 | + | continue | |
| 343 | + | } | |
| 344 | + | for _, keyStr := range actionBinding.Keys { | |
| 345 | + | if binding, ok := ParseKeyString(keyStr); ok { | |
| 346 | + | s.Global[binding] = action | |
| 347 | + | } | |
| 348 | + | } | |
| 349 | 349 | } | |
| 350 | 350 | } | |
| 351 | 351 | ||
| @@ -325,27 +325,27 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *config.WorkspaceConfig) { | |||||
| 325 | // This follows Zellij-style modal keyboard interface. | 325 | // This follows Zellij-style modal keyboard interface. | ||
| 326 | // | 326 | // | ||
| 327 | // However, these standard browser shortcuts ARE global: | 327 | // However, these standard browser shortcuts ARE global: | ||
| 328 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ClosePane); ok { | 328 | + | // Map action names to action constants |
| 329 | - | s.Global[binding] = ActionClosePane // Ctrl+W closes active pane | 329 | + | actionMap := map[string]Action{ |
| 330 | - | } | 330 | + | "close_pane": ActionClosePane, |
| 331 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.NextTab); ok { | 331 | + | "next_tab": ActionNextTab, |
| 332 | - | s.Global[binding] = ActionNextTab // Ctrl+Tab | 332 | + | "previous_tab": ActionPreviousTab, |
| 333 | - | } | 333 | + | "consume_or_expel_left": ActionConsumeOrExpelLeft, |
| 334 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.PreviousTab); ok { | 334 | + | "consume_or_expel_right": ActionConsumeOrExpelRight, |
| 335 | - | s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab | 335 | + | "consume_or_expel_up": ActionConsumeOrExpelUp, |
| 336 | + | "consume_or_expel_down": ActionConsumeOrExpelDown, | |||
| 336 | } | 337 | } | ||
| 337 | 338 | ||||
| 338 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelLeft); ok { | 339 | + | for actionName, actionBinding := range cfg.Shortcuts.Actions { |
| 339 | - | s.Global[binding] = ActionConsumeOrExpelLeft | 340 | + | action, ok := actionMap[actionName] |
| 340 | - | } | 341 | + | if !ok { |
| 341 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelRight); ok { | 342 | + | continue |
| 342 | - | s.Global[binding] = ActionConsumeOrExpelRight | 343 | + | } |
| 343 | - | } | 344 | + | for _, keyStr := range actionBinding.Keys { |
| 344 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelUp); ok { | 345 | + | if binding, ok := ParseKeyString(keyStr); ok { |
| 345 | - | s.Global[binding] = ActionConsumeOrExpelUp | 346 | + | s.Global[binding] = action |
| 346 | - | } | 347 | + | } |
| 347 | - | if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelDown); ok { | 348 | + | } |
| 348 | - | s.Global[binding] = ActionConsumeOrExpelDown | |||
| 349 | } | 349 | } | ||
| 350 | } | 350 | } | ||
| 351 | 351 | ||||