v0.33.2 57f21d6

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, false,
174	) {
175		popupWV.Destroy()
176		return nil
177	}
178	return popupWV
179}
180
181// openExistingPopupInNativePopup attempts to transfer an already-created
182// WebView whose ready-to-show event has already fired (deferred feature
183// resolution). On failure ownership remains with the caller.
184func (pm *popupManager) openExistingPopupInNativePopup(
185	ctx context.Context,
186	hooks popupCoordinatorHooks,
187	parentPaneID entity.PaneID,
188	parentWebViewID port.WebViewID,
189	parentURIAtOpen string,
190	popupWV port.WebView,
191	req port.PopupRequest,
192	decision dto.HostDecision,
193) bool {
194	return pm.adoptPopupInNativePopup(
195		ctx, hooks, parentPaneID, parentWebViewID, parentURIAtOpen, popupWV, req, decision,
196		dto.BrowsingContextFailureHostFailed, true,
197	)
198}
199
200func (pm *popupManager) adoptPopupInNativePopup(
201	ctx context.Context,
202	hooks popupCoordinatorHooks,
203	parentPaneID entity.PaneID,
204	parentWebViewID port.WebViewID,
205	parentURIAtOpen string,
206	popupWV port.WebView,
207	req port.PopupRequest,
208	decision dto.HostDecision,
209	failureCode dto.BrowsingContextFailureCode,
210	alreadyReadyToShow bool,
211) bool {
212	log := logging.FromContext(ctx)
213	normalized := buildPopupBrowsingContextRequest(req)
214	normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
215	if popupWV == nil || pm.onOpenNativePopup == nil {
216		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
217		return false
218	}
219	cfg := pm.currentPopupConfig()
220	allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener &&
221		(req.NoJavaScriptAccess || popupSupportsOpenerBridge(popupWV))
222	var abortOnce sync.Once
223	abortResult := false
224	onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
225		abortOnce.Do(func() {
226			if !allowFallback {
227				logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
228				return
229			}
230			abortResult = pm.openExistingPopupInBrowserWindow(
231				abortCtx, hooks, parentPaneID, parentWebViewID, abortedWV, req, decision, true,
232			)
233		})
234		return abortResult
235	}
236	if err := pm.onOpenNativePopup(ctx, NativePopupInput{
237		ParentPaneID: parentPaneID, ParentWebViewID: parentWebViewID, ParentURIAtOpen: parentURIAtOpen,
238		PopupWebView: popupWV, TargetURI: req.TargetURI, Request: req,
239		ObserveOAuthAutoClose:      cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
240		AllowBrowserWindowFallback: allowFallback, OnNativeHostAbort: onAbort,
241		AlreadyReadyToShow: alreadyReadyToShow,
242	}); err != nil {
243		logBrowsingContextFailure(*log, normalized, decision, failureCode, err)
244		return false
245	}
246	if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
247		lifecycle.PrimePopupNavigation(req.TargetURI)
248	}
249	return true
250}

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