v0.29.0 21b3150

navigation.go RawHistoryFolder

1package coordinator
2
3import (
4	"context"
5	"fmt"
6	"time"
7
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/application/usecase"
10	"github.com/bnema/dumber/internal/domain/entity"
11	"github.com/bnema/dumber/internal/logging"
12	"github.com/bnema/dumber/internal/ui/coordinator/content"
13)
14
15// OmniboxProvider provides access to omnibox operations.
16type OmniboxProvider interface {
17	ToggleOmnibox(ctx context.Context)
18	UpdateOmniboxZoom(factor float64)
19}
20
21// NavigationCoordinator handles URL navigation, history, and browser controls.
22type NavigationCoordinator struct {
23	contextProvider func() context.Context
24	navigateUC      *usecase.NavigateUseCase
25	contentCoord    *content.Coordinator
26	omniboxProvider OmniboxProvider
27}
28
29const faviconPreloadTimeout = 300 * time.Millisecond
30
31// NewNavigationCoordinator creates a new NavigationCoordinator.
32func NewNavigationCoordinator(
33	ctx context.Context,
34	navigateUC *usecase.NavigateUseCase,
35	contentCoord *content.Coordinator,
36) *NavigationCoordinator {
37	log := logging.FromContext(ctx)
38	log.Debug().Msg("creating navigation coordinator")
39	callbackLogger := *log
40
41	return &NavigationCoordinator{
42		contextProvider: func() context.Context {
43			return logging.WithContext(context.Background(), callbackLogger)
44		},
45		navigateUC:   navigateUC,
46		contentCoord: contentCoord,
47	}
48}
49
50// SetOmniboxProvider sets the omnibox provider for toggle/zoom operations.
51func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) {
52	c.omniboxProvider = provider
53}
54
55// requireWebView returns an error if wv is nil, preserving stable error text.
56func requireWebView(wv port.WebView) error {
57	if wv == nil {
58		return fmt.Errorf("no webview provided")
59	}
60	return nil
61}
62
63func (c *NavigationCoordinator) trackNavigationOrigin(ctx context.Context, paneID entity.PaneID, url string) {
64	if c.contentCoord == nil || paneID == "" {
65		return
66	}
67	c.contentCoord.SetNavigationOrigin(paneID, url)
68	go func() {
69		preloadCtx, cancelPreload := context.WithTimeout(ctx, faviconPreloadTimeout)
70		defer cancelPreload()
71		c.contentCoord.PreloadCachedFavicon(preloadCtx, paneID, url)
72	}()
73}
74
75// NavigateWebView loads a URL in the specified pane using the provided WebView.
76// This is the explicit-target equivalent of Navigate: it uses NavigateUseCase if
77// available, falling back to direct LoadURI. Origin tracking via contentCoord is
78// performed when contentCoord and paneID are available.
79func (c *NavigationCoordinator) NavigateWebView(ctx context.Context, url string, paneID entity.PaneID, wv port.WebView) error {
80	log := logging.FromContext(ctx)
81
82	if err := requireWebView(wv); err != nil {
83		log.Warn().Str("url", url).Msg("NavigateWebView called with nil webview")
84		return err
85	}
86
87	paneIDStr := string(paneID)
88
89	if c.navigateUC != nil {
90		input := usecase.NavigateInput{
91			URL:     url,
92			PaneID:  paneIDStr,
93			WebView: wv,
94		}
95		output, err := c.navigateUC.Execute(ctx, input)
96		if err != nil {
97			log.Error().Err(err).Str("url", url).Str("pane_id", paneIDStr).Msg("navigation failed")
98			return err
99		}
100		c.trackNavigationOrigin(ctx, paneID, url)
101		log.Debug().
102			Str("url", url).
103			Str("pane_id", paneIDStr).
104			Float64("zoom", output.AppliedZoom).
105			Uint64("webview_id", uint64(wv.ID())).
106			Msg("navigation initiated via usecase")
107		return nil
108	}
109
110	log.Warn().Msg("navigateUC not available, using direct LoadURI")
111	if err := wv.LoadURI(ctx, url); err != nil {
112		log.Error().Err(err).Str("url", url).Msg("failed to navigate")
113		return err
114	}
115
116	c.trackNavigationOrigin(ctx, paneID, url)
117	log.Debug().Str("url", url).Uint64("webview_id", uint64(wv.ID())).Msg("navigated explicit webview (direct)")
118	return nil
119}
120
121// ReloadWebView reloads the page in the provided WebView, optionally bypassing cache.
122func (c *NavigationCoordinator) ReloadWebView(ctx context.Context, wv port.WebView, bypassCache bool) error {
123	log := logging.FromContext(ctx)
124
125	if err := requireWebView(wv); err != nil {
126		log.Debug().Msg("ReloadWebView called with nil webview")
127		return err
128	}
129
130	if c.navigateUC != nil {
131		return c.navigateUC.Reload(ctx, wv, bypassCache)
132	}
133
134	if bypassCache {
135		log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("reloading explicit webview bypassing cache")
136		return wv.ReloadBypassCache(ctx)
137	}
138
139	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("reloading explicit webview")
140	return wv.Reload(ctx)
141}
142
143// StopWebView stops loading in the provided WebView.
144func (c *NavigationCoordinator) StopWebView(ctx context.Context, wv port.WebView) error {
145	log := logging.FromContext(ctx)
146
147	if err := requireWebView(wv); err != nil {
148		log.Debug().Msg("StopWebView called with nil webview")
149		return err
150	}
151
152	if c.navigateUC != nil {
153		return c.navigateUC.Stop(ctx, wv)
154	}
155
156	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("stopping explicit webview")
157	return wv.Stop(ctx)
158}
159
160// GoBackWebView navigates back in history for the provided WebView.
161// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
162func (c *NavigationCoordinator) GoBackWebView(ctx context.Context, wv port.WebView) error {
163	log := logging.FromContext(ctx)
164
165	if err := requireWebView(wv); err != nil {
166		log.Debug().Msg("GoBackWebView called with nil webview")
167		return err
168	}
169
170	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("going back in explicit webview")
171	return wv.GoBack(ctx)
172}
173
174// GoForwardWebView navigates forward in history for the provided WebView.
175// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
176func (c *NavigationCoordinator) GoForwardWebView(ctx context.Context, wv port.WebView) error {
177	log := logging.FromContext(ctx)
178
179	if err := requireWebView(wv); err != nil {
180		log.Debug().Msg("GoForwardWebView called with nil webview")
181		return err
182	}
183
184	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("going forward in explicit webview")
185	return wv.GoForward(ctx)
186}
187
188// OpenOmnibox toggles the omnibox visibility.
189func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error {
190	log := logging.FromContext(ctx)
191
192	if c.omniboxProvider == nil {
193		log.Error().Msg("omnibox provider not initialized")
194		return fmt.Errorf("omnibox provider not initialized")
195	}
196
197	log.Debug().Msg("toggling omnibox")
198	c.omniboxProvider.ToggleOmnibox(ctx)
199	return nil
200}
201
202// OpenDevToolsWebView opens the WebKit inspector for the provided WebView.
203func (c *NavigationCoordinator) OpenDevToolsWebView(ctx context.Context, wv port.WebView) error {
204	log := logging.FromContext(ctx)
205
206	if err := requireWebView(wv); err != nil {
207		log.Warn().Msg("OpenDevToolsWebView called with nil webview")
208		return err
209	}
210
211	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening devtools")
212
213	if opener, ok := wv.(port.DevToolsOpener); ok {
214		opener.OpenDevTools()
215		return nil
216	}
217
218	return fmt.Errorf("webview does not support devtools")
219}
220
221// PrintWebView opens the print dialog for the provided WebView.
222func (c *NavigationCoordinator) PrintWebView(ctx context.Context, wv port.WebView) error {
223	log := logging.FromContext(ctx)
224
225	if err := requireWebView(wv); err != nil {
226		log.Warn().Msg("PrintWebView called with nil webview")
227		return err
228	}
229
230	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening print dialog")
231
232	if printer, ok := wv.(port.Printer); ok {
233		printer.PrintPage()
234		return nil
235	}
236
237	return fmt.Errorf("webview does not support printing")
238}
239
240// UpdateHistoryTitle updates the title of a history entry after page load.
241func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
242	if c.navigateUC == nil {
243		return
244	}
245
246	c.navigateUC.UpdateHistoryTitle(ctx, url, title)
247}
248
249// RecordHistory records a URL in history on page commit.
250func (c *NavigationCoordinator) RecordHistory(ctx context.Context, paneID entity.PaneID, url string) {
251	if c.navigateUC == nil {
252		return
253	}
254
255	c.navigateUC.RecordHistory(ctx, string(paneID), url)
256}
257
258// ClearPaneHistory clears per-pane navigation history deduplication state.
259func (c *NavigationCoordinator) ClearPaneHistory(paneID entity.PaneID) {
260	if c.navigateUC == nil {
261		return
262	}
263	c.navigateUC.ClearPaneHistory(string(paneID))
264}
265
266// NotifyZoomChanged updates the omnibox zoom indicator.
267func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
268	if c.omniboxProvider != nil {
269		c.omniboxProvider.UpdateOmniboxZoom(factor)
270	}
271}

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