v0.23.1 6c872d5

navigation.go RawHistoryFolder

1package coordinator
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/dumber/internal/application/usecase"
8	"github.com/bnema/dumber/internal/domain/entity"
9	"github.com/bnema/dumber/internal/infrastructure/webkit"
10	"github.com/bnema/dumber/internal/logging"
11)
12
13// OmniboxProvider provides access to omnibox operations.
14type OmniboxProvider interface {
15	ToggleOmnibox(ctx context.Context)
16	UpdateOmniboxZoom(factor float64)
17	SetOmniboxOnNavigate(fn func(url string))
18}
19
20// NavigationCoordinator handles URL navigation, history, and browser controls.
21type NavigationCoordinator struct {
22	navigateUC      *usecase.NavigateUseCase
23	contentCoord    *ContentCoordinator
24	omniboxProvider OmniboxProvider
25}
26
27// NewNavigationCoordinator creates a new NavigationCoordinator.
28func NewNavigationCoordinator(
29	ctx context.Context,
30	navigateUC *usecase.NavigateUseCase,
31	contentCoord *ContentCoordinator,
32) *NavigationCoordinator {
33	log := logging.FromContext(ctx)
34	log.Debug().Msg("creating navigation coordinator")
35
36	return &NavigationCoordinator{
37		navigateUC:   navigateUC,
38		contentCoord: contentCoord,
39	}
40}
41
42// SetOmniboxProvider sets the omnibox provider for toggle/zoom operations.
43func (c *NavigationCoordinator) SetOmniboxProvider(provider OmniboxProvider) {
44	c.omniboxProvider = provider
45}
46
47// Navigate loads a URL in the active pane using NavigateUseCase.
48// This properly handles history recording and zoom application.
49func (c *NavigationCoordinator) Navigate(ctx context.Context, url string) error {
50	log := logging.FromContext(ctx)
51
52	wv := c.contentCoord.ActiveWebView(ctx)
53	if wv == nil {
54		log.Warn().Str("url", url).Msg("no active webview for navigation")
55		return fmt.Errorf("no active webview")
56	}
57
58	// Get active pane ID for tracking
59	ws, _ := c.contentCoord.getActiveWS()
60	var paneID string
61	if ws != nil {
62		if pane := ws.ActivePane(); pane != nil && pane.Pane != nil {
63			paneID = string(pane.Pane.ID)
64			// Track original URL for cross-domain redirect favicon caching
65			c.contentCoord.SetNavigationOrigin(pane.Pane.ID, url)
66			// Pre-load cached favicon asynchronously (don't block navigation start)
67			go c.contentCoord.PreloadCachedFavicon(ctx, pane.Pane.ID, url)
68		}
69	}
70
71	// Use NavigateUseCase which handles history + zoom
72	if c.navigateUC != nil {
73		input := usecase.NavigateInput{
74			URL:     url,
75			PaneID:  paneID,
76			WebView: wv, // webkit.WebView implements port.WebView
77		}
78		output, err := c.navigateUC.Execute(ctx, input)
79		if err != nil {
80			log.Error().Err(err).Str("url", url).Msg("navigation failed")
81			return err
82		}
83		log.Debug().
84			Str("url", url).
85			Float64("zoom", output.AppliedZoom).
86			Msg("navigation initiated via usecase")
87		return nil
88	}
89
90	// Fallback: direct navigation without usecase
91	log.Warn().Msg("navigateUC not available, using direct LoadURI")
92	if err := wv.LoadURI(ctx, url); err != nil {
93		log.Error().Err(err).Str("url", url).Msg("failed to navigate")
94		return err
95	}
96
97	log.Debug().Str("url", url).Msg("navigated active pane (direct)")
98	return nil
99}
100
101// Reload reloads the current page.
102func (c *NavigationCoordinator) Reload(ctx context.Context) error {
103	log := logging.FromContext(ctx)
104
105	wv := c.contentCoord.ActiveWebView(ctx)
106	if wv == nil {
107		log.Debug().Msg("no active webview for reload")
108		return nil
109	}
110
111	if c.navigateUC != nil {
112		return c.navigateUC.Reload(ctx, wv, false)
113	}
114
115	return wv.Reload(ctx)
116}
117
118// HardReload reloads the current page bypassing cache.
119func (c *NavigationCoordinator) HardReload(ctx context.Context) error {
120	log := logging.FromContext(ctx)
121
122	wv := c.contentCoord.ActiveWebView(ctx)
123	if wv == nil {
124		log.Debug().Msg("no active webview for hard reload")
125		return nil
126	}
127
128	if c.navigateUC != nil {
129		return c.navigateUC.Reload(ctx, wv, true)
130	}
131
132	return wv.ReloadBypassCache(ctx)
133}
134
135// GoBack navigates back in history.
136func (c *NavigationCoordinator) GoBack(ctx context.Context) error {
137	log := logging.FromContext(ctx)
138
139	wv := c.contentCoord.ActiveWebView(ctx)
140	if wv == nil {
141		log.Debug().Msg("no active webview for go back")
142		return nil
143	}
144
145	if c.navigateUC != nil {
146		return c.navigateUC.GoBack(ctx, wv)
147	}
148
149	return wv.GoBack(ctx)
150}
151
152// GoForward navigates forward in history.
153func (c *NavigationCoordinator) GoForward(ctx context.Context) error {
154	log := logging.FromContext(ctx)
155
156	wv := c.contentCoord.ActiveWebView(ctx)
157	if wv == nil {
158		log.Debug().Msg("no active webview for go forward")
159		return nil
160	}
161
162	if c.navigateUC != nil {
163		return c.navigateUC.GoForward(ctx, wv)
164	}
165
166	return wv.GoForward(ctx)
167}
168
169// OpenOmnibox toggles the omnibox visibility.
170func (c *NavigationCoordinator) OpenOmnibox(ctx context.Context) error {
171	log := logging.FromContext(ctx)
172
173	if c.omniboxProvider == nil {
174		log.Error().Msg("omnibox provider not initialized")
175		return fmt.Errorf("omnibox provider not initialized")
176	}
177
178	log.Debug().Msg("toggling omnibox")
179	c.omniboxProvider.ToggleOmnibox(ctx)
180	return nil
181}
182
183// OpenDevTools opens the WebKit inspector for the active WebView.
184func (c *NavigationCoordinator) OpenDevTools(ctx context.Context) error {
185	log := logging.FromContext(ctx)
186
187	wv := c.contentCoord.ActiveWebView(ctx)
188	if wv == nil {
189		log.Warn().Msg("no active webview for devtools")
190		return fmt.Errorf("no active webview")
191	}
192
193	log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening devtools")
194
195	// Type assert to access ShowDevTools (not in port.WebView interface)
196	if webkitWV, ok := interface{}(wv).(*webkit.WebView); ok {
197		return webkitWV.ShowDevTools()
198	}
199
200	return fmt.Errorf("webview does not support devtools")
201}
202
203// UpdateHistoryTitle updates the title of a history entry after page load.
204func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
205	log := logging.FromContext(ctx)
206
207	if c.navigateUC == nil {
208		return
209	}
210
211	if err := c.navigateUC.UpdateHistoryTitle(ctx, url, title); err != nil {
212		log.Warn().Err(err).Str("url", url).Msg("failed to update history title")
213	}
214}
215
216// RecordHistory records a URL in history on page commit.
217func (c *NavigationCoordinator) RecordHistory(ctx context.Context, paneID entity.PaneID, url string) {
218	if c.navigateUC == nil {
219		return
220	}
221
222	c.navigateUC.RecordHistory(ctx, url)
223}
224
225// ActiveWebView returns the WebView for the active pane (for zoom operations).
226func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
227	return c.contentCoord.ActiveWebView(ctx)
228}
229
230// NotifyZoomChanged updates the omnibox zoom indicator.
231func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
232	if c.omniboxProvider != nil {
233		c.omniboxProvider.UpdateOmniboxZoom(factor)
234	}
235}

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