browser_window_history_sidebar.go RawHistoryFolder
1package ui
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/logging"
10 "github.com/bnema/dumber/internal/ui/component"
11 "github.com/bnema/dumber/internal/ui/window"
12)
13
14// newHistorySidebarComponent constructs the native history sidebar. It is a
15// package-level seam so headless tests can observe on-demand construction
16// without instantiating GTK widgets. Mutable global: tests must restore it
17// via defer and must not use t.Parallel alongside seam users.
18var newHistorySidebarComponent = component.NewHistorySidebar
19
20// ensureHistorySidebar constructs the history sidebar on first request.
21// Construction is separated from mounting and shared visibility mutation:
22// the caller mounts and reveals through mountNativeSidebar/showHistorySidebar.
23// The component lifetime is detached from the caller's request context via
24// context.WithoutCancel so a short-lived window-creation or shortcut context
25// cannot cancel the sidebar's in-flight loads. It returns false when the
26// window, dependencies, or native construction are unavailable.
27func (bw *browserWindow) ensureHistorySidebar(ctx context.Context, a *App) bool {
28 if bw == nil || a == nil || bw.mainWindow == nil {
29 return false
30 }
31 if bw.historySidebar != nil {
32 return true
33 }
34 if a.deps == nil || a.deps.HistoryUC == nil {
35 return false
36 }
37 log := logging.FromContext(ctx)
38
39 cfg := a.buildHistorySidebarConfig(bw)
40
41 sidebar := newHistorySidebarComponent(context.WithoutCancel(ctx), cfg)
42 if sidebar == nil {
43 log.Warn().Msg("failed to create history sidebar")
44 return false
45 }
46
47 bw.historySidebar = sidebar
48
49 // Apply sidebar width from config, falling back to the default 320px.
50 // The width is clamped to [280, 380] by SetSidebarWidth internally.
51 // Visibility is left untouched: the sidebar container keeps whatever
52 // panel (or none) is currently mounted until the caller reveals history.
53 bw.applySidebarWidthConfig(a)
54
55 log.Debug().Msg("history sidebar initialized")
56 return true
57}
58
59// buildHistorySidebarConfig constructs the HistorySidebarConfig for the given
60// browser window. Extracted from initHistorySidebar for testability.
61//
62//nolint:dupl // History and favorites sidebar configs intentionally mirror each other for separate component types.
63func (a *App) buildHistorySidebarConfig(bw *browserWindow) component.HistorySidebarConfig {
64 var historyUC port.HistorySidebarHistory
65 if a.deps != nil {
66 historyUC = a.deps.HistoryUC
67 }
68
69 return component.HistorySidebarConfig{
70 HistoryUC: historyUC,
71 OnNavigate: func(navCtx context.Context, url string) error {
72 return a.navigateHistorySidebarSelection(navCtx, bw, url)
73 },
74 OnNavigateKeepOpen: func(navCtx context.Context, url string) error {
75 return a.navigateHistorySidebarSelection(navCtx, bw, url)
76 },
77 OnOpenInNewPane: func(splitCtx context.Context, url string) error {
78 if a.wsCoord == nil || !a.hasBrowserWindow(bw) {
79 return nil
80 }
81 a.activateBrowserWindow(bw)
82 return a.wsCoord.SplitWithURL(splitCtx, usecase.SplitRight, url)
83 },
84 OnClose: func() {
85 a.hideAndRestoreFocusForBrowserWindow(bw)
86 },
87 }
88}
89
90func (a *App) navigateHistorySidebarSelection(ctx context.Context, bw *browserWindow, url string) error {
91 if a == nil || bw == nil || !a.hasBrowserWindow(bw) {
92 return nil
93 }
94 return a.navigateFromBrowserWindow(ctx, bw, url)
95}
96
97// toggleHistorySidebar toggles sidebar visibility.
98func (bw *browserWindow) toggleHistorySidebar() {
99 if bw == nil || bw.historySidebar == nil {
100 return
101 }
102
103 if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory {
104 bw.hideHistorySidebar()
105 } else {
106 bw.showHistorySidebar()
107 }
108}
109
110// showHistorySidebar makes the sidebar visible and grabs focus for the search
111// entry.
112func (bw *browserWindow) showHistorySidebar() {
113 if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil {
114 return
115 }
116 bw.mountNativeSidebar(nativeSidebarHistory)
117 bw.historySidebar.Show()
118 bw.mainWindow.SetSidebarVisible(true)
119 bw.sidebarVisible = true
120 bw.activeSidebarKind = nativeSidebarHistory
121}
122
123// hideHistorySidebar hides the sidebar. Callers should also restore focus
124// to the active content pane after calling this.
125func (bw *browserWindow) hideHistorySidebar() {
126 if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil {
127 return
128 }
129 bw.historySidebar.Hide()
130 bw.mainWindow.SetSidebarVisible(false)
131 bw.sidebarVisible = false
132 if bw.activeSidebarKind == nativeSidebarHistory {
133 bw.activeSidebarKind = nativeSidebarNone
134 }
135}
136
137func (bw *browserWindow) mountNativeSidebar(kind nativeSidebarKind) {
138 if bw == nil || bw.mainWindow == nil || bw.activeSidebarKind == kind {
139 return
140 }
141 if bw.historySidebar != nil {
142 bw.historySidebar.Hide()
143 }
144 if bw.favoritesSidebar != nil {
145 bw.favoritesSidebar.Hide()
146 }
147 switch kind {
148 case nativeSidebarHistory:
149 if bw.historySidebar != nil {
150 bw.mainWindow.SetSidebarWidget(bw.historySidebar.Widget())
151 }
152 case nativeSidebarFavorites:
153 if bw.favoritesSidebar != nil {
154 bw.mainWindow.SetSidebarWidget(bw.favoritesSidebar.Widget())
155 }
156 }
157}
158
159// historySidebarWidthConfig extracts the config-backed sidebar width and
160// applies it via the MainWindow.SetSidebarWidth path. It is called during
161// initialization and can be reused if config is reloaded at runtime.
162func historySidebarWidthConfig(widthPx int) window.SidebarWidthConfig {
163 cfg := window.SidebarDefaultWidth()
164 if widthPx > 0 {
165 cfg.WidthPx = widthPx
166 }
167 return cfg
168}
169
170// applySidebarWidthConfig applies the config-backed sidebar width to the
171// main window's sidebar.
172func (bw *browserWindow) applySidebarWidthConfig(a *App) {
173 if bw == nil || bw.mainWindow == nil || a == nil {
174 return
175 }
176 bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.runtimeConfigSnapshot().UI.SidebarWidth))
177}
178
179// toggleHistorySidebarAction is the keyboard-action handler for toggling the
180// history sidebar on the last focused browser window. It ensures on-demand
181// construction before testing availability so shortcuts work without eager
182// window-creation work.
183func (a *App) toggleHistorySidebarAction(ctx context.Context) error {
184 bw := a.lastFocusedBrowserWindow()
185 if bw == nil {
186 return fmt.Errorf("history sidebar unavailable: no focused browser window")
187 }
188 if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory {
189 a.hideAndRestoreFocusForBrowserWindow(bw)
190 return nil
191 }
192 if !bw.ensureHistorySidebar(ctx, a) {
193 return fmt.Errorf("history sidebar unavailable: native sidebar not initialized")
194 }
195 bw.toggleHistorySidebar()
196 return nil
197}