detached d7d6f84

feat(ui): update window title with active pane's page title

- Add SetTitle method to MainWindow with 255 char truncation
- Add onWindowTitleChanged callback to ContentCoordinator
- Update title on pane focus change and page navigation
- Format: '<Page Title> - Dumber'

Closes #26
commit
d7d6f84ef20cc01b7b2352fc8aed22579466770d
author
bnema · · 2025-12-25 18:18 UTC
parent
cd6e600
signature
GPG Unverified2B82207D370127A0 The key is unknown
3 files · +66 -1
M internal/ui/app.go +33 -0
@@ -796,6 +796,11 @@ func (a *App) initCoordinators(ctx context.Context) {
796796 a.navCoord.RecordHistory(ctx, paneID, url)
797797 })
798798
799+ // Wire window title updates when active pane's title changes
800+ a.contentCoord.SetOnWindowTitleChanged(func(title string) {
801+ a.updateWindowTitle(title)
802+ })
803+
799804 // Wire pane URI updates for session snapshots (searches all tabs)
800805 a.contentCoord.SetOnPaneURIUpdated(func(paneID entity.PaneID, url string) {
801806 a.updatePaneURIInAllTabs(paneID, url)
@@ -875,6 +880,31 @@ func RunWithArgs(ctx context.Context, deps *Dependencies) int {
875880 return app.Run(ctx, os.Args)
876881 }
877882
883+// updateWindowTitle updates the window title with the given page title.
884+// Format: "<Page Title> - Dumber" or just "Dumber" if title is empty.
885+func (a *App) updateWindowTitle(pageTitle string) {
886+ if a.mainWindow == nil {
887+ return
888+ }
889+
890+ title := "Dumber"
891+ if pageTitle != "" {
892+ title = pageTitle + " - Dumber"
893+ }
894+ a.mainWindow.SetTitle(title)
895+}
896+
897+// updateWindowTitleFromActivePane updates the window title based on the current active pane.
898+func (a *App) updateWindowTitleFromActivePane() {
899+ ws := a.activeWorkspace()
900+ if ws == nil || a.contentCoord == nil {
901+ a.updateWindowTitle("")
902+ return
903+ }
904+ title := a.contentCoord.GetTitle(ws.ActivePaneID)
905+ a.updateWindowTitle(title)
906+}
907+
878908 // handleModeChange is called when the input mode changes.
879909 func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) {
880910 log := logging.FromContext(ctx)
@@ -1043,6 +1073,9 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) {
10431073 a.mainWindow.SetContent(gtkWidget)
10441074 }
10451075
1076+ // Update window title with the new active pane's title
1077+ a.updateWindowTitleFromActivePane()
1078+
10461079 log.Debug().Str("tab_id", string(tabID)).Msg("workspace view switched")
10471080 }
10481081
@@ -796,6 +796,11 @@ func (a *App) initCoordinators(ctx context.Context) {
796 a.navCoord.RecordHistory(ctx, paneID, url)796 a.navCoord.RecordHistory(ctx, paneID, url)
797 })797 })
798 798
799+ // Wire window title updates when active pane's title changes
800+ a.contentCoord.SetOnWindowTitleChanged(func(title string) {
801+ a.updateWindowTitle(title)
802+ })
803+
799 // Wire pane URI updates for session snapshots (searches all tabs)804 // Wire pane URI updates for session snapshots (searches all tabs)
800 a.contentCoord.SetOnPaneURIUpdated(func(paneID entity.PaneID, url string) {805 a.contentCoord.SetOnPaneURIUpdated(func(paneID entity.PaneID, url string) {
801 a.updatePaneURIInAllTabs(paneID, url)806 a.updatePaneURIInAllTabs(paneID, url)
@@ -875,6 +880,31 @@ func RunWithArgs(ctx context.Context, deps *Dependencies) int {
875 return app.Run(ctx, os.Args)880 return app.Run(ctx, os.Args)
876 }881 }
877 882
883+// updateWindowTitle updates the window title with the given page title.
884+// Format: "<Page Title> - Dumber" or just "Dumber" if title is empty.
885+func (a *App) updateWindowTitle(pageTitle string) {
886+ if a.mainWindow == nil {
887+ return
888+ }
889+
890+ title := "Dumber"
891+ if pageTitle != "" {
892+ title = pageTitle + " - Dumber"
893+ }
894+ a.mainWindow.SetTitle(title)
895+}
896+
897+// updateWindowTitleFromActivePane updates the window title based on the current active pane.
898+func (a *App) updateWindowTitleFromActivePane() {
899+ ws := a.activeWorkspace()
900+ if ws == nil || a.contentCoord == nil {
901+ a.updateWindowTitle("")
902+ return
903+ }
904+ title := a.contentCoord.GetTitle(ws.ActivePaneID)
905+ a.updateWindowTitle(title)
906+}
907+
878 // handleModeChange is called when the input mode changes.908 // handleModeChange is called when the input mode changes.
879 func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) {909 func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) {
880 log := logging.FromContext(ctx)910 log := logging.FromContext(ctx)
@@ -1043,6 +1073,9 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) {
1043 a.mainWindow.SetContent(gtkWidget)1073 a.mainWindow.SetContent(gtkWidget)
1044 }1074 }
1045 1075
1076+ // Update window title with the new active pane's title
1077+ a.updateWindowTitleFromActivePane()
1078+
1046 log.Debug().Str("tab_id", string(tabID)).Msg("workspace view switched")1079 log.Debug().Str("tab_id", string(tabID)).Msg("workspace view switched")
1047 }1080 }
1048 1081
M internal/ui/coordinator/content.go +19 -1
@@ -49,6 +49,9 @@ type ContentCoordinator struct {
4949 // Callback when pane URI changes (for session snapshots)
5050 onPaneURIUpdated func(paneID entity.PaneID, url string)
5151
52+ // Callback when active pane title changes (for window title updates)
53+ onWindowTitleChanged func(title string)
54+
5255 // Gesture action handler for mouse button navigation
5356 gestureActionHandler input.ActionHandler
5457
@@ -111,6 +114,11 @@ func (c *ContentCoordinator) SetOnPaneURIUpdated(fn func(paneID entity.PaneID, u
111114 c.onPaneURIUpdated = fn
112115 }
113116
117+// SetOnWindowTitleChanged sets the callback for active pane title changes (for window title updates).
118+func (c *ContentCoordinator) SetOnWindowTitleChanged(fn func(title string)) {
119+ c.onWindowTitleChanged = fn
120+}
121+
114122 // SetGestureActionHandler sets the callback for mouse button navigation gestures.
115123 func (c *ContentCoordinator) SetGestureActionHandler(handler input.ActionHandler) {
116124 c.gestureActionHandler = handler
@@ -483,13 +491,18 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P
483491 c.paneTitles[paneID] = title
484492 c.titleMu.Unlock()
485493
486- // Update domain model
494+ // Update domain model and check if this is the active pane
495+ isActivePaneTitle := false
487496 ws, wsView := c.getActiveWS()
488497 if ws != nil {
489498 paneNode := ws.FindPane(paneID)
490499 if paneNode != nil && paneNode.Pane != nil {
491500 paneNode.Pane.Title = title
492501 }
502+ // Check if this pane is the active one
503+ if ws.ActivePaneID == paneID {
504+ isActivePaneTitle = true
505+ }
493506 }
494507
495508 // Update StackedView title bar if this pane is in a stack
@@ -513,6 +526,11 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P
513526 }
514527 }
515528
529+ // Notify window title update if this is the active pane
530+ if isActivePaneTitle && c.onWindowTitleChanged != nil {
531+ c.onWindowTitleChanged(title)
532+ }
533+
516534 log.Debug().
517535 Str("pane_id", string(paneID)).
518536 Str("title", title).
@@ -49,6 +49,9 @@ type ContentCoordinator struct {
49 // Callback when pane URI changes (for session snapshots)49 // Callback when pane URI changes (for session snapshots)
50 onPaneURIUpdated func(paneID entity.PaneID, url string)50 onPaneURIUpdated func(paneID entity.PaneID, url string)
51 51
52+ // Callback when active pane title changes (for window title updates)
53+ onWindowTitleChanged func(title string)
54+
52 // Gesture action handler for mouse button navigation55 // Gesture action handler for mouse button navigation
53 gestureActionHandler input.ActionHandler56 gestureActionHandler input.ActionHandler
54 57
@@ -111,6 +114,11 @@ func (c *ContentCoordinator) SetOnPaneURIUpdated(fn func(paneID entity.PaneID, u
111 c.onPaneURIUpdated = fn114 c.onPaneURIUpdated = fn
112 }115 }
113 116
117+// SetOnWindowTitleChanged sets the callback for active pane title changes (for window title updates).
118+func (c *ContentCoordinator) SetOnWindowTitleChanged(fn func(title string)) {
119+ c.onWindowTitleChanged = fn
120+}
121+
114 // SetGestureActionHandler sets the callback for mouse button navigation gestures.122 // SetGestureActionHandler sets the callback for mouse button navigation gestures.
115 func (c *ContentCoordinator) SetGestureActionHandler(handler input.ActionHandler) {123 func (c *ContentCoordinator) SetGestureActionHandler(handler input.ActionHandler) {
116 c.gestureActionHandler = handler124 c.gestureActionHandler = handler
@@ -483,13 +491,18 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P
483 c.paneTitles[paneID] = title491 c.paneTitles[paneID] = title
484 c.titleMu.Unlock()492 c.titleMu.Unlock()
485 493
486- // Update domain model494+ // Update domain model and check if this is the active pane
495+ isActivePaneTitle := false
487 ws, wsView := c.getActiveWS()496 ws, wsView := c.getActiveWS()
488 if ws != nil {497 if ws != nil {
489 paneNode := ws.FindPane(paneID)498 paneNode := ws.FindPane(paneID)
490 if paneNode != nil && paneNode.Pane != nil {499 if paneNode != nil && paneNode.Pane != nil {
491 paneNode.Pane.Title = title500 paneNode.Pane.Title = title
492 }501 }
502+ // Check if this pane is the active one
503+ if ws.ActivePaneID == paneID {
504+ isActivePaneTitle = true
505+ }
493 }506 }
494 507
495 // Update StackedView title bar if this pane is in a stack508 // Update StackedView title bar if this pane is in a stack
@@ -513,6 +526,11 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P
513 }526 }
514 }527 }
515 528
529+ // Notify window title update if this is the active pane
530+ if isActivePaneTitle && c.onWindowTitleChanged != nil {
531+ c.onWindowTitleChanged(title)
532+ }
533+
516 log.Debug().534 log.Debug().
517 Str("pane_id", string(paneID)).535 Str("pane_id", string(paneID)).
518 Str("title", title).536 Str("title", title).
M internal/ui/window/main_window.go +14 -0
@@ -170,6 +170,20 @@ func (mw *MainWindow) Window() *gtk.ApplicationWindow {
170170 return mw.window
171171 }
172172
173+// SetTitle updates the window title with proper formatting.
174+// The title is capped at 255 characters for display.
175+func (mw *MainWindow) SetTitle(title string) {
176+ if mw.window == nil {
177+ return
178+ }
179+ // Truncate title to 255 characters max
180+ const maxTitleLen = 255
181+ if len(title) > maxTitleLen {
182+ title = title[:maxTitleLen-3] + "..."
183+ }
184+ mw.window.SetTitle(&title)
185+}
186+
173187 // AddOverlay adds a widget to the content overlay.
174188 // The widget will be displayed on top of the workspace content.
175189 func (mw *MainWindow) AddOverlay(widget *gtk.Widget) {
@@ -170,6 +170,20 @@ func (mw *MainWindow) Window() *gtk.ApplicationWindow {
170 return mw.window170 return mw.window
171 }171 }
172 172
173+// SetTitle updates the window title with proper formatting.
174+// The title is capped at 255 characters for display.
175+func (mw *MainWindow) SetTitle(title string) {
176+ if mw.window == nil {
177+ return
178+ }
179+ // Truncate title to 255 characters max
180+ const maxTitleLen = 255
181+ if len(title) > maxTitleLen {
182+ title = title[:maxTitleLen-3] + "..."
183+ }
184+ mw.window.SetTitle(&title)
185+}
186+
173 // AddOverlay adds a widget to the content overlay.187 // AddOverlay adds a widget to the content overlay.
174 // The widget will be displayed on top of the workspace content.188 // The widget will be displayed on top of the workspace content.
175 func (mw *MainWindow) AddOverlay(widget *gtk.Widget) {189 func (mw *MainWindow) AddOverlay(widget *gtk.Widget) {

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