v0.33.1 9577fee

browser_window_favorites_sidebar.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/application/usecase"
10	"github.com/bnema/dumber/internal/logging"
11	"github.com/bnema/dumber/internal/ui/component"
12)
13
14// newFavoritesSidebarComponent constructs the native favorites sidebar. It is
15// a 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 newFavoritesSidebarComponent = component.NewFavoritesSidebar
19
20// ensureFavoritesSidebar constructs the favorites sidebar on first request.
21// Construction is separated from mounting and shared visibility mutation:
22// the caller mounts and reveals through mountNativeSidebar/showFavoritesSidebar.
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) ensureFavoritesSidebar(ctx context.Context, a *App) bool {
28	if bw == nil || a == nil || bw.mainWindow == nil {
29		return false
30	}
31	if bw.favoritesSidebar != nil {
32		return true
33	}
34	if a.deps == nil || a.deps.FavoritesUC == nil {
35		return false
36	}
37	log := logging.FromContext(ctx)
38
39	cfg := a.buildFavoritesSidebarConfig(bw)
40	sidebar := newFavoritesSidebarComponent(context.WithoutCancel(ctx), cfg)
41	if sidebar == nil {
42		log.Warn().Msg("failed to create favorites sidebar")
43		return false
44	}
45
46	bw.favoritesSidebar = sidebar
47	bw.applySidebarWidthConfig(a)
48	log.Debug().Msg("favorites sidebar initialized")
49	return true
50}
51
52//nolint:dupl // Favorites and history sidebar configs intentionally mirror each other for separate component types.
53func (a *App) buildFavoritesSidebarConfig(bw *browserWindow) component.FavoritesSidebarConfig {
54	var favoritesUC port.FavoritesSidebarFavorites
55	if a.deps != nil {
56		favoritesUC = a.deps.FavoritesUC
57	}
58	return component.FavoritesSidebarConfig{
59		FavoritesUC: favoritesUC,
60		OnNavigate: func(navCtx context.Context, url string) error {
61			return a.navigateFavoritesSidebarSelection(navCtx, bw, url)
62		},
63		OnNavigateKeepOpen: func(navCtx context.Context, url string) error {
64			return a.navigateFavoritesSidebarSelection(navCtx, bw, url)
65		},
66		OnOpenInNewPane: func(splitCtx context.Context, url string) error {
67			if a.wsCoord == nil || !a.hasBrowserWindow(bw) {
68				return nil
69			}
70			a.activateBrowserWindow(bw)
71			return a.wsCoord.SplitWithURL(splitCtx, usecase.SplitRight, url)
72		},
73		OnClose: func() {
74			a.hideAndRestoreFocusForBrowserWindow(bw)
75		},
76	}
77}
78
79func (a *App) navigateFavoritesSidebarSelection(ctx context.Context, bw *browserWindow, url string) error {
80	if a == nil || bw == nil || !a.hasBrowserWindow(bw) {
81		return nil
82	}
83	return a.navigateFromBrowserWindow(ctx, bw, url)
84}
85
86func (bw *browserWindow) toggleFavoritesSidebar() {
87	if bw == nil || bw.favoritesSidebar == nil {
88		return
89	}
90	if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites {
91		bw.hideFavoritesSidebar()
92		return
93	}
94	bw.showFavoritesSidebar()
95}
96
97func (bw *browserWindow) showFavoritesSidebar() {
98	if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil {
99		return
100	}
101	bw.mountNativeSidebar(nativeSidebarFavorites)
102	bw.favoritesSidebar.Show()
103	bw.mainWindow.SetSidebarVisible(true)
104	bw.sidebarVisible = true
105	bw.activeSidebarKind = nativeSidebarFavorites
106}
107
108func (bw *browserWindow) hideFavoritesSidebar() {
109	if bw == nil || bw.favoritesSidebar == nil || bw.mainWindow == nil {
110		return
111	}
112	bw.favoritesSidebar.Hide()
113	bw.mainWindow.SetSidebarVisible(false)
114	bw.sidebarVisible = false
115	if bw.activeSidebarKind == nativeSidebarFavorites {
116		bw.activeSidebarKind = nativeSidebarNone
117	}
118}
119
120func (a *App) toggleFavoritesSidebarAction(ctx context.Context) error {
121	bw := a.lastFocusedBrowserWindow()
122	if bw == nil {
123		return fmt.Errorf("favorites sidebar unavailable: no focused browser window")
124	}
125	if bw.sidebarVisible && bw.activeSidebarKind == nativeSidebarFavorites {
126		a.hideAndRestoreFocusForBrowserWindow(bw)
127		return nil
128	}
129	if !bw.ensureFavoritesSidebar(ctx, a) {
130		return fmt.Errorf("favorites sidebar unavailable: native sidebar not initialized")
131	}
132	bw.toggleFavoritesSidebar()
133	return nil
134}
135
136func (a *App) toggleCurrentPageFavoriteAction(ctx context.Context) error {
137	if a == nil || a.deps == nil || a.deps.FavoritesUC == nil {
138		return fmt.Errorf("favorites unavailable: usecase not configured")
139	}
140	bw := a.lastFocusedBrowserWindow()
141	_, wv := a.activeWebViewForBrowserWindow(bw)
142	if wv == nil {
143		return fmt.Errorf("favorites unavailable: no active webview")
144	}
145	uri := strings.TrimSpace(wv.URI())
146	if uri == "" {
147		return fmt.Errorf("favorites unavailable: active page has no URI")
148	}
149	result, err := a.deps.FavoritesUC.Toggle(ctx, uri, wv.Title())
150	if err != nil {
151		return err
152	}
153	if result != nil && strings.TrimSpace(result.Message) != "" {
154		a.showToastOnBrowserWindow(ctx, bw, result.Message, component.ToastSuccess,
155			component.WithDuration(component.ToastBriefDurationMs),
156			component.WithPosition(component.ToastPositionBottomRight),
157		)
158	}
159	a.reloadVisibleFavoritesSidebars("current-page-favorite-toggle")
160	return nil
161}
162
163func (a *App) reloadVisibleFavoritesSidebars(reason string) {
164	if a == nil {
165		return
166	}
167	for _, bw := range a.browserWindows {
168		if bw == nil || bw.favoritesSidebar == nil {
169			continue
170		}
171		bw.favoritesSidebar.RequestReloadIfVisible(reason)
172	}
173}

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