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 Unverified
2B82207D370127A0The 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) { | |||
| 796 | 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 | 804 | // Wire pane URI updates for session snapshots (searches all tabs) | |
| 800 | 805 | a.contentCoord.SetOnPaneURIUpdated(func(paneID entity.PaneID, url string) { | |
| 801 | 806 | a.updatePaneURIInAllTabs(paneID, url) | |
| @@ -875,6 +880,31 @@ func RunWithArgs(ctx context.Context, deps *Dependencies) int { | |||
| 875 | 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 | 908 | // handleModeChange is called when the input mode changes. | |
| 879 | 909 | func (a *App) handleModeChange(ctx context.Context, from, to input.Mode) { | |
| 880 | 910 | log := logging.FromContext(ctx) | |
| @@ -1043,6 +1073,9 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) { | |||
| 1043 | 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 | 1079 | log.Debug().Str("tab_id", string(tabID)).Msg("workspace view switched") | |
| 1047 | 1080 | } | |
| 1048 | 1081 | ||
| @@ -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 { | |||
| 49 | 49 | // Callback when pane URI changes (for session snapshots) | |
| 50 | 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 | 55 | // Gesture action handler for mouse button navigation | |
| 53 | 56 | gestureActionHandler input.ActionHandler | |
| 54 | 57 | ||
| @@ -111,6 +114,11 @@ func (c *ContentCoordinator) SetOnPaneURIUpdated(fn func(paneID entity.PaneID, u | |||
| 111 | 114 | 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 | 122 | // SetGestureActionHandler sets the callback for mouse button navigation gestures. | |
| 115 | 123 | func (c *ContentCoordinator) SetGestureActionHandler(handler input.ActionHandler) { | |
| 116 | 124 | c.gestureActionHandler = handler | |
| @@ -483,13 +491,18 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P | |||
| 483 | 491 | c.paneTitles[paneID] = title | |
| 484 | 492 | c.titleMu.Unlock() | |
| 485 | 493 | ||
| 486 | - | // Update domain model | |
| 494 | + | // Update domain model and check if this is the active pane | |
| 495 | + | isActivePaneTitle := false | |
| 487 | 496 | ws, wsView := c.getActiveWS() | |
| 488 | 497 | if ws != nil { | |
| 489 | 498 | paneNode := ws.FindPane(paneID) | |
| 490 | 499 | if paneNode != nil && paneNode.Pane != nil { | |
| 491 | 500 | 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 | 508 | // 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 | 534 | log.Debug(). | |
| 517 | 535 | Str("pane_id", string(paneID)). | |
| 518 | 536 | 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 navigation | 55 | // Gesture action handler for mouse button navigation | ||
| 53 | gestureActionHandler input.ActionHandler | 56 | gestureActionHandler input.ActionHandler | ||
| 54 | 57 | ||||
| @@ -111,6 +114,11 @@ func (c *ContentCoordinator) SetOnPaneURIUpdated(fn func(paneID entity.PaneID, u | |||||
| 111 | c.onPaneURIUpdated = fn | 114 | 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 = handler | 124 | c.gestureActionHandler = handler | ||
| @@ -483,13 +491,18 @@ func (c *ContentCoordinator) onTitleChanged(ctx context.Context, paneID entity.P | |||||
| 483 | c.paneTitles[paneID] = title | 491 | c.paneTitles[paneID] = title | ||
| 484 | c.titleMu.Unlock() | 492 | c.titleMu.Unlock() | ||
| 485 | 493 | ||||
| 486 | - | // Update domain model | 494 | + | // 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 = title | 500 | 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 stack | 508 | // 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 { | |||
| 170 | 170 | 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 | 187 | // AddOverlay adds a widget to the content overlay. | |
| 174 | 188 | // The widget will be displayed on top of the workspace content. | |
| 175 | 189 | func (mw *MainWindow) AddOverlay(widget *gtk.Widget) { | |
| @@ -170,6 +170,20 @@ func (mw *MainWindow) Window() *gtk.ApplicationWindow { | |||||
| 170 | return mw.window | 170 | 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) { | ||