v0.30.0 eb3068f

popup.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"time"
6
7	"github.com/bnema/dumber/internal/application/port"
8	"github.com/bnema/dumber/internal/domain/entity"
9	"github.com/bnema/dumber/internal/logging"
10)
11
12// PopupType indicates whether the popup was triggered by a link or JavaScript.
13type PopupType int
14
15const (
16	// PopupTypeTab represents a popup from target="_blank" links.
17	// These are typically user-initiated and open like new tabs.
18	PopupTypeTab PopupType = iota
19	// PopupTypePopup represents a popup from window.open() JavaScript calls.
20	// These may have different placement behavior than tab-like popups.
21	PopupTypePopup
22)
23
24// String returns a human-readable name for the popup type.
25func (t PopupType) String() string {
26	switch t {
27	case PopupTypeTab:
28		return "tab"
29	case PopupTypePopup:
30		return "popup"
31	default:
32		return "unknown"
33	}
34}
35
36// DetectPopupType determines if this is a tab-like or JS popup based on frame name.
37func DetectPopupType(frameName string) PopupType {
38	// _blank is the standard indicator for "open in new tab/window"
39	if frameName == "_blank" {
40		return PopupTypeTab
41	}
42	// Empty or custom frame names indicate JS window.open()
43	return PopupTypePopup
44}
45
46// PendingPopup tracks a popup WebView that has been created but not yet
47// inserted into the workspace. This is used during the three-phase popup
48// lifecycle: create → ready-to-show → (insert into workspace).
49type PendingPopup struct {
50	// PaneID is the popup pane created during the create phase.
51	PaneID entity.PaneID
52
53	// WebView is the related WebView created in the "create" phase.
54	// It shares cookies/session with the parent for OAuth support.
55	WebView port.WebView
56
57	// ParentPaneID is the pane that spawned this popup.
58	ParentPaneID entity.PaneID
59
60	// ParentWebViewID is the WebView ID of the parent (for lookup).
61	ParentWebViewID port.WebViewID
62
63	// TargetURI is the initial URL the popup will load.
64	TargetURI string
65
66	// FrameName is the target frame name from the navigation action.
67	// "_blank" indicates a tab-like popup, other values indicate JS popups.
68	FrameName string
69
70	// IsUserGesture indicates if the popup was triggered by user action
71	// (click) vs script-initiated (e.g., popup after timeout).
72	IsUserGesture bool
73
74	// PopupType categorizes this as a tab-like or JS popup.
75	PopupType PopupType
76
77	// CreatedAt is when the popup was created (for timeout handling).
78	CreatedAt time.Time
79}
80
81// InsertPopupInput contains the data needed to insert a popup into the workspace.
82type InsertPopupInput struct {
83	// ParentPaneID is the pane that spawned this popup.
84	ParentPaneID entity.PaneID
85
86	// PopupPane is the pre-created pane entity for the popup.
87	// It should have IsRelated=true and ParentPaneID set.
88	PopupPane *entity.Pane
89
90	// WebView is the related WebView for the popup.
91	WebView port.WebView
92
93	// Behavior determines how the popup is inserted (split/stacked/tabbed).
94	Behavior entity.PopupBehavior
95
96	// Placement specifies direction for split behavior (right/left/top/bottom).
97	Placement string
98
99	// PopupType indicates if this is a tab-like or JS popup.
100	PopupType PopupType
101
102	// TargetURI is the URL to load in the popup.
103	TargetURI string
104}
105
106// NativePopupInput contains the data needed to host a native-required popup in
107// a dedicated top-level GTK window instead of the workspace.
108type NativePopupInput struct {
109	ParentPaneID          entity.PaneID
110	ParentWebViewID       port.WebViewID
111	ParentURIAtOpen       string
112	PopupWebView          port.WebView
113	TargetURI             string
114	Request               port.PopupRequest
115	ObserveOAuthAutoClose bool
116}
117
118// GetBehavior returns the appropriate behavior based on popup type and config.
119func GetBehavior(popupType PopupType, cfg *entity.BrowsingContextConfig) entity.PopupBehavior {
120	if cfg == nil {
121		return entity.PopupBehaviorSplit // Default
122	}
123
124	switch popupType {
125	case PopupTypeTab:
126		// Tab-like popups (_blank) use blank_target_behavior
127		switch cfg.BlankTargetBehavior {
128		case "split":
129			return entity.PopupBehaviorSplit
130		case "stacked":
131			return entity.PopupBehaviorStacked
132		case "tabbed":
133			return entity.PopupBehaviorTabbed
134		default:
135			return entity.PopupBehaviorStacked // Default for _blank
136		}
137	case PopupTypePopup:
138		// JS popups use the main behavior setting
139		return cfg.Behavior
140	default:
141		return cfg.Behavior
142	}
143}
144
145// SetPopupConfig configures popup handling.
146func (c *Coordinator) SetPopupConfig(
147	factory port.WebViewFactory,
148	popupConfig *entity.BrowsingContextConfig,
149	generateID func() string,
150) {
151	c.ensurePopupManager().setConfig(factory, popupConfig, generateID)
152}
153
154func (c *Coordinator) UpdatePopupConfig(popupConfig entity.BrowsingContextConfig) {
155	c.ensurePopupManager().updatePopupConfig(popupConfig)
156}
157
158func (c *Coordinator) SetPopupWindowIDResolver(fn func(entity.PaneID) (string, bool)) {
159	c.ensurePopupManager().setWindowIDResolver(fn)
160}
161
162func (c *Coordinator) ClearPopupNamedContextsForWindow(windowID string) {
163	c.ensurePopupManager().clearReusableNamedPopupsForWindow(windowID)
164}
165
166// SetOnInsertPopup sets the callback to insert popups into the workspace.
167func (c *Coordinator) SetOnInsertPopup(fn func(ctx context.Context, input InsertPopupInput) error) {
168	c.ensurePopupManager().setOnInsertPopup(fn)
169}
170
171// SetOnClosePane sets the callback to close a pane when its popup closes.
172func (c *Coordinator) SetOnClosePane(fn func(ctx context.Context, paneID entity.PaneID) error) {
173	c.ensurePopupManager().setOnClosePane(fn)
174}
175
176func (c *Coordinator) SetOnOpenNativePopup(fn func(ctx context.Context, input NativePopupInput) error) {
177	c.ensurePopupManager().setOnOpenNativePopup(fn)
178}
179
180// buildPopupCreateHandler returns the OnCreate callback for a WebView.
181// Returns nil if popup handling is not configured.
182func (c *Coordinator) buildPopupCreateHandler(
183	ctx context.Context, paneID entity.PaneID, wv port.WebView,
184) func(port.PopupRequest) port.WebView {
185	if wv == nil {
186		return nil
187	}
188
189	log := logging.FromContext(ctx)
190	log.Debug().Str("pane_id", string(paneID)).Msg("popup handling configured for webview")
191
192	return func(req port.PopupRequest) port.WebView {
193		return c.handlePopupCreate(ctx, paneID, wv, req)
194	}
195}
196
197// handlePopupCreate handles a popup request from the current WebView.
198// Returns a WebView if popup handling is allowed, nil to block.
199//
200// IMPORTANT: The WebView MUST be added to a GtkWindow hierarchy BEFORE this
201// signal handler returns for engines that require native popup lifecycles.
202// The WebView stays hidden until ready-to-show signal is received.
203func (c *Coordinator) handlePopupCreate(
204	ctx context.Context,
205	parentPaneID entity.PaneID,
206	parentWV port.WebView,
207	req port.PopupRequest,
208) port.WebView {
209	return c.ensurePopupManager().handlePopupCreate(ctx, c.popupHooks(), parentPaneID, parentWV, req)
210}
211
212// handleLinkMiddleClick handles middle-click / Ctrl+click on links.
213// Opens the link in a new pane using blank_target_behavior config.
214func (c *Coordinator) handleLinkMiddleClick(ctx context.Context, parentPaneID entity.PaneID, uri string) bool {
215	return c.ensurePopupManager().handleLinkMiddleClick(ctx, c.popupHooks(), parentPaneID, uri)
216}

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