detached 46f04d2

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"]
199199 split-left = ["arrowleft", "l"]
200200 split-up = ["arrowup", "u"]
201201 split-down = ["arrowdown", "d"]
202+stack-pane = ["s"]
202203 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"]
203208 confirm = ["enter"]
204209 cancel = ["escape"]
205210 ```
@@ -228,14 +233,15 @@ cancel = ["escape"]
228233
229234 > **Note:** Actions are inverted to key→action map in memory for O(1) lookup performance during navigation.
230235
231-### Tab Shortcuts
236+### Global Shortcuts
232237
233238 | Key | Type | Default | Description |
234239 |-----|------|---------|-------------|
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.
239245
240246 ### Popup Behavior
241247
@@ -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 Shortcuts236+### 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 Behavior246 ### Popup Behavior
241 247
M internal/infrastructure/config/defaults.go +6 -3
@@ -126,6 +126,10 @@ func DefaultConfig() *Config {
126126 "split-down": {"arrowdown", "d"},
127127 "stack-pane": {"s"},
128128 "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"},
129133 "confirm": {"enter"},
130134 "cancel": {"escape"},
131135 },
@@ -143,9 +147,8 @@ func DefaultConfig() *Config {
143147 "cancel": {"escape"},
144148 },
145149 },
146- Tabs: TabKeyConfig{
147- NewTab: "ctrl+t",
148- CloseTab: "ctrl+w",
150+ Shortcuts: GlobalShortcutsConfig{
151+ ClosePane: "ctrl+w",
149152 NextTab: "ctrl+tab",
150153 PreviousTab: "ctrl+shift+tab",
151154 },
@@ -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() {
262262 m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
263263 m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
264264 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)
269268 m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
270269 m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
271270 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 {
169169 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" toml:"pane_mode" json:"pane_mode"`
170170 // TabMode defines modal tab behaviour and bindings (Alt+T).
171171 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"`
174174 // TabBarPosition determines tab bar placement: "top" or "bottom".
175175 TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"`
176176 // HideTabBarWhenSingleTab hides the tab bar when only one tab exists.
@@ -219,10 +219,9 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string {
219219 return keyToAction
220220 }
221221
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"`
226225 NextTab string `mapstructure:"next_tab" yaml:"next_tab" toml:"next_tab" json:"next_tab"`
227226 PreviousTab string `mapstructure:"previous_tab" yaml:"previous_tab" toml:"previous_tab" json:"previous_tab"`
228227 }
@@ -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 keyToAction219 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)
8686 return d.tabCoord.SwitchByIndex(ctx, 8)
8787 case input.ActionSwitchTabIndex10:
8888 return d.tabCoord.SwitchByIndex(ctx, 9)
89+ case input.ActionRenameTab:
90+ log.Debug().Msg("rename tab action (not yet implemented)")
8991
9092 // Pane actions
9193 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 actions92 // 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
161161 // This follows Zellij-style modal keyboard interface.
162162 //
163163 // 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
166166 }
167- if binding, ok := ParseKeyString(cfg.Tabs.NextTab); ok {
167+ if binding, ok := ParseKeyString(cfg.Shortcuts.NextTab); ok {
168168 s.Global[binding] = ActionNextTab // Ctrl+Tab
169169 }
170- if binding, ok := ParseKeyString(cfg.Tabs.PreviousTab); ok {
170+ if binding, ok := ParseKeyString(cfg.Shortcuts.PreviousTab); ok {
171171 s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab
172172 }
173173
@@ -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+W165+ 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+Tab168 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+Tab171 s.Global[binding] = ActionPreviousTab // Ctrl+Shift+Tab
172 }172 }
173 173

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close