main 5041353

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}

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