v0.30.0 eb3068f

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

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