v0.28.0 3cb3770

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	SetOmniboxOnNavigate(fn func(url string))
20}
21
22// NavigationCoordinator handles URL navigation, history, and browser controls.
23type NavigationCoordinator struct {
24	contextProvider func() context.Context
25	navigateUC      *usecase.NavigateUseCase
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	log := logging.FromContext(ctx)
39	log.Debug().Msg("creating navigation coordinator")
40	callbackLogger := *log
41
42	return &NavigationCoordinator{
43		contextProvider: func() context.Context {
44			return logging.WithContext(context.Background(), callbackLogger)
45		},
46		navigateUC:   navigateUC,
47		contentCoord: contentCoord,
48	}
49}
50
51// SetOmniboxProvider sets the omnibox provider for toggle/zoom operations.
52func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) {
53	c.omniboxProvider = provider
54	if c.omniboxProvider != nil {
55		c.omniboxProvider.SetOmniboxOnNavigate(func(url string) {
56			navCtx := context.Background()
57			if c.contextProvider != nil {
58				navCtx = c.contextProvider()
59			}
60			if err := c.Navigate(navCtx, url); err != nil {
61				logging.FromContext(navCtx).Warn().Err(err).Str("url", url).Msg("omnibox-initiated navigation failed")
62			}
63		})
64	}
65}
66
67// Navigate loads a URL in the active pane using NavigateUseCase.
68// This properly handles history recording and zoom application.
69func (c *NavigationCoordinator) Navigate(ctx context.Context, url string) error {
70	log := logging.FromContext(ctx)
71	if c.contentCoord == nil {
72		log.Warn().Str("url", url).Msg("content coordinator not initialized")
73		return fmt.Errorf("content coordinator not initialized")
74	}
75
76	wv := c.contentCoord.ActiveWebView(ctx)
77	if wv == nil {
78		log.Warn().Str("url", url).Msg("no active webview for navigation")
79		return fmt.Errorf("no active webview")
80	}
81
82	// Get active pane ID for tracking
83	activePaneID := c.contentCoord.ActivePaneID(ctx)
84	paneID := string(activePaneID)
85	if activePaneID != "" {
86		// Track original URL for cross-domain redirect favicon caching
87		c.contentCoord.SetNavigationOrigin(activePaneID, url)
88		// Pre-load cached favicon asynchronously (don't block navigation start)
89		go func() {
90			preloadCtx, cancelPreload := context.WithTimeout(ctx, faviconPreloadTimeout)
91			defer cancelPreload()
92			c.contentCoord.PreloadCachedFavicon(preloadCtx, activePaneID, url)
93		}()
94	}
95
96	// Use NavigateUseCase which handles history + zoom
97	if c.navigateUC != nil {
98		input := usecase.NavigateInput{
99			URL:     url,
100			PaneID:  paneID,
101			WebView: wv, // webkit.WebView implements port.WebView
102		}
103		output, err := c.navigateUC.Execute(ctx, input)
104		if err != nil {
105			log.Error().Err(err).Str("url", url).Msg("navigation failed")
106			return err
107		}
108		log.Debug().
109			Str("url", url).
110			Float64("zoom", output.AppliedZoom).
111			Msg("navigation initiated via usecase")
112		return nil
113	}
114
115	// Fallback: direct navigation without usecase
116	log.Warn().Msg("navigateUC not available, using direct LoadURI")
117	if err := wv.LoadURI(ctx, url); err != nil {
118		log.Error().Err(err).Str("url", url).Msg("failed to navigate")
119		return err
120	}
121
122	log.Debug().Str("url", url).Msg("navigated active pane (direct)")
123	return nil
124}
125
126// Reload reloads the current page.
127func (c *NavigationCoordinator) Reload(ctx context.Context) error {
128	log := logging.FromContext(ctx)
129
130	wv := c.contentCoord.ActiveWebView(ctx)
131	if wv == nil {
132		log.Debug().Msg("no active webview for reload")
133		return nil
134	}
135
136	if c.navigateUC != nil {
137		return c.navigateUC.Reload(ctx, wv, false)
138	}
139
140	return wv.Reload(ctx)
141}
142
143// HardReload reloads the current page bypassing cache.
144func (c *NavigationCoordinator) HardReload(ctx context.Context) error {
145	log := logging.FromContext(ctx)
146
147	wv := c.contentCoord.ActiveWebView(ctx)
148	if wv == nil {
149		log.Debug().Msg("no active webview for hard reload")
150		return nil
151	}
152
153	if c.navigateUC != nil {
154		return c.navigateUC.Reload(ctx, wv, true)
155	}
156
157	return wv.ReloadBypassCache(ctx)
158}
159
160// GoBack navigates back in history.
161// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
162func (c *NavigationCoordinator) GoBack(ctx context.Context) error {
163	log := logging.FromContext(ctx)
164
165	wv := c.contentCoord.ActiveWebView(ctx)
166	if wv == nil {
167		log.Debug().Msg("no active webview for go back")
168		return nil
169	}
170
171	return wv.GoBack(ctx)
172}
173
174// GoForward navigates forward in history.
175// Calls webview directly - the webview layer handles SPA navigation via JS fallback.
176func (c *NavigationCoordinator) GoForward(ctx context.Context) error {
177	log := logging.FromContext(ctx)
178
179	wv := c.contentCoord.ActiveWebView(ctx)
180	if wv == nil {
181		log.Debug().Msg("no active webview for go forward")
182		return nil
183	}
184
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// OpenDevTools opens the WebKit inspector for the active WebView.
203func (c *NavigationCoordinator) OpenDevTools(ctx context.Context) error {
204	log := logging.FromContext(ctx)
205
206	wv := c.contentCoord.ActiveWebView(ctx)
207	if wv == nil {
208		log.Warn().Msg("no active webview for devtools")
209		return fmt.Errorf("no active webview")
210	}
211
212	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening devtools")
213
214	if opener, ok := wv.(port.DevToolsOpener); ok {
215		opener.OpenDevTools()
216		return nil
217	}
218
219	return fmt.Errorf("webview does not support devtools")
220}
221
222// PrintPage opens the print dialog for the active WebView.
223func (c *NavigationCoordinator) PrintPage(ctx context.Context) error {
224	log := logging.FromContext(ctx)
225
226	wv := c.contentCoord.ActiveWebView(ctx)
227	if wv == nil {
228		log.Warn().Msg("no active webview for print")
229		return fmt.Errorf("no active webview")
230	}
231
232	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening print dialog")
233
234	if printer, ok := wv.(port.Printer); ok {
235		printer.PrintPage()
236		return nil
237	}
238
239	return fmt.Errorf("webview does not support printing")
240}
241
242// UpdateHistoryTitle updates the title of a history entry after page load.
243func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
244	log := logging.FromContext(ctx)
245
246	if c.navigateUC == nil {
247		return
248	}
249
250	if err := c.navigateUC.UpdateHistoryTitle(ctx, url, title); err != nil {
251		log.Warn().Err(err).Str("url", url).Msg("failed to update history title")
252	}
253}
254
255// RecordHistory records a URL in history on page commit.
256func (c *NavigationCoordinator) RecordHistory(ctx context.Context, paneID entity.PaneID, url string) {
257	if c.navigateUC == nil {
258		return
259	}
260
261	c.navigateUC.RecordHistory(ctx, string(paneID), url)
262}
263
264// ClearPaneHistory clears per-pane navigation history deduplication state.
265func (c *NavigationCoordinator) ClearPaneHistory(paneID entity.PaneID) {
266	if c.navigateUC == nil {
267		return
268	}
269	c.navigateUC.ClearPaneHistory(string(paneID))
270}
271
272// ActiveWebView returns the WebView for the active pane (for zoom operations).
273func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) port.WebView {
274	return c.contentCoord.ActiveWebView(ctx)
275}
276
277// NotifyZoomChanged updates the omnibox zoom indicator.
278func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
279	if c.omniboxProvider != nil {
280		c.omniboxProvider.UpdateOmniboxZoom(factor)
281	}
282}

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