main 5041353

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

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