detached 0f0e7d6

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 {
191191 NextTab: "ctrl+tab",
192192 PreviousTab: "ctrl+shift+tab",
193193 },
194- TabBarPosition: defaultTabBarPosition,
194+ TabBarPosition: defaultTabBarPosition,
195+ HideTabBarWhenSingleTab: true,
195196 Popups: PopupBehaviorConfig{
196197 Behavior: PopupBehaviorSplit, // Default: open JavaScript popups in split panes
197198 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 panes197 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 {
227227 Tabs TabKeyConfig `mapstructure:"tabs" yaml:"tabs" toml:"tabs" json:"tabs"`
228228 // TabBarPosition determines tab bar placement: "top" or "bottom".
229229 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"`
230232 // Popups configures default popup placement rules.
231233 Popups PopupBehaviorConfig `mapstructure:"popups" yaml:"popups" toml:"popups" json:"popups"`
232234 // 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 (
1818 uihandler "github.com/bnema/dumber/internal/ui/handler"
1919 "github.com/bnema/dumber/internal/ui/input"
2020 "github.com/bnema/dumber/internal/ui/layout"
21+ "github.com/bnema/dumber/internal/ui/theme"
2122 "github.com/bnema/dumber/internal/ui/window"
2223 )
2324
@@ -141,6 +142,11 @@ func (a *App) onActivate(ctx context.Context) {
141142 return
142143 }
143144
145+ // Apply GTK CSS styling for tab bar
146+ if display := a.mainWindow.Window().GetDisplay(); display != nil {
147+ theme.ApplyCSS(display)
148+ }
149+
144150 // Initialize widget factory for pane layout
145151 a.widgetFactory = layout.NewGtkWidgetFactory()
146152
@@ -211,6 +217,9 @@ func (a *App) createInitialTab(ctx context.Context) {
211217 a.mainWindow.TabBar().SetActive(output.Tab.ID)
212218 }
213219
220+ // Update tab bar visibility (hide if only 1 tab)
221+ a.updateTabBarVisibility(ctx)
222+
214223 // Create workspace view for this tab
215224 a.createWorkspaceView(ctx, output.Tab)
216225
@@ -351,6 +360,9 @@ func (a *App) handleNewTab(ctx context.Context) error {
351360 a.mainWindow.TabBar().SetActive(output.Tab.ID)
352361 }
353362
363+ // Update tab bar visibility
364+ a.updateTabBarVisibility(ctx)
365+
354366 log.Debug().Str("tab_id", string(output.Tab.ID)).Msg("new tab created")
355367
356368 return nil
@@ -377,6 +389,9 @@ func (a *App) handleCloseTab(ctx context.Context) error {
377389 }
378390 }
379391
392+ // Update tab bar visibility
393+ a.updateTabBarVisibility(ctx)
394+
380395 // Quit if no tabs left
381396 if wasLast {
382397 a.Quit()
@@ -385,6 +400,32 @@ func (a *App) handleCloseTab(ctx context.Context) error {
385400 return nil
386401 }
387402
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+
388429 // handleNextTab switches to the next tab.
389430 func (a *App) handleNextTab(ctx context.Context) error {
390431 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 return142 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 layout150 // 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 tab223 // 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 nil368 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 left395 // 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 nil400 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 {

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