v0.31.0 bf0756b

popup_hosting.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"errors"
6	"sync"
7
8	"github.com/bnema/dumber/internal/application/dto"
9	"github.com/bnema/dumber/internal/application/port"
10	"github.com/bnema/dumber/internal/domain/entity"
11	"github.com/bnema/dumber/internal/logging"
12)
13
14// openBrowserWindow transfers a related WebView to a complete browser window.
15// The popup manager retains ownership until the host callback succeeds.
16func (pm *popupManager) openBrowserWindow(
17	ctx context.Context,
18	hooks popupCoordinatorHooks,
19	parentPaneID entity.PaneID,
20	parentWebViewID port.WebViewID,
21	req port.PopupRequest,
22	decision dto.HostDecision,
23	ready bool,
24) port.WebView {
25	log := logging.FromContext(ctx)
26	normalized := buildPopupBrowsingContextRequest(req)
27	normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
28	if pm.onOpenBrowserWindow == nil {
29		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
30		return nil
31	}
32	popupWV, err := pm.createPopupWebView(ctx, parentWebViewID, req.TargetURI, true)
33	if err != nil {
34		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
35		return nil
36	}
37	pm.setBrowsingContextDecision(popupWV, decision)
38	if !pm.adoptPopupInBrowserWindow(
39		ctx, hooks, parentPaneID, parentWebViewID, popupWV, req, decision, ready,
40		dto.BrowsingContextFailureHostUnavailable, dto.BrowsingContextFailureHostFailed,
41		"browser window host returned an empty window ID",
42	) {
43		popupWV.Destroy()
44		return nil
45	}
46	if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
47		lifecycle.PrimePopupNavigation(req.TargetURI)
48	}
49	return popupWV
50}
51
52// openExistingPopupInBrowserWindow attempts to transfer an already-created
53// WebView. On failure ownership remains with the caller.
54func (pm *popupManager) openExistingPopupInBrowserWindow(
55	ctx context.Context,
56	hooks popupCoordinatorHooks,
57	parentPaneID entity.PaneID,
58	parentWebViewID port.WebViewID,
59	popupWV port.WebView,
60	req port.PopupRequest,
61	decision dto.HostDecision,
62	ready bool,
63) bool {
64	return pm.adoptPopupInBrowserWindow(
65		ctx, hooks, parentPaneID, parentWebViewID, popupWV, req, decision, ready,
66		dto.BrowsingContextFailureFallback, dto.BrowsingContextFailureFallback,
67		"browser window fallback returned an empty window ID",
68	)
69}
70
71func (pm *popupManager) adoptPopupInBrowserWindow(
72	ctx context.Context,
73	hooks popupCoordinatorHooks,
74	parentPaneID entity.PaneID,
75	parentWebViewID port.WebViewID,
76	popupWV port.WebView,
77	req port.PopupRequest,
78	decision dto.HostDecision,
79	ready bool,
80	unavailableCode dto.BrowsingContextFailureCode,
81	failureCode dto.BrowsingContextFailureCode,
82	emptyWindowError string,
83) bool {
84	log := logging.FromContext(ctx)
85	normalized := buildPopupBrowsingContextRequest(req)
86	normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
87	if popupWV == nil || pm.onOpenBrowserWindow == nil {
88		logBrowsingContextFailure(*log, normalized, decision, unavailableCode, nil)
89		return false
90	}
91
92	paneID, popupPane := pm.createPopupPane(popupWV.ID(), parentPaneID, req.TargetURI)
93	if hooks.setupWebViewCallbacks != nil {
94		hooks.setupWebViewCallbacks(ctx, paneID, popupWV)
95	}
96	result, err := pm.onOpenBrowserWindow(ctx, BrowserWindowInput{
97		ParentPaneID: parentPaneID, ParentWebViewID: parentWebViewID, PopupPane: popupPane,
98		PopupWebView: popupWV, TargetURI: req.TargetURI, Request: req, Ready: ready,
99	})
100	if err != nil || result.WindowID == "" {
101		if err == nil {
102			err = errors.New(emptyWindowError)
103		}
104		logBrowsingContextFailure(*log, normalized, decision, failureCode, err)
105		return false
106	}
107	if !req.NoJavaScriptAccess {
108		pm.storeReusableNamedPopupWithHost(parentPaneID, req.FrameName, paneID, popupWV, result.WindowID)
109	}
110	return true
111}
112
113func (pm *popupManager) navigatePopupSource(
114	ctx context.Context,
115	parentID port.WebViewID,
116	parentWV port.WebView,
117	req port.PopupRequest,
118	request dto.NewBrowsingContextRequest,
119	decision dto.HostDecision,
120) {
121	log := logging.FromContext(ctx)
122	popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
123	if err != nil {
124		logBrowsingContextFailure(*log, request, decision, dto.BrowsingContextFailureHostFailed, err)
125		return
126	}
127	pm.setBrowsingContextDecision(popupWV, decision)
128	popupWV.Destroy()
129	if err := parentWV.LoadURI(ctx, req.TargetURI); err != nil {
130		logBrowsingContextFailure(*log, request, decision, dto.BrowsingContextFailureHostFailed, err)
131	}
132}
133
134func (*popupManager) navigateSource(ctx context.Context, parentWV port.WebView, uri string) bool {
135	if err := parentWV.LoadURI(ctx, uri); err != nil {
136		logging.FromContext(ctx).Error().Err(err).
137			Str("uri", logging.TruncateURL(uri, logURLMaxLen)).
138			Msg("failed to load URI in source floating pane")
139		return false
140	}
141	return true
142}
143
144func popupSupportsOpenerBridge(wv port.WebView) bool {
145	_, ok := wv.(port.PopupOpenerCapable)
146	return ok
147}
148
149func (pm *popupManager) openNativePopup(
150	ctx context.Context,
151	hooks popupCoordinatorHooks,
152	parentPaneID entity.PaneID,
153	parentID port.WebViewID,
154	parentURIAtOpen string,
155	req port.PopupRequest,
156	decision dto.HostDecision,
157) port.WebView {
158	log := logging.FromContext(ctx)
159	normalized := buildPopupBrowsingContextRequest(req)
160	normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
161	if pm.onOpenNativePopup == nil {
162		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
163		return nil
164	}
165	popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
166	if err != nil {
167		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
168		return nil
169	}
170	pm.setBrowsingContextDecision(popupWV, decision)
171	if !pm.adoptPopupInNativePopup(
172		ctx, hooks, parentPaneID, parentID, parentURIAtOpen, popupWV, req, decision,
173		dto.BrowsingContextFailureNativeArm,
174	) {
175		popupWV.Destroy()
176		return nil
177	}
178	return popupWV
179}
180
181// openExistingPopupInNativePopup attempts to transfer an already-created
182// WebView. On failure ownership remains with the caller.
183func (pm *popupManager) openExistingPopupInNativePopup(
184	ctx context.Context,
185	hooks popupCoordinatorHooks,
186	parentPaneID entity.PaneID,
187	parentWebViewID port.WebViewID,
188	parentURIAtOpen string,
189	popupWV port.WebView,
190	req port.PopupRequest,
191	decision dto.HostDecision,
192) bool {
193	return pm.adoptPopupInNativePopup(
194		ctx, hooks, parentPaneID, parentWebViewID, parentURIAtOpen, popupWV, req, decision,
195		dto.BrowsingContextFailureHostFailed,
196	)
197}
198
199func (pm *popupManager) adoptPopupInNativePopup(
200	ctx context.Context,
201	hooks popupCoordinatorHooks,
202	parentPaneID entity.PaneID,
203	parentWebViewID port.WebViewID,
204	parentURIAtOpen string,
205	popupWV port.WebView,
206	req port.PopupRequest,
207	decision dto.HostDecision,
208	failureCode dto.BrowsingContextFailureCode,
209) bool {
210	log := logging.FromContext(ctx)
211	normalized := buildPopupBrowsingContextRequest(req)
212	normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
213	if popupWV == nil || pm.onOpenNativePopup == nil {
214		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
215		return false
216	}
217	cfg := pm.currentPopupConfig()
218	allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener &&
219		(req.NoJavaScriptAccess || popupSupportsOpenerBridge(popupWV))
220	var abortOnce sync.Once
221	abortResult := false
222	onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
223		abortOnce.Do(func() {
224			if !allowFallback {
225				logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
226				return
227			}
228			abortResult = pm.openExistingPopupInBrowserWindow(
229				abortCtx, hooks, parentPaneID, parentWebViewID, abortedWV, req, decision, true,
230			)
231		})
232		return abortResult
233	}
234	if err := pm.onOpenNativePopup(ctx, NativePopupInput{
235		ParentPaneID: parentPaneID, ParentWebViewID: parentWebViewID, ParentURIAtOpen: parentURIAtOpen,
236		PopupWebView: popupWV, TargetURI: req.TargetURI, Request: req,
237		ObserveOAuthAutoClose:      cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
238		AllowBrowserWindowFallback: allowFallback, OnNativeHostAbort: onAbort,
239	}); err != nil {
240		logBrowsingContextFailure(*log, normalized, decision, failureCode, err)
241		return false
242	}
243	if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
244		lifecycle.PrimePopupNavigation(req.TargetURI)
245	}
246	return true
247}

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