v0.27.1 c78ce83

navigation.go RawHistoryFolder

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

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