v0.32.0 0617278

popup_deferred.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"time"
6
7	"github.com/bnema/dumber/internal/application/dto"
8	"github.com/bnema/dumber/internal/application/port"
9	"github.com/bnema/dumber/internal/domain/entity"
10	"github.com/bnema/dumber/internal/logging"
11)
12
13func (pm *popupManager) awaitPopupFeatures(
14	ctx context.Context,
15	hooks popupCoordinatorHooks,
16	parentPaneID entity.PaneID,
17	parentWebViewID port.WebViewID,
18	parentWebView port.WebView,
19	parentURIAtOpen string,
20	req port.PopupRequest,
21	decision dto.HostDecision,
22) port.WebView {
23	log := logging.FromContext(ctx)
24	normalized := buildPopupBrowsingContextRequest(req)
25	normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
26	popupWV, err := pm.createPopupWebView(ctx, parentWebViewID, req.TargetURI, false)
27	if err != nil {
28		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
29		return nil
30	}
31	resolver, resolverOK := popupWV.(port.PopupFeatureResolver)
32	lifecycle, lifecycleOK := popupWV.(port.PopupLifecycleCapable)
33	if !resolverOK || !lifecycleOK || pm.onStagePopup == nil {
34		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
35		popupWV.Destroy()
36		return nil
37	}
38	staging, err := pm.onStagePopup(ctx, StagePopupInput{PopupWebView: popupWV})
39	if err != nil || staging == nil {
40		logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
41		popupWV.Destroy()
42		return nil
43	}
44	pm.setBrowsingContextDecision(popupWV, decision)
45	paneID, _ := pm.createPopupPane(popupWV.ID(), parentPaneID, req.TargetURI)
46	ownerWindowID := ""
47	if pm.windowIDForPane != nil {
48		ownerWindowID, _ = pm.windowIDForPane(parentPaneID)
49	}
50	pending := &deferredPendingPopup{
51		PendingPopup: &PendingPopup{
52			PaneID: paneID, WebView: popupWV, ParentPaneID: parentPaneID,
53			ParentWebViewID: parentWebViewID, TargetURI: req.TargetURI, FrameName: req.FrameName,
54			IsUserGesture: req.IsUserGesture, PopupType: DetectPopupType(req.FrameName), CreatedAt: time.Now(),
55		},
56		Request: req, Decision: decision, StagingHost: staging, OwnerWindowID: ownerWindowID,
57		ParentWebView: parentWebView,
58	}
59	pm.storeDeferredPopup(popupWV.ID(), pending)
60	callbackCtx := logging.WithContext(context.Background(), *log)
61	lifecycle.SetOnReadyToShow(func() {
62		pm.resolvePendingPopupFeatures(callbackCtx, hooks, popupWV.ID(), parentURIAtOpen, resolver)
63	})
64	lifecycle.SetOnClose(func() {
65		pm.failPendingPopup(callbackCtx, popupWV.ID(), dto.BrowsingContextFailureHostFailed, nil)
66	})
67	return popupWV
68}
69
70func (pm *popupManager) cleanupDeferredPending(pending *deferredPendingPopup) {
71	if pending == nil {
72		return
73	}
74	pending.cleanupOnce.Do(func() {
75		if lifecycle, ok := pending.WebView.(port.PopupLifecycleCapable); ok {
76			lifecycle.SetOnReadyToShow(nil)
77			lifecycle.SetOnClose(nil)
78		}
79		if pending.StagingHost != nil {
80			pending.StagingHost.Destroy()
81		}
82		if pending.WebView != nil {
83			pending.WebView.Destroy()
84		}
85	})
86}
87
88func (pm *popupManager) cancelDeferredPopupsMatching(match func(*deferredPendingPopup) bool) {
89	if pm == nil || match == nil {
90		return
91	}
92	pm.mu.Lock()
93	canceled := make([]*deferredPendingPopup, 0)
94	for popupID, pending := range pm.deferredPopups {
95		if pending == nil || !match(pending) {
96			continue
97		}
98		delete(pm.deferredPopups, popupID)
99		delete(pm.pendingPopups, popupID)
100		canceled = append(canceled, pending)
101	}
102	pm.mu.Unlock()
103	for _, pending := range canceled {
104		pm.cleanupDeferredPending(pending)
105	}
106}
107
108func (pm *popupManager) cancelDeferredPopupsForParent(parentPaneID entity.PaneID) {
109	pm.cancelDeferredPopupsMatching(func(pending *deferredPendingPopup) bool {
110		return pending.ParentPaneID == parentPaneID
111	})
112}
113
114func (pm *popupManager) cancelDeferredPopupsForWindow(windowID string) {
115	if pm == nil || windowID == "" {
116		return
117	}
118	pm.cancelDeferredPopupsMatching(func(pending *deferredPendingPopup) bool {
119		return pending.OwnerWindowID == windowID
120	})
121}
122
123func (pm *popupManager) cancelAllDeferredPopups() {
124	pm.cancelDeferredPopupsMatching(func(*deferredPendingPopup) bool { return true })
125}
126
127func (pm *popupManager) failPendingPopup(
128	ctx context.Context,
129	popupID port.WebViewID,
130	code dto.BrowsingContextFailureCode,
131	err error,
132) {
133	pending, ok := pm.takeDeferredPopup(popupID)
134	if !ok || pending == nil {
135		return
136	}
137	req := buildPopupBrowsingContextRequest(pending.Request)
138	req.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
139	logBrowsingContextFailure(*logging.FromContext(ctx), req, pending.Decision, code, err)
140	pm.cleanupDeferredPending(pending)
141}
142
143func (pm *popupManager) resolvePendingPopupFeatures(
144	ctx context.Context,
145	hooks popupCoordinatorHooks,
146	popupID port.WebViewID,
147	parentURIAtOpen string,
148	resolver port.PopupFeatureResolver,
149) {
150	pending, ok := pm.takeDeferredPopup(popupID)
151	if !ok || pending == nil {
152		return
153	}
154	resolved := resolver.ResolvePopupFeatures()
155	if resolved.State == dto.PopupFeaturesUnknown {
156		req := buildPopupBrowsingContextRequest(pending.Request)
157		req.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
158		logBrowsingContextFailure(*logging.FromContext(ctx), req, pending.Decision, dto.BrowsingContextFailureFeatureResolution, nil)
159		pm.cleanupDeferredPending(pending)
160		return
161	}
162	if err := pending.StagingHost.Detach(); err != nil {
163		req := buildPopupBrowsingContextRequest(pending.Request)
164		req.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
165		logBrowsingContextFailure(*logging.FromContext(ctx), req, pending.Decision, dto.BrowsingContextFailureStagingDetach, err)
166		pm.cleanupDeferredPending(pending)
167		return
168	}
169
170	request := pending.Request
171	request.PopupFeatures = resolved
172	normalized := buildPopupBrowsingContextRequest(request)
173	normalized.SourceHost = pm.resolveSourceHost(pending.ParentPaneID)
174	namedContextExists := false
175	if !request.NoJavaScriptAccess {
176		_, namedContextExists = pm.lookupReusableNamedPopup(pending.ParentPaneID, request.FrameName, hooks)
177	}
178	decision := pm.policy.Decide(normalized, namedContextExists)
179	logBrowsingContextDecision(*logging.FromContext(ctx), normalized, decision)
180	pm.setBrowsingContextDecision(pending.WebView, decision)
181
182	transferred := false
183	switch decision.Kind {
184	case dto.HostDecisionNavigateSource:
185		if pending.ParentWebView == nil {
186			logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
187			break
188		}
189		if err := pending.ParentWebView.LoadURI(ctx, request.TargetURI); err != nil {
190			logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureHostFailed, err)
191		}
192	case dto.HostDecisionCreateBrowserWindow:
193		transferred = pm.openExistingPopupInBrowserWindow(
194			ctx, hooks, pending.ParentPaneID, pending.ParentWebViewID,
195			pending.WebView, request, decision, true,
196		)
197	case dto.HostDecisionCreateNativePopup:
198		transferred = pm.openExistingPopupInNativePopup(
199			ctx, hooks, pending.ParentPaneID, pending.ParentWebViewID,
200			parentURIAtOpen, pending.WebView, request, decision,
201		)
202	case dto.HostDecisionReuseNamedPane:
203		// Reuse transfers no ownership: the existing context is navigated while
204		// the newly staged WebView remains ours and must be discarded below.
205		if _, ok := pm.reuseNamedPopup(ctx, hooks, pending.ParentPaneID, request.FrameName, request.TargetURI); !ok {
206			logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
207		}
208	default:
209		logBrowsingContextFailure(*logging.FromContext(ctx), normalized, decision, dto.BrowsingContextFailureFeatureResolution, nil)
210	}
211	if !transferred {
212		pm.cleanupDeferredPending(pending)
213	}
214}

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