refactor(shortcuts): change ctrl+w to close pane instead of tab
- commit
- 46f04d2797578d740898e5566116e72776a0a5c0
- author
- bnema · · 2025-12-18 22:16 UTC
- parent
- 8de4070
- signature
- unsigned
6 files · +31 -22
M docs/CONFIG.md +11 -5
| @@ -199,7 +199,12 @@ split-right = ["arrowright", "r"] | |||
| 199 | 199 | split-left = ["arrowleft", "l"] | |
| 200 | 200 | split-up = ["arrowup", "u"] | |
| 201 | 201 | split-down = ["arrowdown", "d"] | |
| 202 | + | stack-pane = ["s"] | |
| 202 | 203 | close-pane = ["x"] | |
| 204 | + | focus-right = ["shift+arrowright", "shift+l"] | |
| 205 | + | focus-left = ["shift+arrowleft", "shift+h"] | |
| 206 | + | focus-up = ["shift+arrowup", "shift+k"] | |
| 207 | + | focus-down = ["shift+arrowdown", "shift+j"] | |
| 203 | 208 | confirm = ["enter"] | |
| 204 | 209 | cancel = ["escape"] | |
| 205 | 210 | ``` | |
| @@ -228,14 +233,15 @@ cancel = ["escape"] | |||
| 228 | 233 | ||
| 229 | 234 | > **Note:** Actions are inverted to key→action map in memory for O(1) lookup performance during navigation. | |
| 230 | 235 | ||
| 231 | - | ### Tab Shortcuts | |
| 236 | + | ### Global Shortcuts | |
| 232 | 237 | ||
| 233 | 238 | | Key | Type | Default | Description | | |
| 234 | 239 | |-----|------|---------|-------------| | |
| 235 | - | | `workspace.tabs.new_tab` | string | `"ctrl+t"` | New tab shortcut | | |
| 236 | - | | `workspace.tabs.close_tab` | string | `"ctrl+w"` | Close tab shortcut | | |
| 237 | - | | `workspace.tabs.next_tab` | string | `"ctrl+tab"` | Next tab shortcut | | |
| 238 | - | | `workspace.tabs.previous_tab` | string | `"ctrl+shift+tab"` | Previous tab shortcut | | |
| 240 | + | | `workspace.shortcuts.close_pane` | string | `"ctrl+w"` | Close active pane (closes tab if last pane) | | |
| 241 | + | | `workspace.shortcuts.next_tab` | string | `"ctrl+tab"` | Next tab shortcut | | |
| 242 | + | | `workspace.shortcuts.previous_tab` | string | `"ctrl+shift+tab"` | Previous tab shortcut | | |
| 243 | + | ||
| 244 | + | > **Note:** New tab creation uses modal tab mode (Ctrl+T then n/c). This follows the Zellij-style modal keyboard interface. | |
| 239 | 245 | ||
| 240 | 246 | ### Popup Behavior | |
| 241 | 247 | ||
| @@ -199,7 +199,12 @@ split-right = ["arrowright", "r"] | |||||
| 199 | split-left = ["arrowleft", "l"] | 199 | split-left = ["arrowleft", "l"] | ||
| 200 | split-up = ["arrowup", "u"] | 200 | split-up = ["arrowup", "u"] | ||
| 201 | split-down = ["arrowdown", "d"] | 201 | split-down = ["arrowdown", "d"] | ||
| 202 | + | stack-pane = ["s"] | |||
| 202 | close-pane = ["x"] | 203 | close-pane = ["x"] | ||
| 204 | + | focus-right = ["shift+arrowright", "shift+l"] | |||
| 205 | + | focus-left = ["shift+arrowleft", "shift+h"] | |||
| 206 | + | focus-up = ["shift+arrowup", "shift+k"] | |||
| 207 | + | focus-down = ["shift+arrowdown", "shift+j"] | |||
| 203 | confirm = ["enter"] | 208 | confirm = ["enter"] | ||
| 204 | cancel = ["escape"] | 209 | cancel = ["escape"] | ||
| 205 | ``` | 210 | ``` | ||
| @@ -228,14 +233,15 @@ cancel = ["escape"] | |||||
| 228 | 233 | ||||
| 229 | > **Note:** Actions are inverted to key→action map in memory for O(1) lookup performance during navigation. | 234 | > **Note:** Actions are inverted to key→action map in memory for O(1) lookup performance during navigation. | ||
| 230 | 235 | ||||
| 231 | - | ### Tab Shortcuts | 236 | + | ### Global Shortcuts |
| 232 | 237 | ||||
| 233 | | Key | Type | Default | Description | | 238 | | Key | Type | Default | Description | | ||
| 234 | |-----|------|---------|-------------| | 239 | |-----|------|---------|-------------| | ||
| 235 | - | | `workspace.tabs.new_tab` | string | `"ctrl+t"` | New tab shortcut | | 240 | + | | `workspace.shortcuts.close_pane` | string | `"ctrl+w"` | Close active pane (closes tab if last pane) | |
| 236 | - | | `workspace.tabs.close_tab` | string | `"ctrl+w"` | Close tab shortcut | | 241 | + | | `workspace.shortcuts.next_tab` | string | `"ctrl+tab"` | Next tab shortcut | |
| 237 | - | | `workspace.tabs.next_tab` | string | `"ctrl+tab"` | Next tab shortcut | | 242 | + | | `workspace.shortcuts.previous_tab` | string | `"ctrl+shift+tab"` | Previous tab shortcut | |
| 238 | - | | `workspace.tabs.previous_tab` | string | `"ctrl+shift+tab"` | Previous tab shortcut | | 243 | + | |
| 244 | + | > **Note:** New tab creation uses modal tab mode (Ctrl+T then n/c). This follows the Zellij-style modal keyboard interface. | |||
| 239 | 245 | ||||
| 240 | ### Popup Behavior | 246 | ### Popup Behavior | ||
| 241 | 247 | ||||
M internal/infrastructure/config/defaults.go +6 -3
| @@ -126,6 +126,10 @@ func DefaultConfig() *Config { | |||
| 126 | 126 | "split-down": {"arrowdown", "d"}, | |
| 127 | 127 | "stack-pane": {"s"}, | |
| 128 | 128 | "close-pane": {"x"}, | |
| 129 | + | "focus-right": {"shift+arrowright", "shift+l"}, | |
| 130 | + | "focus-left": {"shift+arrowleft", "shift+h"}, | |
| 131 | + | "focus-up": {"shift+arrowup", "shift+k"}, | |
| 132 | + | "focus-down": {"shift+arrowdown", "shift+j"}, | |
| 129 | 133 | "confirm": {"enter"}, | |
| 130 | 134 | "cancel": {"escape"}, | |
| 131 | 135 | }, | |
| @@ -143,9 +147,8 @@ func DefaultConfig() *Config { | |||
| 143 | 147 | "cancel": {"escape"}, | |
| 144 | 148 | }, | |
| 145 | 149 | }, | |
| 146 | - | Tabs: TabKeyConfig{ | |
| 147 | - | NewTab: "ctrl+t", | |
| 148 | - | CloseTab: "ctrl+w", | |
| 150 | + | Shortcuts: GlobalShortcutsConfig{ | |
| 151 | + | ClosePane: "ctrl+w", | |
| 149 | 152 | NextTab: "ctrl+tab", | |
| 150 | 153 | PreviousTab: "ctrl+shift+tab", | |
| 151 | 154 | }, | |
| @@ -126,6 +126,10 @@ func DefaultConfig() *Config { | |||||
| 126 | "split-down": {"arrowdown", "d"}, | 126 | "split-down": {"arrowdown", "d"}, | ||
| 127 | "stack-pane": {"s"}, | 127 | "stack-pane": {"s"}, | ||
| 128 | "close-pane": {"x"}, | 128 | "close-pane": {"x"}, | ||
| 129 | + | "focus-right": {"shift+arrowright", "shift+l"}, | |||
| 130 | + | "focus-left": {"shift+arrowleft", "shift+h"}, | |||
| 131 | + | "focus-up": {"shift+arrowup", "shift+k"}, | |||
| 132 | + | "focus-down": {"shift+arrowdown", "shift+j"}, | |||
| 129 | "confirm": {"enter"}, | 133 | "confirm": {"enter"}, | ||
| 130 | "cancel": {"escape"}, | 134 | "cancel": {"escape"}, | ||
| 131 | }, | 135 | }, | ||
| @@ -143,9 +147,8 @@ func DefaultConfig() *Config { | |||||
| 143 | "cancel": {"escape"}, | 147 | "cancel": {"escape"}, | ||
| 144 | }, | 148 | }, | ||
| 145 | }, | 149 | }, | ||
| 146 | - | Tabs: TabKeyConfig{ | 150 | + | Shortcuts: GlobalShortcutsConfig{ |
| 147 | - | NewTab: "ctrl+t", | 151 | + | ClosePane: "ctrl+w", |
| 148 | - | CloseTab: "ctrl+w", | |||
| 149 | NextTab: "ctrl+tab", | 152 | NextTab: "ctrl+tab", | ||
| 150 | PreviousTab: "ctrl+shift+tab", | 153 | PreviousTab: "ctrl+shift+tab", | ||
| 151 | }, | 154 | }, | ||
M internal/infrastructure/config/loader.go +3 -4
| @@ -262,10 +262,9 @@ func (m *Manager) setDefaults() { | |||
| 262 | 262 | m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut) | |
| 263 | 263 | m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds) | |
| 264 | 264 | m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions) | |
| 265 | - | m.viper.SetDefault("workspace.tabs.new_tab", defaults.Workspace.Tabs.NewTab) | |
| 266 | - | m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab) | |
| 267 | - | m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab) | |
| 268 | - | m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab) | |
| 265 | + | m.viper.SetDefault("workspace.shortcuts.close_pane", defaults.Workspace.Shortcuts.ClosePane) | |
| 266 | + | m.viper.SetDefault("workspace.shortcuts.next_tab", defaults.Workspace.Shortcuts.NextTab) | |
| 267 | + | m.viper.SetDefault("workspace.shortcuts.previous_tab", defaults.Workspace.Shortcuts.PreviousTab) | |
| 269 | 268 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | |
| 270 | 269 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | |
| 271 | 270 | m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior)) | |
| @@ -262,10 +262,9 @@ func (m *Manager) setDefaults() { | |||||
| 262 | m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut) | 262 | m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut) | ||
| 263 | m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds) | 263 | m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds) | ||
| 264 | m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions) | 264 | m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions) | ||
| 265 | - | m.viper.SetDefault("workspace.tabs.new_tab", defaults.Workspace.Tabs.NewTab) | 265 | + | m.viper.SetDefault("workspace.shortcuts.close_pane", defaults.Workspace.Shortcuts.ClosePane) |
| 266 | - | m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab) | 266 | + | m.viper.SetDefault("workspace.shortcuts.next_tab", defaults.Workspace.Shortcuts.NextTab) |
| 267 | - | m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab) | 267 | + | m.viper.SetDefault("workspace.shortcuts.previous_tab", defaults.Workspace.Shortcuts.PreviousTab) |
| 268 | - | m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab) | |||
| 269 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | 268 | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | ||
| 270 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | 269 | m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab) | ||
| 271 | m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior)) | 270 | m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior)) | ||
M internal/infrastructure/config/schema.go +5 -6
| @@ -169,8 +169,8 @@ type WorkspaceConfig struct { | |||
| 169 | 169 | PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"` | |
| 170 | 170 | // TabMode defines modal tab behaviour and bindings (Alt+T). | |
| 171 | 171 | TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"` | |
| 172 | - | // Tabs holds classic browser tab shortcuts. | |
| 173 | - | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" toml:"tabs" json:"tabs"` | |
| 172 | + | // Shortcuts holds global (non-modal) keyboard shortcuts. | |
| 173 | + | Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"` | |
| 174 | 174 | // TabBarPosition determines tab bar placement: "top" or "bottom". | |
| 175 | 175 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | |
| 176 | 176 | // HideTabBarWhenSingleTab hides the tab bar when only one tab exists. | |
| @@ -219,10 +219,9 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string { | |||
| 219 | 219 | return keyToAction | |
| 220 | 220 | } | |
| 221 | 221 | ||
| 222 | - | // TabKeyConfig defines Zellij-inspired tab shortcuts. | |
| 223 | - | type TabKeyConfig struct { | |
| 224 | - | NewTab string `mapstructure:"new_tab" yaml:"new_tab" toml:"new_tab" json:"new_tab"` | |
| 225 | - | CloseTab string `mapstructure:"close_tab" yaml:"close_tab" toml:"close_tab" json:"close_tab"` | |
| 222 | + | // GlobalShortcutsConfig defines global shortcuts (always active, not modal). | |
| 223 | + | type GlobalShortcutsConfig struct { | |
| 224 | + | ClosePane string `mapstructure:"close_pane" yaml:"close_pane" toml:"close_pane" json:"close_pane"` | |
| 226 | 225 | NextTab string `mapstructure:"next_tab" yaml:"next_tab" toml:"next_tab" json:"next_tab"` | |
| 227 | 226 | PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" toml:"previous_tab" json:"previous_tab"` | |
| 228 | 227 | } | |
| @@ -169,8 +169,8 @@ type WorkspaceConfig struct { | |||||
| 169 | PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"` | 169 | PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"` | ||
| 170 | // TabMode defines modal tab behaviour and bindings (Alt+T). | 170 | // TabMode defines modal tab behaviour and bindings (Alt+T). | ||
| 171 | TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"` | 171 | TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" toml:"tab_mode" json:"tab_mode"` | ||
| 172 | - | // Tabs holds classic browser tab shortcuts. | 172 | + | // Shortcuts holds global (non-modal) keyboard shortcuts. |
| 173 | - | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" toml:"tabs" json:"tabs"` | 173 | + | Shortcuts GlobalShortcutsConfig `mapstructure:"shortcuts" yaml:"shortcuts" toml:"shortcuts" json:"shortcuts"` |
| 174 | // TabBarPosition determines tab bar placement: "top" or "bottom". | 174 | // TabBarPosition determines tab bar placement: "top" or "bottom". | ||
| 175 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | 175 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | ||
| 176 | // HideTabBarWhenSingleTab hides the tab bar when only one tab exists. | 176 | // HideTabBarWhenSingleTab hides the tab bar when only one tab exists. | ||
| @@ -219,10 +219,9 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string { | |||||
| 219 | return keyToAction | 219 | return keyToAction | ||
| 220 | } | 220 | } | ||
| 221 | 221 | ||||
| 222 | - | // TabKeyConfig defines Zellij-inspired tab shortcuts. | 222 | + | // GlobalShortcutsConfig defines global shortcuts (always active, not modal). |
| 223 | - | type TabKeyConfig struct { | 223 | + | type GlobalShortcutsConfig struct { |
| 224 | - | NewTab string `mapstructure:"new_tab" yaml:"new_tab" toml:"new_tab" json:"new_tab"` | 224 | + | ClosePane string `mapstructure:"close_pane" yaml:"close_pane" toml:"close_pane" json:"close_pane"` |
| 225 | - | CloseTab string `mapstructure:"close_tab" yaml:"close_tab" toml:"close_tab" json:"close_tab"` | |||
| 226 | NextTab string `mapstructure:"next_tab" yaml:"next_tab" toml:"next_tab" json:"next_tab"` | 225 | NextTab string `mapstructure:"next_tab" yaml:"next_tab" toml:"next_tab" json:"next_tab"` | ||
| 227 | PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" toml:"previous_tab" json:"previous_tab"` | 226 | PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" toml:"previous_tab" json:"previous_tab"` | ||
| 228 | } | 227 | } | ||
M internal/ui/dispatcher/keyboard.go +2 -0
| @@ -86,6 +86,8 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action) | |||
| 86 | 86 | return d.tabCoord.SwitchByIndex(ctx, 8) | |
| 87 | 87 | case input.ActionSwitchTabIndex10: | |
| 88 | 88 | return d.tabCoord.SwitchByIndex(ctx, 9) | |
| 89 | + | case input.ActionRenameTab: | |
| 90 | + | log.Debug().Msg("rename tab action (not yet implemented)") | |
| 89 | 91 | ||
| 90 | 92 | // Pane actions | |
| 91 | 93 | case input.ActionSplitRight: | |
| @@ -86,6 +86,8 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action) | |||||
| 86 | return d.tabCoord.SwitchByIndex(ctx, 8) | 86 | return d.tabCoord.SwitchByIndex(ctx, 8) | ||
| 87 | case input.ActionSwitchTabIndex10: | 87 | case input.ActionSwitchTabIndex10: | ||
| 88 | return d.tabCoord.SwitchByIndex(ctx, 9) | 88 | return d.tabCoord.SwitchByIndex(ctx, 9) | ||
| 89 | + | case input.ActionRenameTab: | |||
| 90 | + | log.Debug().Msg("rename tab action (not yet implemented)") | |||
| 89 | 91 | ||||
| 90 | // Pane actions | 92 | // Pane actions | ||
| 91 | case input.ActionSplitRight: | 93 | case input.ActionSplitRight: | ||
M internal/ui/input/shortcuts.go +4 -4
| @@ -161,13 +161,13 @@ func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Work | |||
| 161 | 161 | // This follows Zellij-style modal keyboard interface. | |
| 162 | 162 | // | |
| 163 | 163 | // However, these standard browser shortcuts ARE global: | |
| 164 | - | if binding, ok := ParseKeyString(cfg.Tabs.CloseTab); ok { | |
| 165 | - | s.Global[binding] = ActionCloseTab // Ctrl+W | |
| 164 | + | if binding, ok := ParseKeyString(cfg.Shortcuts.ClosePane); ok { | |
| 165 | + | s.Global[binding] = ActionClosePane // Ctrl+W closes active pane | |
| 166 | 166 | } | |
| 167 | - | if binding, ok := ParseKeyString(cfg.Tabs.NextTab); ok { | |
| 167 | + | if binding, ok := ParseKeyString(cfg.Shortcuts.NextTab); ok { | |
| 168 | 168 | s.Global[binding] = ActionNextTab // Ctrl+Tab | |
| 169 | 169 | } | |
| 170 | - | if binding, ok := ParseKeyString(cfg.Tabs.PreviousTab); ok { | |
| 170 | + | if binding, ok := ParseKeyString(cfg.Shortcuts.PreviousTab); ok { | |
| 171 | 171 | s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab | |
| 172 | 172 | } | |
| 173 | 173 | ||
| @@ -161,13 +161,13 @@ func (s *ShortcutSet) buildGlobalShortcuts(ctx context.Context, cfg *config.Work | |||||
| 161 | // This follows Zellij-style modal keyboard interface. | 161 | // This follows Zellij-style modal keyboard interface. | ||
| 162 | // | 162 | // | ||
| 163 | // However, these standard browser shortcuts ARE global: | 163 | // However, these standard browser shortcuts ARE global: | ||
| 164 | - | if binding, ok := ParseKeyString(cfg.Tabs.CloseTab); ok { | 164 | + | if binding, ok := ParseKeyString(cfg.Shortcuts.ClosePane); ok { |
| 165 | - | s.Global[binding] = ActionCloseTab // Ctrl+W | 165 | + | s.Global[binding] = ActionClosePane // Ctrl+W closes active pane |
| 166 | } | 166 | } | ||
| 167 | - | if binding, ok := ParseKeyString(cfg.Tabs.NextTab); ok { | 167 | + | if binding, ok := ParseKeyString(cfg.Shortcuts.NextTab); ok { |
| 168 | s.Global[binding] = ActionNextTab // Ctrl+Tab | 168 | s.Global[binding] = ActionNextTab // Ctrl+Tab | ||
| 169 | } | 169 | } | ||
| 170 | - | if binding, ok := ParseKeyString(cfg.Tabs.PreviousTab); ok { | 170 | + | if binding, ok := ParseKeyString(cfg.Shortcuts.PreviousTab); ok { |
| 171 | s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab | 171 | s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab | ||
| 172 | } | 172 | } | ||
| 173 | 173 | ||||