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 { | |||
| 218 | 218 | type WorkspaceConfig struct { | |
| 219 | 219 | // PaneMode defines modal pane behaviour and bindings. | |
| 220 | 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 | 223 | // Tabs holds classic browser tab shortcuts. | |
| 222 | 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 | 227 | // Popups configures default popup placement rules. | |
| 224 | 228 | Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" json:"popups"` | |
| 225 | 229 | // Styling configures workspace visual appearance. | |
| @@ -245,6 +249,25 @@ func (p *PaneModeConfig) GetKeyBindings() map[string]string { | |||
| 245 | 249 | 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 | 271 | // TabKeyConfig defines Zellij-inspired tab shortcuts. | |
| 249 | 272 | type TabKeyConfig struct { | |
| 250 | 273 | NewTab string `mapstructure:"new_tab" yaml:"new_tab" json:"new_tab"` | |
| @@ -307,8 +330,11 @@ type WorkspaceStylingConfig struct { | |||
| 307 | 330 | // ShowStackedTitleBorder enables the separator line below stacked pane titles | |
| 308 | 331 | ShowStackedTitleBorder bool `mapstructure:"show_stacked_title_border" yaml:"show_stacked_title_border" json:"show_stacked_title_border"` | |
| 309 | 332 | // 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 | |
| 311 | 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 | 338 | // TransitionDuration in milliseconds for border animations | |
| 313 | 339 | TransitionDuration int `mapstructure:"transition_duration" yaml:"transition_duration" json:"transition_duration"` | |
| 314 | 340 | // BorderRadius in pixels for pane border corners | |
| @@ -693,10 +719,14 @@ func (m *Manager) setDefaults() { | |||
| 693 | 719 | m.viper.SetDefault("workspace.pane_mode.activation_shortcut", defaults.Workspace.PaneMode.ActivationShortcut) | |
| 694 | 720 | m.viper.SetDefault("workspace.pane_mode.timeout_ms", defaults.Workspace.PaneMode.TimeoutMilliseconds) | |
| 695 | 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 | 725 | m.viper.SetDefault("workspace.tabs.new_tab", defaults.Workspace.Tabs.NewTab) | |
| 697 | 726 | m.viper.SetDefault("workspace.tabs.close_tab", defaults.Workspace.Tabs.CloseTab) | |
| 698 | 727 | m.viper.SetDefault("workspace.tabs.next_tab", defaults.Workspace.Tabs.NextTab) | |
| 699 | 728 | m.viper.SetDefault("workspace.tabs.previous_tab", defaults.Workspace.Tabs.PreviousTab) | |
| 729 | + | m.viper.SetDefault("workspace.tab_bar_position", defaults.Workspace.TabBarPosition) | |
| 700 | 730 | m.viper.SetDefault("workspace.popups.behavior", string(defaults.Workspace.Popups.Behavior)) | |
| 701 | 731 | m.viper.SetDefault("workspace.popups.placement", defaults.Workspace.Popups.Placement) | |
| 702 | 732 | 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 keyToAction | 249 | 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 titles | 330 | // 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 set | 333 | + | // 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 animations | 338 | // 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 corners | 340 | // 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 ( | |||
| 21 | 21 | // Workspace defaults | |
| 22 | 22 | defaultPaneActivationShortcut = "ctrl+p" | |
| 23 | 23 | defaultPaneTimeoutMilliseconds = 3000 | |
| 24 | + | defaultTabActivationShortcut = "ctrl+t" | |
| 25 | + | defaultTabTimeoutMilliseconds = 3000 | |
| 26 | + | defaultTabBarPosition = "bottom" | |
| 24 | 27 | defaultPopupPlacement = "right" | |
| 25 | 28 | ||
| 26 | 29 | // Workspace styling defaults | |
| @@ -29,7 +32,8 @@ const ( | |||
| 29 | 32 | defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift | |
| 30 | 33 | defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable | |
| 31 | 34 | 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) | |
| 33 | 37 | defaultTransitionDuration = 120 | |
| 34 | 38 | defaultBorderRadius = 0 | |
| 35 | 39 | defaultUIScale = 1.0 // UI scale multiplier (1.0 = 100%, 1.2 = 120%) | |
| @@ -160,12 +164,26 @@ func DefaultConfig() *Config { | |||
| 160 | 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 | 180 | Tabs: TabKeyConfig{ | |
| 164 | 181 | NewTab: "ctrl+t", | |
| 165 | 182 | CloseTab: "ctrl+w", | |
| 166 | 183 | NextTab: "ctrl+tab", | |
| 167 | 184 | PreviousTab: "ctrl+shift+tab", | |
| 168 | 185 | }, | |
| 186 | + | TabBarPosition: defaultTabBarPosition, | |
| 169 | 187 | Popups: PopupBehaviorConfig{ | |
| 170 | 188 | Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes | |
| 171 | 189 | Placement: defaultPopupPlacement, | |
| @@ -182,6 +200,7 @@ func DefaultConfig() *Config { | |||
| 182 | 200 | InactiveBorderColor: defaultInactiveBorderColor, | |
| 183 | 201 | ShowStackedTitleBorder: defaultShowStackedTitleBorder, | |
| 184 | 202 | PaneModeBorderColor: defaultPaneModeBorderColor, | |
| 203 | + | TabModeBorderColor: defaultTabModeBorderColor, | |
| 185 | 204 | TransitionDuration: defaultTransitionDuration, | |
| 186 | 205 | BorderRadius: defaultBorderRadius, | |
| 187 | 206 | UIScale: defaultUIScale, | |
| @@ -21,6 +21,9 @@ const ( | |||||
| 21 | // Workspace defaults | 21 | // Workspace defaults | ||
| 22 | defaultPaneActivationShortcut = "ctrl+p" | 22 | defaultPaneActivationShortcut = "ctrl+p" | ||
| 23 | defaultPaneTimeoutMilliseconds = 3000 | 23 | defaultPaneTimeoutMilliseconds = 3000 | ||
| 24 | + | defaultTabActivationShortcut = "ctrl+t" | |||
| 25 | + | defaultTabTimeoutMilliseconds = 3000 | |||
| 26 | + | defaultTabBarPosition = "bottom" | |||
| 24 | defaultPopupPlacement = "right" | 27 | defaultPopupPlacement = "right" | ||
| 25 | 28 | ||||
| 26 | // Workspace styling defaults | 29 | // Workspace styling defaults | ||
| @@ -29,7 +32,8 @@ const ( | |||||
| 29 | defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift | 32 | defaultInactiveBorderWidth = 1 // Same width as active to prevent layout shift | ||
| 30 | defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable | 33 | defaultInactiveBorderColor = "@theme_unfocused_bg_color" // GTK theme variable | ||
| 31 | defaultShowStackedTitleBorder = false // Hidden by default | 34 | 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) | |||
| 33 | defaultTransitionDuration = 120 | 37 | defaultTransitionDuration = 120 | ||
| 34 | defaultBorderRadius = 0 | 38 | 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 panes | 188 | 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 { | |||
| 93 | 93 | validationErrors = append(validationErrors, "workspace.pane_mode.actions cannot be empty") | |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | - | // Check for duplicate keys and empty action key lists | |
| 96 | + | // Check for duplicate keys and empty action key lists in pane mode | |
| 97 | 97 | seenKeys := make(map[string]string) | |
| 98 | 98 | for action, keys := range config.Workspace.PaneMode.Actions { | |
| 99 | 99 | if len(keys) == 0 { | |
| @@ -101,12 +101,44 @@ func validateConfig(config *Config) error { | |||
| 101 | 101 | } | |
| 102 | 102 | for _, key := range keys { | |
| 103 | 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 | 106 | 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 | 142 | // Validate codec buffer sizes | |
| 111 | 143 | if config.CodecPreferences.VideoBufferSizeMB < 0 { | |
| 112 | 144 | 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 lists | 96 | + | // 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] = action | 106 | 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 sizes | 142 | // 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") | ||