feat(ui): implement auto-hide tab bar when single tab exists
Added HideTabBarWhenSingleTab config field with default value true. Implemented updateTabBarVisibility() method that hides the tab bar when only one tab is open and shows it when multiple tabs exist.
- commit
- 0f0e7d6c966aa339f2a0392748f97e08f219c928
- author
- brice · · 2025-12-11 21:48 UTC
- committer
- brice · · 2025-12-11 22:48 UTC
- parent
- bdbf9d5
- signature
- unsigned
3 files · +45 -1
M internal/infrastructure/config/defaults.go +2 -1
| @@ -191,7 +191,8 @@ func DefaultConfig() *Config { | |||
| 191 | 191 | NextTab: "ctrl+tab", | |
| 192 | 192 | PreviousTab: "ctrl+shift+tab", | |
| 193 | 193 | }, | |
| 194 | - | TabBarPosition: defaultTabBarPosition, | |
| 194 | + | TabBarPosition: defaultTabBarPosition, | |
| 195 | + | HideTabBarWhenSingleTab: true, | |
| 195 | 196 | Popups: PopupBehaviorConfig{ | |
| 196 | 197 | Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes | |
| 197 | 198 | Placement: defaultPopupPlacement, | |
| @@ -191,7 +191,8 @@ func DefaultConfig() *Config { | |||||
| 191 | NextTab: "ctrl+tab", | 191 | NextTab: "ctrl+tab", | ||
| 192 | PreviousTab: "ctrl+shift+tab", | 192 | PreviousTab: "ctrl+shift+tab", | ||
| 193 | }, | 193 | }, | ||
| 194 | - | TabBarPosition: defaultTabBarPosition, | 194 | + | TabBarPosition: defaultTabBarPosition, |
| 195 | + | HideTabBarWhenSingleTab: true, | |||
| 195 | Popups: PopupBehaviorConfig{ | 196 | Popups: PopupBehaviorConfig{ | ||
| 196 | Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes | 197 | Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes | ||
| 197 | Placement: defaultPopupPlacement, | 198 | Placement: defaultPopupPlacement, | ||
M internal/infrastructure/config/schema.go +2 -0
| @@ -227,6 +227,8 @@ type WorkspaceConfig struct { | |||
| 227 | 227 | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" toml:"tabs" json:"tabs"` | |
| 228 | 228 | // TabBarPosition determines tab bar placement: "top" or "bottom". | |
| 229 | 229 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | |
| 230 | + | // HideTabBarWhenSingleTab hides the tab bar when only one tab exists. | |
| 231 | + | HideTabBarWhenSingleTab bool `mapstructure:"hide_tab_bar_when_single_tab" yaml:"hide_tab_bar_when_single_tab" toml:"hide_tab_bar_when_single_tab" json:"hide_tab_bar_when_single_tab"` | |
| 230 | 232 | // Popups configures default popup placement rules. | |
| 231 | 233 | Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" toml:"popups" json:"popups"` | |
| 232 | 234 | // Styling configures workspace visual appearance. | |
| @@ -227,6 +227,8 @@ type WorkspaceConfig struct { | |||||
| 227 | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" toml:"tabs" json:"tabs"` | 227 | Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" toml:"tabs" json:"tabs"` | ||
| 228 | // TabBarPosition determines tab bar placement: "top" or "bottom". | 228 | // TabBarPosition determines tab bar placement: "top" or "bottom". | ||
| 229 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | 229 | TabBarPosition string `mapstructure:"tab_bar_position" yaml:"tab_bar_position" toml:"tab_bar_position" json:"tab_bar_position"` | ||
| 230 | + | // HideTabBarWhenSingleTab hides the tab bar when only one tab exists. | |||
| 231 | + | HideTabBarWhenSingleTab bool `mapstructure:"hide_tab_bar_when_single_tab" yaml:"hide_tab_bar_when_single_tab" toml:"hide_tab_bar_when_single_tab" json:"hide_tab_bar_when_single_tab"` | |||
| 230 | // Popups configures default popup placement rules. | 232 | // Popups configures default popup placement rules. | ||
| 231 | Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" toml:"popups" json:"popups"` | 233 | Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" toml:"popups" json:"popups"` | ||
| 232 | // Styling configures workspace visual appearance. | 234 | // Styling configures workspace visual appearance. | ||
M internal/ui/app.go +41 -0
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | 18 | uihandler "github.com/bnema/dumber/internal/ui/handler" | |
| 19 | 19 | "github.com/bnema/dumber/internal/ui/input" | |
| 20 | 20 | "github.com/bnema/dumber/internal/ui/layout" | |
| 21 | + | "github.com/bnema/dumber/internal/ui/theme" | |
| 21 | 22 | "github.com/bnema/dumber/internal/ui/window" | |
| 22 | 23 | ) | |
| 23 | 24 | ||
| @@ -141,6 +142,11 @@ func (a *App) onActivate(ctx context.Context) { | |||
| 141 | 142 | return | |
| 142 | 143 | } | |
| 143 | 144 | ||
| 145 | + | // Apply GTK CSS styling for tab bar | |
| 146 | + | if display := a.mainWindow.Window().GetDisplay(); display != nil { | |
| 147 | + | theme.ApplyCSS(display) | |
| 148 | + | } | |
| 149 | + | ||
| 144 | 150 | // Initialize widget factory for pane layout | |
| 145 | 151 | a.widgetFactory = layout.NewGtkWidgetFactory() | |
| 146 | 152 | ||
| @@ -211,6 +217,9 @@ func (a *App) createInitialTab(ctx context.Context) { | |||
| 211 | 217 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | |
| 212 | 218 | } | |
| 213 | 219 | ||
| 220 | + | // Update tab bar visibility (hide if only 1 tab) | |
| 221 | + | a.updateTabBarVisibility(ctx) | |
| 222 | + | ||
| 214 | 223 | // Create workspace view for this tab | |
| 215 | 224 | a.createWorkspaceView(ctx, output.Tab) | |
| 216 | 225 | ||
| @@ -351,6 +360,9 @@ func (a *App) handleNewTab(ctx context.Context) error { | |||
| 351 | 360 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | |
| 352 | 361 | } | |
| 353 | 362 | ||
| 363 | + | // Update tab bar visibility | |
| 364 | + | a.updateTabBarVisibility(ctx) | |
| 365 | + | ||
| 354 | 366 | log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("new tab created") | |
| 355 | 367 | ||
| 356 | 368 | return nil | |
| @@ -377,6 +389,9 @@ func (a *App) handleCloseTab(ctx context.Context) error { | |||
| 377 | 389 | } | |
| 378 | 390 | } | |
| 379 | 391 | ||
| 392 | + | // Update tab bar visibility | |
| 393 | + | a.updateTabBarVisibility(ctx) | |
| 394 | + | ||
| 380 | 395 | // Quit if no tabs left | |
| 381 | 396 | if wasLast { | |
| 382 | 397 | a.Quit() | |
| @@ -385,6 +400,32 @@ func (a *App) handleCloseTab(ctx context.Context) error { | |||
| 385 | 400 | return nil | |
| 386 | 401 | } | |
| 387 | 402 | ||
| 403 | + | // updateTabBarVisibility shows or hides the tab bar based on tab count. | |
| 404 | + | // Tab bar is hidden when there's only 1 tab (configurable). | |
| 405 | + | func (a *App) updateTabBarVisibility(ctx context.Context) { | |
| 406 | + | log := logging.FromContext(ctx) | |
| 407 | + | ||
| 408 | + | // Check if feature is enabled | |
| 409 | + | if a.deps.Config != nil && !a.deps.Config.Workspace.HideTabBarWhenSingleTab { | |
| 410 | + | return | |
| 411 | + | } | |
| 412 | + | ||
| 413 | + | if a.mainWindow == nil || a.mainWindow.TabBar() == nil { | |
| 414 | + | return | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | tabCount := a.tabs.Count() | |
| 418 | + | shouldShow := tabCount > 1 | |
| 419 | + | ||
| 420 | + | a.mainWindow.TabBar().SetVisible(shouldShow) | |
| 421 | + | ||
| 422 | + | if shouldShow { | |
| 423 | + | log.Debug().Int("tab_count", tabCount).Msg("tab bar visible") | |
| 424 | + | } else { | |
| 425 | + | log.Debug().Msg("tab bar hidden (single tab)") | |
| 426 | + | } | |
| 427 | + | } | |
| 428 | + | ||
| 388 | 429 | // handleNextTab switches to the next tab. | |
| 389 | 430 | func (a *App) handleNextTab(ctx context.Context) error { | |
| 390 | 431 | if err := a.tabsUC.SwitchNext(ctx, a.tabs); err != nil { | |
| @@ -18,6 +18,7 @@ import ( | |||||
| 18 | uihandler "github.com/bnema/dumber/internal/ui/handler" | 18 | uihandler "github.com/bnema/dumber/internal/ui/handler" | ||
| 19 | "github.com/bnema/dumber/internal/ui/input" | 19 | "github.com/bnema/dumber/internal/ui/input" | ||
| 20 | "github.com/bnema/dumber/internal/ui/layout" | 20 | "github.com/bnema/dumber/internal/ui/layout" | ||
| 21 | + | "github.com/bnema/dumber/internal/ui/theme" | |||
| 21 | "github.com/bnema/dumber/internal/ui/window" | 22 | "github.com/bnema/dumber/internal/ui/window" | ||
| 22 | ) | 23 | ) | ||
| 23 | 24 | ||||
| @@ -141,6 +142,11 @@ func (a *App) onActivate(ctx context.Context) { | |||||
| 141 | return | 142 | return | ||
| 142 | } | 143 | } | ||
| 143 | 144 | ||||
| 145 | + | // Apply GTK CSS styling for tab bar | |||
| 146 | + | if display := a.mainWindow.Window().GetDisplay(); display != nil { | |||
| 147 | + | theme.ApplyCSS(display) | |||
| 148 | + | } | |||
| 149 | + | ||||
| 144 | // Initialize widget factory for pane layout | 150 | // Initialize widget factory for pane layout | ||
| 145 | a.widgetFactory = layout.NewGtkWidgetFactory() | 151 | a.widgetFactory = layout.NewGtkWidgetFactory() | ||
| 146 | 152 | ||||
| @@ -211,6 +217,9 @@ func (a *App) createInitialTab(ctx context.Context) { | |||||
| 211 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | 217 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | ||
| 212 | } | 218 | } | ||
| 213 | 219 | ||||
| 220 | + | // Update tab bar visibility (hide if only 1 tab) | |||
| 221 | + | a.updateTabBarVisibility(ctx) | |||
| 222 | + | ||||
| 214 | // Create workspace view for this tab | 223 | // Create workspace view for this tab | ||
| 215 | a.createWorkspaceView(ctx, output.Tab) | 224 | a.createWorkspaceView(ctx, output.Tab) | ||
| 216 | 225 | ||||
| @@ -351,6 +360,9 @@ func (a *App) handleNewTab(ctx context.Context) error { | |||||
| 351 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | 360 | a.mainWindow.TabBar().SetActive(output.Tab.ID) | ||
| 352 | } | 361 | } | ||
| 353 | 362 | ||||
| 363 | + | // Update tab bar visibility | |||
| 364 | + | a.updateTabBarVisibility(ctx) | |||
| 365 | + | ||||
| 354 | log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("new tab created") | 366 | log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("new tab created") | ||
| 355 | 367 | ||||
| 356 | return nil | 368 | return nil | ||
| @@ -377,6 +389,9 @@ func (a *App) handleCloseTab(ctx context.Context) error { | |||||
| 377 | } | 389 | } | ||
| 378 | } | 390 | } | ||
| 379 | 391 | ||||
| 392 | + | // Update tab bar visibility | |||
| 393 | + | a.updateTabBarVisibility(ctx) | |||
| 394 | + | ||||
| 380 | // Quit if no tabs left | 395 | // Quit if no tabs left | ||
| 381 | if wasLast { | 396 | if wasLast { | ||
| 382 | a.Quit() | 397 | a.Quit() | ||
| @@ -385,6 +400,32 @@ func (a *App) handleCloseTab(ctx context.Context) error { | |||||
| 385 | return nil | 400 | return nil | ||
| 386 | } | 401 | } | ||
| 387 | 402 | ||||
| 403 | + | // updateTabBarVisibility shows or hides the tab bar based on tab count. | |||
| 404 | + | // Tab bar is hidden when there's only 1 tab (configurable). | |||
| 405 | + | func (a *App) updateTabBarVisibility(ctx context.Context) { | |||
| 406 | + | log := logging.FromContext(ctx) | |||
| 407 | + | ||||
| 408 | + | // Check if feature is enabled | |||
| 409 | + | if a.deps.Config != nil && !a.deps.Config.Workspace.HideTabBarWhenSingleTab { | |||
| 410 | + | return | |||
| 411 | + | } | |||
| 412 | + | ||||
| 413 | + | if a.mainWindow == nil || a.mainWindow.TabBar() == nil { | |||
| 414 | + | return | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | tabCount := a.tabs.Count() | |||
| 418 | + | shouldShow := tabCount > 1 | |||
| 419 | + | ||||
| 420 | + | a.mainWindow.TabBar().SetVisible(shouldShow) | |||
| 421 | + | ||||
| 422 | + | if shouldShow { | |||
| 423 | + | log.Debug().Int("tab_count", tabCount).Msg("tab bar visible") | |||
| 424 | + | } else { | |||
| 425 | + | log.Debug().Msg("tab bar hidden (single tab)") | |||
| 426 | + | } | |||
| 427 | + | } | |||
| 428 | + | ||||
| 388 | // handleNextTab switches to the next tab. | 429 | // handleNextTab switches to the next tab. | ||
| 389 | func (a *App) handleNextTab(ctx context.Context) error { | 430 | func (a *App) handleNextTab(ctx context.Context) error { | ||
| 390 | if err := a.tabsUC.SwitchNext(ctx, a.tabs); err != nil { | 431 | if err := a.tabsUC.SwitchNext(ctx, a.tabs); err != nil { | ||