detached 56c25f7

feat(config): add tab system configuration

- Add TabModeConfig for modal tab management (Ctrl+T activation)
- Add TabKeyConfig for quick tab actions (new, close, next, previous)
- Add TabBarPosition setting (top/bottom)
- Add TabModeBorderColor distinct from PaneModeBorderColor
- Add validation for tab bar position and tab mode actions
- Add default timeout of 3000ms for tab mode
- Add default key bindings: n/c=new, x=close, l/h=navigate, r=rename
commit
56c25f7f5777ded0f43efc83fa32a1a988a1f5a2
author
bnema · · 2025-11-17 19:40 UTC
parent
4e65755
signature
unsigned
3 files · +85 -4
M internal/config/config.go +31 -1
@@ -218,8 +218,12 @@ type DebugConfig struct {
218218 type WorkspaceConfig struct {
219219 // PaneMode defines modal pane behaviour and bindings.
220220 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" json:"pane_mode"`
221+ // TabMode defines modal tab behaviour and bindings (Alt+T).
222+ TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" json:"tab_mode"`
221223 // Tabs holds classic browser tab shortcuts.
222224 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"`
225+ // TabBarPosition determines tab bar placement: "top" or "bottom".
226+ TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" json:"tab_bar_position"`
223227 // Popups configures default popup placement rules.
224228 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"`
225229 // Styling configures workspace visual appearance.
@@ -245,6 +249,25 @@ func (p *PaneModeConfig) GetKeyBindings() map[string]string {
245249 return keyToAction
246250 }
247251
252+// TabModeConfig defines modal behaviour for tab management (Zellij-style).
253+type TabModeConfig struct {
254+ ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" json:"activation_shortcut"`
255+ TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" json:"timeout_ms"`
256+ Actions map[string][]string `mapstructure:"actions" yaml:"actions" json:"actions"`
257+}
258+
259+// GetKeyBindings returns an inverted map for O(1) key→action lookup.
260+// This is built from the action→keys structure in the config.
261+func (t *TabModeConfig) GetKeyBindings() map[string]string {
262+ keyToAction := make(map[string]string)
263+ for action, keys := range t.Actions {
264+ for _, key := range keys {
265+ keyToAction[key] = action
266+ }
267+ }
268+ return keyToAction
269+}
270+
248271 // TabKeyConfig defines Zellij-inspired tab shortcuts.
249272 type TabKeyConfig struct {
250273 NewTab string `mapstructure:"new_tab" yaml:"new_tab" json:"new_tab"`
@@ -307,8 +330,11 @@ type WorkspaceStylingConfig struct {
307330 // ShowStackedTitleBorder enables the separator line below stacked pane titles
308331 ShowStackedTitleBorder bool `mapstructure:"show_stacked_title_border" yaml:"show_stacked_title_border" json:"show_stacked_title_border"`
309332 // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable)
310- // Defaults to "#FFA500" (orange) if not set
333+ // Defaults to "#4A90E2" (blue) if not set
311334 PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"`
335+ // TabModeBorderColor for the tab mode indicator border (CSS color value or theme variable)
336+ // Defaults to "#FFA500" (orange) if not set - MUST be different from PaneModeBorderColor
337+ TabModeBorderColor string `mapstructure:"tab_mode_border_color" yaml:"tab_mode_border_color" json:"tab_mode_border_color"`
312338 // TransitionDuration in milliseconds for border animations
313339 TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`
314340 // BorderRadius in pixels for pane border corners
@@ -693,10 +719,14 @@ func (m *Manager) setDefaults() {
693719 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
694720 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
695721 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
722+ m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
723+ m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
724+ m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
696725 m.viper.SetDefault("workspace.tabs.new_tab", defaults.Workspace.Tabs.NewTab)
697726 m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab)
698727 m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab)
699728 m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab)
729+ m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
700730 m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
701731 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
702732 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
@@ -218,8 +218,12 @@ type DebugConfig struct {
218 type WorkspaceConfig struct {218 type WorkspaceConfig struct {
219 // PaneMode defines modal pane behaviour and bindings.219 // PaneMode defines modal pane behaviour and bindings.
220 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" json:"pane_mode"`220 PaneMode PaneModeConfig `mapstructure:"pane_mode" yaml:"pane_mode" json:"pane_mode"`
221+ // TabMode defines modal tab behaviour and bindings (Alt+T).
222+ TabMode TabModeConfig `mapstructure:"tab_mode" yaml:"tab_mode" json:"tab_mode"`
221 // Tabs holds classic browser tab shortcuts.223 // Tabs holds classic browser tab shortcuts.
222 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"`224 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" json:"tabs"`
225+ // TabBarPosition determines tab bar placement: "top" or "bottom".
226+ TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" json:"tab_bar_position"`
223 // Popups configures default popup placement rules.227 // Popups configures default popup placement rules.
224 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"`228 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"`
225 // Styling configures workspace visual appearance.229 // Styling configures workspace visual appearance.
@@ -245,6 +249,25 @@ func (p *PaneModeConfig) GetKeyBindings() map[string]string {
245 return keyToAction249 return keyToAction
246 }250 }
247 251
252+// TabModeConfig defines modal behaviour for tab management (Zellij-style).
253+type TabModeConfig struct {
254+ ActivationShortcut string `mapstructure:"activation_shortcut" yaml:"activation_shortcut" json:"activation_shortcut"`
255+ TimeoutMilliseconds int `mapstructure:"timeout_ms" yaml:"timeout_ms" json:"timeout_ms"`
256+ Actions map[string][]string `mapstructure:"actions" yaml:"actions" json:"actions"`
257+}
258+
259+// GetKeyBindings returns an inverted map for O(1) key→action lookup.
260+// This is built from the action→keys structure in the config.
261+func (t *TabModeConfig) GetKeyBindings() map[string]string {
262+ keyToAction := make(map[string]string)
263+ for action, keys := range t.Actions {
264+ for _, key := range keys {
265+ keyToAction[key] = action
266+ }
267+ }
268+ return keyToAction
269+}
270+
248 // TabKeyConfig defines Zellij-inspired tab shortcuts.271 // TabKeyConfig defines Zellij-inspired tab shortcuts.
249 type TabKeyConfig struct {272 type TabKeyConfig struct {
250 NewTab string `mapstructure:"new_tab" yaml:"new_tab" json:"new_tab"`273 NewTab string `mapstructure:"new_tab" yaml:"new_tab" json:"new_tab"`
@@ -307,8 +330,11 @@ type WorkspaceStylingConfig struct {
307 // ShowStackedTitleBorder enables the separator line below stacked pane titles330 // ShowStackedTitleBorder enables the separator line below stacked pane titles
308 ShowStackedTitleBorder bool `mapstructure:"show_stacked_title_border" yaml:"show_stacked_title_border" json:"show_stacked_title_border"`331 ShowStackedTitleBorder bool `mapstructure:"show_stacked_title_border" yaml:"show_stacked_title_border" json:"show_stacked_title_border"`
309 // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable)332 // PaneModeBorderColor for the pane mode indicator border (CSS color value or theme variable)
310- // Defaults to "#FFA500" (orange) if not set333+ // Defaults to "#4A90E2" (blue) if not set
311 PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"`334 PaneModeBorderColor string `mapstructure:"pane_mode_border_color" yaml:"pane_mode_border_color" json:"pane_mode_border_color"`
335+ // TabModeBorderColor for the tab mode indicator border (CSS color value or theme variable)
336+ // Defaults to "#FFA500" (orange) if not set - MUST be different from PaneModeBorderColor
337+ TabModeBorderColor string `mapstructure:"tab_mode_border_color" yaml:"tab_mode_border_color" json:"tab_mode_border_color"`
312 // TransitionDuration in milliseconds for border animations338 // TransitionDuration in milliseconds for border animations
313 TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`339 TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"`
314 // BorderRadius in pixels for pane border corners340 // BorderRadius in pixels for pane border corners
@@ -693,10 +719,14 @@ func (m *Manager) setDefaults() {
693 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)719 m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut)
694 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)720 m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds)
695 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)721 m.viper.SetDefault("workspace.pane_mode.actions", defaults.Workspace.PaneMode.Actions)
722+ m.viper.SetDefault("workspace.tab_mode.activation_shortcut", defaults.Workspace.TabMode.ActivationShortcut)
723+ m.viper.SetDefault("workspace.tab_mode.timeout_ms", defaults.Workspace.TabMode.TimeoutMilliseconds)
724+ m.viper.SetDefault("workspace.tab_mode.actions", defaults.Workspace.TabMode.Actions)
696 m.viper.SetDefault("workspace.tabs.new_tab", defaults.Workspace.Tabs.NewTab)725 m.viper.SetDefault("workspace.tabs.new_tab", defaults.Workspace.Tabs.NewTab)
697 m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab)726 m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab)
698 m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab)727 m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab)
699 m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab)728 m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab)
729+ m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition)
700 m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))730 m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior))
701 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)731 m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement)
702 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)732 m.viper.SetDefault("workspace.popups.open_in_new_pane", defaults.Workspace.Popups.OpenInNewPane)
M internal/config/defaults.go +20 -1
@@ -21,6 +21,9 @@ const (
2121 // Workspace defaults
2222 defaultPaneActivationShortcut = "ctrl+p"
2323 defaultPaneTimeoutMilliseconds = 3000
24+ defaultTabActivationShortcut = "ctrl+t"
25+ defaultTabTimeoutMilliseconds = 3000
26+ defaultTabBarPosition = "bottom"
2427 defaultPopupPlacement = "right"
2528
2629 // Workspace styling defaults
@@ -29,7 +32,8 @@ const (
2932 defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift
3033 defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable
3134 defaultShowStackedTitleBorder = false // Hidden by default
32- defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator
35+ defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator
36+ defaultTabModeBorderColor = "#FFA500" // Orange for tab mode indicator (distinct from pane mode)
3337 defaultTransitionDuration = 120
3438 defaultBorderRadius = 0
3539 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)
@@ -160,12 +164,26 @@ func DefaultConfig() *Config {
160164 "cancel": {"escape"},
161165 },
162166 },
167+ TabMode: TabModeConfig{
168+ ActivationShortcut: defaultTabActivationShortcut,
169+ TimeoutMilliseconds: defaultTabTimeoutMilliseconds,
170+ Actions: map[string][]string{
171+ "new-tab": {"n", "c"},
172+ "close-tab": {"x"},
173+ "next-tab": {"l", "tab"},
174+ "previous-tab": {"h", "shift+tab"},
175+ "rename-tab": {"r"},
176+ "confirm": {"enter"},
177+ "cancel": {"escape"},
178+ },
179+ },
163180 Tabs: TabKeyConfig{
164181 NewTab: "ctrl+t",
165182 CloseTab: "ctrl+w",
166183 NextTab: "ctrl+tab",
167184 PreviousTab: "ctrl+shift+tab",
168185 },
186+ TabBarPosition: defaultTabBarPosition,
169187 Popups: PopupBehaviorConfig{
170188 Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes
171189 Placement: defaultPopupPlacement,
@@ -182,6 +200,7 @@ func DefaultConfig() *Config {
182200 InactiveBorderColor: defaultInactiveBorderColor,
183201 ShowStackedTitleBorder: defaultShowStackedTitleBorder,
184202 PaneModeBorderColor: defaultPaneModeBorderColor,
203+ TabModeBorderColor: defaultTabModeBorderColor,
185204 TransitionDuration: defaultTransitionDuration,
186205 BorderRadius: defaultBorderRadius,
187206 UIScale: defaultUIScale,
@@ -21,6 +21,9 @@ const (
21 // Workspace defaults21 // Workspace defaults
22 defaultPaneActivationShortcut = "ctrl+p"22 defaultPaneActivationShortcut = "ctrl+p"
23 defaultPaneTimeoutMilliseconds = 300023 defaultPaneTimeoutMilliseconds = 3000
24+ defaultTabActivationShortcut = "ctrl+t"
25+ defaultTabTimeoutMilliseconds = 3000
26+ defaultTabBarPosition = "bottom"
24 defaultPopupPlacement = "right"27 defaultPopupPlacement = "right"
25 28
26 // Workspace styling defaults29 // Workspace styling defaults
@@ -29,7 +32,8 @@ const (
29 defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift32 defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift
30 defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable33 defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable
31 defaultShowStackedTitleBorder = false // Hidden by default34 defaultShowStackedTitleBorder = false // Hidden by default
32- defaultPaneModeBorderColor = "#FFA500" // Orange for pane mode indicator35+ defaultPaneModeBorderColor = "#4A90E2" // Blue for pane mode indicator
36+ defaultTabModeBorderColor = "#FFA500" // Orange for tab mode indicator (distinct from pane mode)
33 defaultTransitionDuration = 12037 defaultTransitionDuration = 120
34 defaultBorderRadius = 038 defaultBorderRadius = 0
35 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)39 defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%)
@@ -160,12 +164,26 @@ func DefaultConfig() *Config {
160 "cancel": {"escape"},164 "cancel": {"escape"},
161 },165 },
162 },166 },
167+ TabMode: TabModeConfig{
168+ ActivationShortcut: defaultTabActivationShortcut,
169+ TimeoutMilliseconds: defaultTabTimeoutMilliseconds,
170+ Actions: map[string][]string{
171+ "new-tab": {"n", "c"},
172+ "close-tab": {"x"},
173+ "next-tab": {"l", "tab"},
174+ "previous-tab": {"h", "shift+tab"},
175+ "rename-tab": {"r"},
176+ "confirm": {"enter"},
177+ "cancel": {"escape"},
178+ },
179+ },
163 Tabs: TabKeyConfig{180 Tabs: TabKeyConfig{
164 NewTab: "ctrl+t",181 NewTab: "ctrl+t",
165 CloseTab: "ctrl+w",182 CloseTab: "ctrl+w",
166 NextTab: "ctrl+tab",183 NextTab: "ctrl+tab",
167 PreviousTab: "ctrl+shift+tab",184 PreviousTab: "ctrl+shift+tab",
168 },185 },
186+ TabBarPosition: defaultTabBarPosition,
169 Popups: PopupBehaviorConfig{187 Popups: PopupBehaviorConfig{
170 Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes188 Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes
171 Placement: defaultPopupPlacement,189 Placement: defaultPopupPlacement,
@@ -182,6 +200,7 @@ func DefaultConfig() *Config {
182 InactiveBorderColor: defaultInactiveBorderColor,200 InactiveBorderColor: defaultInactiveBorderColor,
183 ShowStackedTitleBorder: defaultShowStackedTitleBorder,201 ShowStackedTitleBorder: defaultShowStackedTitleBorder,
184 PaneModeBorderColor: defaultPaneModeBorderColor,202 PaneModeBorderColor: defaultPaneModeBorderColor,
203+ TabModeBorderColor: defaultTabModeBorderColor,
185 TransitionDuration: defaultTransitionDuration,204 TransitionDuration: defaultTransitionDuration,
186 BorderRadius: defaultBorderRadius,205 BorderRadius: defaultBorderRadius,
187 UIScale: defaultUIScale,206 UIScale: defaultUIScale,
M internal/config/validation.go +34 -2
@@ -93,7 +93,7 @@ func validateConfig(config *Config) error {
9393 validationErrors = append(validationErrors, "workspace.pane_mode.actions cannot be empty")
9494 }
9595
96- // Check for duplicate keys and empty action key lists
96+ // Check for duplicate keys and empty action key lists in pane mode
9797 seenKeys := make(map[string]string)
9898 for action, keys := range config.Workspace.PaneMode.Actions {
9999 if len(keys) == 0 {
@@ -101,12 +101,44 @@ func validateConfig(config *Config) error {
101101 }
102102 for _, key := range keys {
103103 if existingAction, exists := seenKeys[key]; exists {
104- validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in actions '%s' and '%s'", key, existingAction, action))
104+ validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in pane_mode actions '%s' and '%s'", key, existingAction, action))
105105 }
106106 seenKeys[key] = action
107107 }
108108 }
109109
110+ // Validate tab bar position
111+ switch config.Workspace.TabBarPosition {
112+ case "top", "bottom":
113+ // Valid
114+ default:
115+ validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_bar_position must be 'top' or 'bottom' (got: %s)", config.Workspace.TabBarPosition))
116+ }
117+
118+ // Validate tab mode timeout
119+ if config.Workspace.TabMode.TimeoutMilliseconds < 0 {
120+ validationErrors = append(validationErrors, "workspace.tab_mode.timeout_ms must be non-negative")
121+ }
122+
123+ // Validate tab mode actions
124+ if len(config.Workspace.TabMode.Actions) == 0 {
125+ validationErrors = append(validationErrors, "workspace.tab_mode.actions cannot be empty")
126+ }
127+
128+ // Check for duplicate keys and empty action key lists in tab mode
129+ tabSeenKeys := make(map[string]string)
130+ for action, keys := range config.Workspace.TabMode.Actions {
131+ if len(keys) == 0 {
132+ validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action))
133+ }
134+ for _, key := range keys {
135+ if existingAction, exists := tabSeenKeys[key]; exists {
136+ validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in tab_mode actions '%s' and '%s'", key, existingAction, action))
137+ }
138+ tabSeenKeys[key] = action
139+ }
140+ }
141+
110142 // Validate codec buffer sizes
111143 if config.CodecPreferences.VideoBufferSizeMB < 0 {
112144 validationErrors = append(validationErrors, "codec_preferences.video_buffer_size_mb must be non-negative")
@@ -93,7 +93,7 @@ func validateConfig(config *Config) error {
93 validationErrors = append(validationErrors, "workspace.pane_mode.actions cannot be empty")93 validationErrors = append(validationErrors, "workspace.pane_mode.actions cannot be empty")
94 }94 }
95 95
96- // Check for duplicate keys and empty action key lists96+ // Check for duplicate keys and empty action key lists in pane mode
97 seenKeys := make(map[string]string)97 seenKeys := make(map[string]string)
98 for action, keys := range config.Workspace.PaneMode.Actions {98 for action, keys := range config.Workspace.PaneMode.Actions {
99 if len(keys) == 0 {99 if len(keys) == 0 {
@@ -101,12 +101,44 @@ func validateConfig(config *Config) error {
101 }101 }
102 for _, key := range keys {102 for _, key := range keys {
103 if existingAction, exists := seenKeys[key]; exists {103 if existingAction, exists := seenKeys[key]; exists {
104- validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in actions '%s' and '%s'", key, existingAction, action))104+ validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in pane_mode actions '%s' and '%s'", key, existingAction, action))
105 }105 }
106 seenKeys[key] = action106 seenKeys[key] = action
107 }107 }
108 }108 }
109 109
110+ // Validate tab bar position
111+ switch config.Workspace.TabBarPosition {
112+ case "top", "bottom":
113+ // Valid
114+ default:
115+ validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_bar_position must be 'top' or 'bottom' (got: %s)", config.Workspace.TabBarPosition))
116+ }
117+
118+ // Validate tab mode timeout
119+ if config.Workspace.TabMode.TimeoutMilliseconds < 0 {
120+ validationErrors = append(validationErrors, "workspace.tab_mode.timeout_ms must be non-negative")
121+ }
122+
123+ // Validate tab mode actions
124+ if len(config.Workspace.TabMode.Actions) == 0 {
125+ validationErrors = append(validationErrors, "workspace.tab_mode.actions cannot be empty")
126+ }
127+
128+ // Check for duplicate keys and empty action key lists in tab mode
129+ tabSeenKeys := make(map[string]string)
130+ for action, keys := range config.Workspace.TabMode.Actions {
131+ if len(keys) == 0 {
132+ validationErrors = append(validationErrors, fmt.Sprintf("workspace.tab_mode.actions.%s must have at least one key binding", action))
133+ }
134+ for _, key := range keys {
135+ if existingAction, exists := tabSeenKeys[key]; exists {
136+ validationErrors = append(validationErrors, fmt.Sprintf("duplicate key binding '%s' found in tab_mode actions '%s' and '%s'", key, existingAction, action))
137+ }
138+ tabSeenKeys[key] = action
139+ }
140+ }
141+
110 // Validate codec buffer sizes142 // Validate codec buffer sizes
111 if config.CodecPreferences.VideoBufferSizeMB < 0 {143 if config.CodecPreferences.VideoBufferSizeMB < 0 {
112 validationErrors = append(validationErrors, "codec_preferences.video_buffer_size_mb must be non-negative")144 validationErrors = append(validationErrors, "codec_preferences.video_buffer_size_mb must be non-negative")

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