package ui import ( "context" "fmt" "github.com/bnema/dumber/internal/application/port" "github.com/bnema/dumber/internal/application/usecase" "github.com/bnema/dumber/internal/logging" "github.com/bnema/dumber/internal/ui/component" "github.com/bnema/dumber/internal/ui/window" ) // newHistorySidebarComponent constructs the native history sidebar. It is a // package-level seam so headless tests can observe on-demand construction // without instantiating GTK widgets. Mutable global: tests must restore it // via defer and must not use t.Parallel alongside seam users. var newHistorySidebarComponent = component.NewHistorySidebar // ensureHistorySidebar constructs the history sidebar on first request. // Construction is separated from mounting and shared visibility mutation: // the caller mounts and reveals through mountNativeSidebar/showHistorySidebar. // The component lifetime is detached from the caller's request context via // context.WithoutCancel so a short-lived window-creation or shortcut context // cannot cancel the sidebar's in-flight loads. It returns false when the // window, dependencies, or native construction are unavailable. func (bw *browserWindow) ensureHistorySidebar(ctx context.Context, a *App) bool { if bw == nil || a == nil || bw.mainWindow == nil { return false } if bw.historySidebar != nil { return true } if a.deps == nil || a.deps.HistoryUC == nil { return false } log := logging.FromContext(ctx) cfg := a.buildHistorySidebarConfig(bw) sidebar := newHistorySidebarComponent(context.WithoutCancel(ctx), cfg) if sidebar == nil { log.Warn().Msg("failed to create history sidebar") return false } bw.historySidebar = sidebar // Apply sidebar width from config, falling back to the default 320px. // The width is clamped to [280, 380] by SetSidebarWidth internally. // Visibility is left untouched: the sidebar container keeps whatever // panel (or none) is currently mounted until the caller reveals history. bw.applySidebarWidthConfig(a) log.Debug().Msg("history sidebar initialized") return true } // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given // browser window. Extracted from initHistorySidebar for testability. // //nolint:dupl // History and favorites sidebar configs intentionally mirror each other for separate component types. func (a *App) buildHistorySidebarConfig(bw *browserWindow) component.HistorySidebarConfig { var historyUC port.HistorySidebarHistory if a.deps != nil { historyUC = a.deps.HistoryUC } return component.HistorySidebarConfig{ HistoryUC: historyUC, OnNavigate: func(navCtx context.Context, url string) error { return a.navigateHistorySidebarSelection(navCtx, bw, url) }, OnNavigateKeepOpen: func(navCtx context.Context, url string) error { return a.navigateHistorySidebarSelection(navCtx, bw, url) }, OnOpenInNewPane: func(splitCtx context.Context, url string) error { if a.wsCoord == nil || !a.hasBrowserWindow(bw) { return nil } a.activateBrowserWindow(bw) return a.wsCoord.SplitWithURL(splitCtx, usecase.SplitRight, url) }, OnClose: func() { a.hideAndRestoreFocusForBrowserWindow(bw) }, } } func (a *App) navigateHistorySidebarSelection(ctx context.Context, bw *browserWindow, url string) error { if a == nil || bw == nil || !a.hasBrowserWindow(bw) { return nil } return a.navigateFromBrowserWindow(ctx, bw, url) } // toggleHistorySidebar toggles sidebar visibility. func (bw *browserWindow) toggleHistorySidebar() { if bw == nil || bw.historySidebar == nil { return } if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory { bw.hideHistorySidebar() } else { bw.showHistorySidebar() } } // showHistorySidebar makes the sidebar visible and grabs focus for the search // entry. func (bw *browserWindow) showHistorySidebar() { if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil { return } bw.mountNativeSidebar(nativeSidebarHistory) bw.historySidebar.Show() bw.mainWindow.SetSidebarVisible(true) bw.sidebarVisible = true bw.activeSidebarKind = nativeSidebarHistory } // hideHistorySidebar hides the sidebar. Callers should also restore focus // to the active content pane after calling this. func (bw *browserWindow) hideHistorySidebar() { if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil { return } bw.historySidebar.Hide() bw.mainWindow.SetSidebarVisible(false) bw.sidebarVisible = false if bw.activeSidebarKind == nativeSidebarHistory { bw.activeSidebarKind = nativeSidebarNone } } func (bw *browserWindow) mountNativeSidebar(kind nativeSidebarKind) { if bw == nil || bw.mainWindow == nil || bw.activeSidebarKind == kind { return } if bw.historySidebar != nil { bw.historySidebar.Hide() } if bw.favoritesSidebar != nil { bw.favoritesSidebar.Hide() } switch kind { case nativeSidebarHistory: if bw.historySidebar != nil { bw.mainWindow.SetSidebarWidget(bw.historySidebar.Widget()) } case nativeSidebarFavorites: if bw.favoritesSidebar != nil { bw.mainWindow.SetSidebarWidget(bw.favoritesSidebar.Widget()) } } } // historySidebarWidthConfig extracts the config-backed sidebar width and // applies it via the MainWindow.SetSidebarWidth path. It is called during // initialization and can be reused if config is reloaded at runtime. func historySidebarWidthConfig(widthPx int) window.SidebarWidthConfig { cfg := window.SidebarDefaultWidth() if widthPx > 0 { cfg.WidthPx = widthPx } return cfg } // applySidebarWidthConfig applies the config-backed sidebar width to the // main window's sidebar. func (bw *browserWindow) applySidebarWidthConfig(a *App) { if bw == nil || bw.mainWindow == nil || a == nil { return } bw.mainWindow.SetSidebarWidth(historySidebarWidthConfig(a.runtimeConfigSnapshot().UI.SidebarWidth)) } // toggleHistorySidebarAction is the keyboard-action handler for toggling the // history sidebar on the last focused browser window. It ensures on-demand // construction before testing availability so shortcuts work without eager // window-creation work. func (a *App) toggleHistorySidebarAction(ctx context.Context) error { bw := a.lastFocusedBrowserWindow() if bw == nil { return fmt.Errorf("history sidebar unavailable: no focused browser window") } if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarHistory { a.hideAndRestoreFocusForBrowserWindow(bw) return nil } if !bw.ensureHistorySidebar(ctx, a) { return fmt.Errorf("history sidebar unavailable: native sidebar not initialized") } bw.toggleHistorySidebar() return nil }