detached d701001

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
161161 PaneMode: PaneModeConfig{
162162 ActivationShortcut: defaultPaneActivationShortcut,
163163 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"},
173173
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"},
178178
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"},
185185 },
186186 },
187187 TabMode: TabModeConfig{
188188 ActivationShortcut: defaultTabActivationShortcut,
189189 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"},
198198 },
199199 },
200200 ResizeMode: ResizeModeConfig{
@@ -202,30 +202,31 @@ func DefaultConfig() *Config {
202202 TimeoutMilliseconds: defaultResizeTimeoutMilliseconds,
203203 StepPercent: defaultResizeStepPercent,
204204 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"},
218218 },
219219 },
220220 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+ },
229230 },
230231 TabBarPosition: defaultTabBarPosition,
231232 HideTabBarWhenSingleTab: true,
@@ -269,10 +270,10 @@ func DefaultConfig() *Config {
269270 SessionMode: SessionModeConfig{
270271 ActivationShortcut: defaultSessionActivationShortcut,
271272 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"},
276277 },
277278 },
278279 },
@@ -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 {
289289 return nil
290290 }
291291
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+
292335 // GetConfigFile returns the path to the configuration file being used.
293336 func (m *Manager) GetConfigFile() string {
294337 return m.viper.ConfigFileUsed()
@@ -443,13 +486,7 @@ func (m *Manager) setWorkspaceDefaults(defaults *Config) {
443486 m.viper.SetDefault("workspace.resize_mode.step_percent", defaults.Workspace.ResizeMode.StepPercent)
444487 m.viper.SetDefault("workspace.resize_mode.min_pane_percent", defaults.Workspace.ResizeMode.MinPanePercent)
445488 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)
453490 m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
454491 m.viper.SetDefault("workspace.hide_tab_bar_when_single_tab", defaults.Workspace.HideTabBarWhenSingleTab)
455492 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 nil289 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 {
410410 Styling WorkspaceStylingConfig `mapstructure:"styling" yaml:"styling" toml:"styling" json:"styling"`
411411 }
412412
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+
413419 // PaneModeConfig defines modal behavior for pane management.
414420 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"`
418424 }
419425
420426 // GetKeyBindings returns an inverted map for O(1) key→action lookup.
421427 // This is built from the action→keys structure in the config.
422428 func (p *PaneModeConfig) GetKeyBindings() map[string]string {
423429 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 {
426432 keyToAction[key] = action
427433 }
428434 }
@@ -431,17 +437,17 @@ func (p *PaneModeConfig) GetKeyBindings() map[string]string {
431437
432438 // TabModeConfig defines modal behavior for tab management (Zellij-style).
433439 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"`
437443 }
438444
439445 // GetKeyBindings returns an inverted map for O(1) key→action lookup.
440446 // This is built from the action→keys structure in the config.
441447 func (t *TabModeConfig) GetKeyBindings() map[string]string {
442448 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 {
445451 keyToAction[key] = action
446452 }
447453 }
@@ -450,19 +456,19 @@ func (t *TabModeConfig) GetKeyBindings() map[string]string {
450456
451457 // ResizeModeConfig defines modal behavior for resizing panes (Zellij-style).
452458 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"`
458464 }
459465
460466 // GetKeyBindings returns an inverted map for O(1) key→action lookup.
461467 // This is built from the action→keys structure in the config.
462468 func (r *ResizeModeConfig) GetKeyBindings() map[string]string {
463469 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 {
466472 keyToAction[key] = action
467473 }
468474 }
@@ -471,14 +477,18 @@ func (r *ResizeModeConfig) GetKeyBindings() map[string]string {
471477
472478 // GlobalShortcutsConfig defines global shortcuts (always active, not modal).
473479 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+}
477482
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
482492 }
483493
484494 // PopupBehavior defines how popup windows should be opened
@@ -574,17 +584,17 @@ type SessionConfig struct {
574584
575585 // SessionModeConfig defines modal behavior for session management.
576586 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"`
580590 }
581591
582592 // GetKeyBindings returns an inverted map for O(1) key→action lookup.
583593 // This is built from the action→keys structure in the config.
584594 func (s *SessionModeConfig) GetKeyBindings() map[string]string {
585595 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 {
588598 keyToAction[key] = action
589599 }
590600 }
@@ -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 line421+ 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] = action432 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 line440+ 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] = action451 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 line459+ 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 line462+ 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] = action472 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 line483+// 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 line484+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 line485+ 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 line486+ 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 opened494 // 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 line587+ 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] = action598 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
445445 Description: "Key bindings for resize mode actions",
446446 Section: SectionWorkspace,
447447 },
448- // Global shortcuts
448+ // Global shortcuts - managed via dedicated keybindings UI
449449 {
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)",
496454 Section: SectionWorkspace,
497455 },
498456 // 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 shortcuts448+ // 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 // Popups456 // Popups
M internal/infrastructure/config/validation.go +6 -6
@@ -166,11 +166,11 @@ func validatePaneMode(config *Config) []string {
166166 }
167167
168168 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 {
171171 validationErrors = append(validationErrors, fmt.Sprintf("workspace.pane_mode.actions.%s must have at least one key binding", action))
172172 }
173- for _, key := range keys {
173+ for _, key := range binding.Keys {
174174 if existingAction, exists := seenKeys[key]; exists {
175175 validationErrors = append(validationErrors, fmt.Sprintf(
176176 "duplicate key binding '%s' found in pane_mode actions '%s' and '%s'",
@@ -204,11 +204,11 @@ func validateTabMode(config *Config) []string {
204204 }
205205
206206 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 {
209209 validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action))
210210 }
211- for _, key := range keys {
211+ for _, key := range binding.Keys {
212212 if existingAction, exists := tabSeenKeys[key]; exists {
213213 validationErrors = append(validationErrors, fmt.Sprintf(
214214 "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(
9494 Msg("registered global shortcut")
9595
9696 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]
99107 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")
102118 }
103- h.registerShortcut(binding.Keyval, gdk.ModifierType(binding.Modifiers), action)
104- log.Trace().Str("shortcut", key).Str("action", string(action)).Msg("registered global shortcut")
105119 }
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)
111120 }
112121
113122 // 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- return109+ }
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 window122 // Attach to window
M internal/ui/input/shortcuts.go +19 -19
@@ -325,27 +325,27 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *config.WorkspaceConfig) {
325325 // This follows Zellij-style modal keyboard interface.
326326 //
327327 // 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,
336337 }
337338
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+ }
349349 }
350350 }
351351
@@ -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 pane329+ 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+Tab332+ "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+Tab335+ "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] = ActionConsumeOrExpelLeft340+ action, ok := actionMap[actionName]
340- }341+ if !ok {
341- if binding, ok := ParseKeyString(cfg.Shortcuts.ConsumeOrExpelRight); ok {342+ continue
342- s.Global[binding] = ActionConsumeOrExpelRight343+ }
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] = ActionConsumeOrExpelUp346+ 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

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