v0.28.0 3cb3770

coordinator.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"sync"
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
12	"github.com/bnema/dumber/internal/logging"
13	"github.com/bnema/dumber/internal/ui/adapter"
14	"github.com/bnema/dumber/internal/ui/component"
15	"github.com/bnema/dumber/internal/ui/input"
16	"github.com/bnema/dumber/internal/ui/layout"
17)
18
19// Coordinator manages WebView lifecycle, title tracking, and content attachment.
20type Coordinator struct {
21	pool            port.WebViewPool
22	widgetFactory   layout.WidgetFactory
23	faviconAdapter  *adapter.FaviconAdapter
24	zoomUC          *usecase.ManageZoomUseCase
25	permissionUC    *usecase.HandlePermissionUseCase
26	injector        port.ContentInjector
27	settingsApplier port.SettingsApplier // optional: nil if engine doesn't support
28	filterApplier   port.FilterApplier   // optional: nil if engine doesn't support
29
30	webViews   map[entity.PaneID]port.WebView
31	webViewsMu sync.RWMutex
32
33	activePaneOverride   entity.PaneID
34	activePaneOverrideMu sync.RWMutex
35
36	paneTitles map[entity.PaneID]string
37	titleMu    sync.RWMutex
38
39	// Track original navigation URLs to handle cross-domain redirects
40	// e.g., google.fr → google.com: cache favicon under both domains
41	navOrigins  map[entity.PaneID]string
42	navOriginMu sync.RWMutex
43
44	// Callback to get active workspace state (avoids circular dependency)
45	getActiveWS func() (*entity.Workspace, *component.WorkspaceView)
46
47	// Callback when title changes (for history persistence)
48	onTitleUpdated func(ctx context.Context, paneID entity.PaneID, url, title string)
49
50	// Callback when page is committed (for history recording)
51	onHistoryRecord func(ctx context.Context, paneID entity.PaneID, url string)
52
53	// Callback when pane URI changes (for session snapshots)
54	onPaneURIUpdated func(paneID entity.PaneID, url string)
55
56	// Callback when active pane title changes (for window title updates)
57	onWindowTitleChanged func(title string)
58
59	// Callback when media permission activity changes (requesting/allowed/blocked).
60	onPermissionActivity func(origin string, permTypes []entity.PermissionType, state PermissionActivityState)
61
62	// Callback when the active pane commits a navigation (new page loading).
63	onActiveNavigationCommitted func(uri string)
64
65	// Callback when the WebView becomes visible (first real commit)
66	onWebViewShown func(paneID entity.PaneID)
67
68	revealMu      sync.Mutex
69	pendingReveal map[entity.PaneID]bool
70
71	appearanceMu          sync.Mutex
72	pendingScriptRefresh  map[entity.PaneID]bool
73	pendingThemePanes     map[entity.PaneID]bool
74	pendingThemeUpdate    pendingThemeUpdate
75	hasPendingThemeUpdate bool
76	currentTheme          pendingThemeUpdate
77	hasCurrentTheme       bool
78
79	// Gesture action handler for mouse button navigation
80	gestureActionHandler input.ActionHandler
81
82	// Popup handling
83	factory       port.WebViewFactory
84	popupConfig   *entity.PopupBehaviorConfig
85	pendingPopups map[port.WebViewID]*PendingPopup
86	popupOAuth    map[port.WebViewID]*popupOAuthState
87	popupRefresh  map[entity.PaneID]*time.Timer
88	popupMu       sync.RWMutex
89
90	// Callback to insert popup into workspace (avoids circular dependency)
91	onInsertPopup func(ctx context.Context, input InsertPopupInput) error
92
93	// Callback to close a pane when popup closes
94	onClosePane func(ctx context.Context, paneID entity.PaneID) error
95
96	// ID generator for popup panes
97	generateID func() string
98
99	// Idle inhibitor for fullscreen video playback
100	idleInhibitor port.IdleInhibitor
101
102	// Callback when fullscreen state changes (for hiding/showing tab bar)
103	onFullscreenChanged func(entering bool)
104
105	// Callback when WebView gains focus (for accent picker text input targeting)
106	onWebViewFocused func(paneID entity.PaneID, wv port.WebView)
107
108	// Callback for first load_started event (triggers deferred initialization)
109	onFirstLoadStarted func()
110	loadStartedOnce    sync.Once
111
112	// Callback to open a URL with the system's default handler (e.g. xdg-open).
113	// Used for external URL schemes like vscode://, spotify://, etc.
114	onLaunchExternalURL func(uri string)
115}
116
117type pendingThemeUpdate struct {
118	prefersDark bool
119	cssText     string
120}
121
122type popupOAuthState struct {
123	ParentPaneID entity.PaneID
124	CallbackURI  string
125	Success      bool
126	Error        bool
127	Seen         bool
128}
129
130// PermissionActivityState represents the visible state for media permission activity.
131type PermissionActivityState string
132
133const (
134	PermissionActivityRequesting PermissionActivityState = "requesting"
135	PermissionActivityAllowed    PermissionActivityState = "allowed"
136	PermissionActivityBlocked    PermissionActivityState = "blocked"
137)
138
139// NewCoordinator creates a new Coordinator.
140func NewCoordinator(
141	ctx context.Context,
142	pool port.WebViewPool,
143	injector port.ContentInjector,
144	widgetFactory layout.WidgetFactory,
145	faviconAdapter *adapter.FaviconAdapter,
146	getActiveWS func() (*entity.Workspace, *component.WorkspaceView),
147	zoomUC *usecase.ManageZoomUseCase,
148	permissionUC *usecase.HandlePermissionUseCase,
149) *Coordinator {
150	log := logging.FromContext(ctx)
151	log.Debug().Msg("creating content coordinator")
152
153	return &Coordinator{
154		pool:                 pool,
155		injector:             injector,
156		widgetFactory:        widgetFactory,
157		faviconAdapter:       faviconAdapter,
158		zoomUC:               zoomUC,
159		permissionUC:         permissionUC,
160		webViews:             make(map[entity.PaneID]port.WebView),
161		paneTitles:           make(map[entity.PaneID]string),
162		navOrigins:           make(map[entity.PaneID]string),
163		pendingReveal:        make(map[entity.PaneID]bool),
164		pendingScriptRefresh: make(map[entity.PaneID]bool),
165		pendingThemePanes:    make(map[entity.PaneID]bool),
166		getActiveWS:          getActiveWS,
167		pendingPopups:        make(map[port.WebViewID]*PendingPopup),
168		popupOAuth:           make(map[port.WebViewID]*popupOAuthState),
169		popupRefresh:         make(map[entity.PaneID]*time.Timer),
170	}
171}
172
173// SetOnTitleUpdated sets the callback for title changes (for history persistence).
174func (c *Coordinator) SetOnTitleUpdated(fn func(ctx context.Context, paneID entity.PaneID, url, title string)) {
175	c.onTitleUpdated = fn
176}
177
178// SetOnHistoryRecord sets the callback for recording history on page commit.
179func (c *Coordinator) SetOnHistoryRecord(fn func(ctx context.Context, paneID entity.PaneID, url string)) {
180	c.onHistoryRecord = fn
181}
182
183// SetOnPermissionActivity sets a callback for WebRTC permission activity changes.
184func (c *Coordinator) SetOnPermissionActivity(
185	fn func(origin string, permTypes []entity.PermissionType, state PermissionActivityState),
186) {
187	c.onPermissionActivity = fn
188}
189
190// SetOnActiveNavigationCommitted sets a callback fired when the active pane commits a navigation.
191func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(uri string)) {
192	c.onActiveNavigationCommitted = fn
193}
194
195// SetOnPaneURIUpdated sets the callback for pane URI changes (for session snapshots).
196func (c *Coordinator) SetOnPaneURIUpdated(fn func(paneID entity.PaneID, url string)) {
197	c.onPaneURIUpdated = fn
198}
199
200// SetOnWindowTitleChanged sets the callback for active pane title changes (for window title updates).
201func (c *Coordinator) SetOnWindowTitleChanged(fn func(title string)) {
202	c.onWindowTitleChanged = fn
203}
204
205// SetOnWebViewShown sets a callback that fires when a pane's WebView is shown.
206func (c *Coordinator) SetOnWebViewShown(fn func(paneID entity.PaneID)) {
207	c.onWebViewShown = fn
208}
209
210// SetGestureActionHandler sets the callback for mouse button navigation gestures.
211func (c *Coordinator) SetGestureActionHandler(handler input.ActionHandler) {
212	c.gestureActionHandler = handler
213}
214
215// SetIdleInhibitor sets the idle inhibitor for fullscreen video playback.
216func (c *Coordinator) SetIdleInhibitor(inhibitor port.IdleInhibitor) {
217	c.idleInhibitor = inhibitor
218}
219
220// SetOnFullscreenChanged sets the callback for fullscreen state changes.
221func (c *Coordinator) SetOnFullscreenChanged(fn func(entering bool)) {
222	c.onFullscreenChanged = fn
223}
224
225// SetOnWebViewFocused sets the callback for when a WebView gains focus.
226func (c *Coordinator) SetOnWebViewFocused(fn func(paneID entity.PaneID, wv port.WebView)) {
227	c.onWebViewFocused = fn
228}
229
230// SetOnFirstLoadStarted sets the callback for when the first navigation starts.
231// This is used to trigger deferred initialization after the initial load_uri()
232// has been processed by the GTK main loop.
233func (c *Coordinator) SetOnFirstLoadStarted(fn func()) {
234	c.onFirstLoadStarted = fn
235}
236
237// SetOnLaunchExternalURL sets the callback invoked when an external URL scheme
238// (e.g. vscode://, spotify://) is detected and must be handed off to the system.
239func (c *Coordinator) SetOnLaunchExternalURL(fn func(uri string)) {
240	c.onLaunchExternalURL = fn
241}
242
243// SetSettingsApplier sets the engine settings applier for config hot-reload.
244func (c *Coordinator) SetSettingsApplier(sa port.SettingsApplier) {
245	c.settingsApplier = sa
246}
247
248// SetFilterApplier sets the content filter applier for late-binding filters.
249func (c *Coordinator) SetFilterApplier(fa port.FilterApplier) {
250	c.filterApplier = fa
251}
252
253// ActivePaneID returns the currently active pane ID used by navigation.
254func (c *Coordinator) ActivePaneID(ctx context.Context) entity.PaneID {
255	if paneID, ok := c.activePaneOverrideID(); ok {
256		return paneID
257	}
258	if c.getActiveWS == nil {
259		return ""
260	}
261
262	ws, _ := c.getActiveWS()
263	if ws == nil {
264		return ""
265	}
266	return ws.ActivePaneID
267}
268
269// SetActivePaneOverride forces ActiveWebView/ActivePaneID to use a specific pane.
270func (c *Coordinator) SetActivePaneOverride(paneID entity.PaneID) {
271	c.activePaneOverrideMu.Lock()
272	c.activePaneOverride = paneID
273	c.activePaneOverrideMu.Unlock()
274}
275
276// ClearActivePaneOverride removes any forced active pane override.
277func (c *Coordinator) ClearActivePaneOverride() {
278	c.activePaneOverrideMu.Lock()
279	c.activePaneOverride = ""
280	c.activePaneOverrideMu.Unlock()
281}
282
283func (c *Coordinator) activePaneOverrideID() (entity.PaneID, bool) {
284	c.activePaneOverrideMu.RLock()
285	defer c.activePaneOverrideMu.RUnlock()
286	if c.activePaneOverride == "" {
287		return "", false
288	}
289	return c.activePaneOverride, true
290}
291
292func (c *Coordinator) webViewCount() int {
293	c.webViewsMu.RLock()
294	defer c.webViewsMu.RUnlock()
295	return len(c.webViews)
296}
297
298func (c *Coordinator) getWebViewLocked(paneID entity.PaneID) port.WebView {
299	c.webViewsMu.RLock()
300	defer c.webViewsMu.RUnlock()
301	return c.webViews[paneID]
302}
303
304func (c *Coordinator) setWebViewLocked(paneID entity.PaneID, wv port.WebView) {
305	c.webViewsMu.Lock()
306	c.webViews[paneID] = wv
307	c.webViewsMu.Unlock()
308}
309
310func (c *Coordinator) deleteWebViewLocked(paneID entity.PaneID) port.WebView {
311	c.webViewsMu.Lock()
312	defer c.webViewsMu.Unlock()
313	wv := c.webViews[paneID]
314	delete(c.webViews, paneID)
315	return wv
316}

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