popup.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
13// PopupType indicates whether the popup was triggered by a link or JavaScript.
14type PopupType int
15
16const (
17	// PopupTypeTab represents a popup from target="_blank" links.
18	// These are typically user-initiated and open like new tabs.
19	PopupTypeTab PopupType = iota
20	// PopupTypePopup represents a popup from window.open() JavaScript calls.
21	// These may have different placement behavior than tab-like popups.
22	PopupTypePopup
23)
24
25// String returns a human-readable name for the popup type.
26func (t PopupType) String() string {
27	switch t {
28	case PopupTypeTab:
29		return "tab"
30	case PopupTypePopup:
31		return "popup"
32	default:
33		return "unknown"
34	}
35}
36
37// DetectPopupType determines if this is a tab-like or JS popup based on frame name.
38func DetectPopupType(frameName string) PopupType {
39	// _blank is the standard indicator for "open in new tab/window"
40	if frameName == "_blank" {
41		return PopupTypeTab
42	}
43	// Empty or custom frame names indicate JS window.open()
44	return PopupTypePopup
45}
46
47// PendingPopup tracks a popup WebView that has been created but not yet
48// inserted into the workspace. This is used during the three-phase popup
49// lifecycle: create → ready-to-show → (insert into workspace).
50type PendingPopup struct {
51	// PaneID is the popup pane created during the create phase.
52	PaneID entity.PaneID
53
54	// WebView is the related WebView created in the "create" phase.
55	// It shares cookies/session with the parent for OAuth support.
56	WebView port.WebView
57
58	// ParentPaneID is the pane that spawned this popup.
59	ParentPaneID entity.PaneID
60
61	// ParentWebViewID is the WebView ID of the parent (for lookup).
62	ParentWebViewID port.WebViewID
63
64	// TargetURI is the initial URL the popup will load.
65	TargetURI string
66
67	// FrameName is the target frame name from the navigation action.
68	// "_blank" indicates a tab-like popup, other values indicate JS popups.
69	FrameName string
70
71	// IsUserGesture indicates if the popup was triggered by user action
72	// (click) vs script-initiated (e.g., popup after timeout).
73	IsUserGesture bool
74
75	// PopupType categorizes this as a tab-like or JS popup.
76	PopupType PopupType
77
78	// CreatedAt is when the popup was created (for timeout handling).
79	CreatedAt time.Time
80}
81
82// InsertPopupInput contains the data needed to insert a popup into the workspace.
83type InsertPopupInput struct {
84	// ParentPaneID is the pane that spawned this popup.
85	ParentPaneID entity.PaneID
86
87	// PopupPane is the pre-created pane entity for the popup.
88	// It should have IsRelated=true and ParentPaneID set.
89	PopupPane *entity.Pane
90
91	// WebView is the related WebView for the popup.
92	WebView port.WebView
93
94	// Behavior determines how the popup is inserted (split/stacked/tabbed).
95	Behavior entity.PopupBehavior
96
97	// Placement specifies direction for split behavior (right/left/top/bottom).
98	Placement string
99
100	// PopupType indicates if this is a tab-like or JS popup.
101	PopupType PopupType
102
103	// TargetURI is the URL to load in the popup.
104	TargetURI string
105}
106
107// NativePopupInput contains the data needed to host a native-required popup in
108// a dedicated top-level GTK window instead of the workspace.
109// BrowserWindowInput contains an engine-created related WebView to adopt into
110// a complete Dumber browser window. A successful callback transfers ownership.
111type BrowserWindowInput struct {
112	ParentPaneID    entity.PaneID
113	ParentWebViewID port.WebViewID
114	PopupPane       *entity.Pane
115	PopupWebView    port.WebView
116	TargetURI       string
117	Request         port.PopupRequest
118	Ready           bool
119}
120
121type BrowserWindowResult struct {
122	WindowID string
123}
124
125// PopupStagingHost temporarily owns a popup widget while late engine metadata
126// is unavailable. Detach transfers the widget without destroying it.
127type PopupStagingHost interface {
128	Detach() error
129	Destroy()
130}
131
132type StagePopupInput struct {
133	PopupWebView port.WebView
134}
135
136type NativePopupInput struct {
137	ParentPaneID               entity.PaneID
138	ParentWebViewID            port.WebViewID
139	ParentURIAtOpen            string
140	PopupWebView               port.WebView
141	TargetURI                  string
142	Request                    port.PopupRequest
143	ObserveOAuthAutoClose      bool
144	AllowBrowserWindowFallback bool
145	// AlreadyReadyToShow is true when the popup's ready-to-show lifecycle
146	// event fired before the native host was attached (deferred feature
147	// resolution). The host must reveal it immediately instead of waiting.
148	AlreadyReadyToShow bool
149	// OnNativeHostAbort returns true when fallback adopted the WebView. A false
150	// result leaves destruction responsibility with the native host.
151	OnNativeHostAbort func(context.Context, port.WebView) bool
152}
153
154// GetBehavior returns the appropriate behavior based on popup type and config.
155func GetBehavior(popupType PopupType, cfg *entity.BrowsingContextConfig) entity.PopupBehavior {
156	if cfg == nil {
157		return entity.PopupBehaviorSplit // Default
158	}
159
160	switch popupType {
161	case PopupTypeTab:
162		// Tab-like popups (_blank) use blank_target_behavior
163		switch cfg.BlankTargetBehavior {
164		case "split":
165			return entity.PopupBehaviorSplit
166		case "stacked":
167			return entity.PopupBehaviorStacked
168		case "tabbed":
169			return entity.PopupBehaviorTabbed
170		default:
171			return entity.PopupBehaviorStacked // Default for _blank
172		}
173	case PopupTypePopup:
174		// JS popups use the main behavior setting
175		return cfg.Behavior
176	default:
177		return cfg.Behavior
178	}
179}
180
181// SetPopupConfig configures popup handling.
182func (c *Coordinator) SetPopupConfig(
183	factory port.WebViewFactory,
184	popupConfig *entity.BrowsingContextConfig,
185	generateID func() string,
186) {
187	c.ensurePopupManager().setConfig(factory, popupConfig, generateID)
188}
189
190func (c *Coordinator) UpdatePopupConfig(popupConfig entity.BrowsingContextConfig) {
191	c.ensurePopupManager().updatePopupConfig(popupConfig)
192}
193
194func (c *Coordinator) SetPopupWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
195	c.ensurePopupManager().setWindowIDResolver(fn)
196}
197
198func (c *Coordinator) SetPopupSourceHostResolver(fn func(entity.PaneID) dto.SourceHostKind) {
199	c.ensurePopupManager().setSourceHostResolver(fn)
200}
201
202func (c *Coordinator) SetOnOpenBrowserWindow(fn func(context.Context, BrowserWindowInput) (BrowserWindowResult, error)) {
203	c.ensurePopupManager().setOnOpenBrowserWindow(fn)
204}
205
206func (c *Coordinator) SetOnStagePopup(fn func(context.Context, StagePopupInput) (PopupStagingHost, error)) {
207	c.ensurePopupManager().setOnStagePopup(fn)
208}
209
210func (c *Coordinator) ClearPopupNamedContextsForWindow(windowID string) {
211	pm := c.ensurePopupManager()
212	pm.cancelDeferredPopupsForWindow(windowID)
213	pm.clearReusableNamedPopupsForWindow(windowID)
214}
215
216// ShutdownPopups releases unresolved staged popups before the browser engine exits.
217func (c *Coordinator) ShutdownPopups() {
218	c.ensurePopupManager().cancelAllDeferredPopups()
219}
220
221// SetOnInsertPopup sets the callback to insert popups into the workspace.
222func (c *Coordinator) SetOnInsertPopup(fn func(ctx context.Context, input InsertPopupInput) error) {
223	c.ensurePopupManager().setOnInsertPopup(fn)
224}
225
226// SetOnClosePane sets the callback to close a pane when its popup closes.
227func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity.PaneID) error) {
228	c.ensurePopupManager().setOnClosePane(fn)
229}
230
231func (c *Coordinator) SetOnOpenNativePopup(fn func(ctx context.Context, input NativePopupInput) error) {
232	c.ensurePopupManager().setOnOpenNativePopup(fn)
233}
234
235// buildPopupCreateHandler returns the OnCreate callback for a WebView.
236// Returns nil if popup handling is not configured.
237func (c *Coordinator) buildPopupCreateHandler(
238	ctx context.Context, paneID entity.PaneID, wv port.WebView,
239) func(port.PopupRequest) port.WebView {
240	if wv == nil {
241		return nil
242	}
243
244	log := logging.FromContext(ctx)
245	log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
246
247	return func(req port.PopupRequest) port.WebView {
248		return c.handlePopupCreate(ctx, paneID, wv, req)
249	}
250}
251
252// handlePopupCreate handles a popup request from the current WebView.
253// Returns a WebView if popup handling is allowed, nil to block.
254//
255// IMPORTANT: The WebView MUST be added to a GtkWindow hierarchy BEFORE this
256// signal handler returns for engines that require native popup lifecycles.
257// The WebView stays hidden until ready-to-show signal is received.
258func (c *Coordinator) handlePopupCreate(
259	ctx context.Context,
260	parentPaneID entity.PaneID,
261	parentWV port.WebView,
262	req port.PopupRequest,
263) port.WebView {
264	return c.ensurePopupManager().handlePopupCreate(ctx, c.popupHooks(), parentPaneID, parentWV, req)
265}
266
267// handleLinkMiddleClick handles middle-click / Ctrl+click on links.
268// Opens the link in a new pane using blank_target_behavior config.
269func (c *Coordinator) handleLinkMiddleClick(ctx context.Context, parentPaneID entity.PaneID, uri string) bool {
270	return c.ensurePopupManager().handleLinkMiddleClick(ctx, c.popupHooks(), parentPaneID, uri)
271}

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