v0.33.2 57f21d6

lifecycle.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"fmt"
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	"github.com/bnema/dumber/internal/ui/component"
11	"github.com/bnema/dumber/internal/ui/input"
12	"github.com/bnema/dumber/internal/ui/layout"
13)
14
15// EnsureWebView acquires or reuses a WebView for the given pane.
16func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) {
17	log := logging.FromContext(ctx)
18
19	if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() {
20		log.Debug().
21			Str("pane_id", string(paneID)).
22			Uint64("webview_id", uint64(wv.ID())).
23			Msg("webview reused for pane")
24		return wv, nil
25	}
26
27	if c.pool == nil {
28		return nil, fmt.Errorf("webview pool not configured")
29	}
30
31	wv, err := c.pool.Acquire(ctx)
32	if err != nil {
33		return nil, err
34	}
35
36	// setWebViewLocked atomically resets presentation state for the acquired
37	// WebView, including a pooled instance previously revealed in another pane.
38	c.setWebViewLocked(paneID, wv)
39	c.setupWebViewCallbacks(ctx, paneID, wv)
40
41	log.Debug().
42		Str("pane_id", string(paneID)).
43		Uint64("webview_id", uint64(wv.ID())).
44		Msg("webview acquired for pane")
45	return wv, nil
46}
47
48// ReleaseWebView returns the WebView for a pane to the pool.
49func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID) {
50	log := logging.FromContext(ctx)
51	c.ensurePopupManager().cancelDeferredPopupsForParent(paneID)
52	c.clearExternalPrompt(paneID)
53
54	// deleteWebViewLocked clears reveal state before returning, even when a
55	// previous cleanup already removed this pane's mapping.
56	wv := c.deleteWebViewLocked(paneID)
57	if wv == nil {
58		return
59	}
60	c.ensurePopupManager().clearReusableNamedPopupByPaneID(paneID)
61	c.ensurePopupManager().clearReusableNamedPopupByWebViewID(wv.ID())
62	c.clearPendingAppearance(paneID)
63
64	// CRITICAL: If this webview was inhibiting idle (fullscreen or audio playing),
65	// we must release the inhibition before destroying the webview.
66	// Otherwise the D-Bus inhibit request stays active forever.
67	if c.idleInhibitor != nil {
68		if wv.IsFullscreen() {
69			log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was fullscreen)")
70			if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
71				log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (fullscreen)")
72			}
73		}
74		if wv.IsPlayingAudio() {
75			log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was playing audio)")
76			if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
77				log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (audio)")
78			}
79		}
80	}
81
82	// Clean up title tracking
83	c.titleMu.Lock()
84	delete(c.paneTitles, paneID)
85	c.titleMu.Unlock()
86
87	// Clean up navigation origin tracking
88	c.navOriginMu.Lock()
89	delete(c.navOrigins, paneID)
90	c.navOriginMu.Unlock()
91
92	if c.pool != nil {
93		c.pool.Release(wv)
94	} else {
95		wv.Destroy()
96	}
97
98	log.Debug().Str("pane_id", string(paneID)).Msg("webview released")
99}
100
101// AttachToWorkspace ensures each pane in the workspace has a WebView widget attached.
102func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) {
103	log := logging.FromContext(ctx)
104
105	if ws == nil || wsView == nil || c.widgetFactory == nil {
106		return
107	}
108
109	for _, pane := range ws.AllPanes() {
110		if pane == nil {
111			continue
112		}
113
114		wv, err := c.EnsureWebView(ctx, pane.ID)
115		if err != nil {
116			log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to ensure webview for pane")
117			continue
118		}
119
120		// Load the pane's URI if set and different from current
121		if pane.URI != "" && pane.URI != wv.URI() {
122			if err := wv.LoadURI(ctx, pane.URI); err != nil {
123				log.Warn().Err(err).Str("pane_id", string(pane.ID)).Str("uri", pane.URI).Msg("failed to load pane URI")
124			}
125		}
126
127		widget := c.WrapWidget(ctx, wv)
128		if widget == nil {
129			continue
130		}
131
132		log.Debug().
133			Str("pane_id", string(pane.ID)).
134			Uint64("webview_id", uint64(wv.ID())).
135			Int("wrapped_widget_alloc_width", widget.GetAllocatedWidth()).
136			Int("wrapped_widget_alloc_height", widget.GetAllocatedHeight()).
137			Msg("wrapped webview widget prepared for pane")
138
139		if err := wsView.AttachWebViewWidget(pane.ID, widget, c.webViewRevealed(pane.ID, wv)); err != nil {
140			log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget")
141			continue
142		}
143	}
144}
145
146// WrapWidget converts a WebView to a layout.Widget for embedding.
147// It also attaches gesture handlers for mouse button navigation.
148func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget {
149	log := logging.FromContext(ctx)
150
151	if wv == nil || c.widgetFactory == nil {
152		log.Debug().Msg("cannot wrap nil webview or factory")
153		return nil
154	}
155
156	// Use NativeWidgetProvider interface for GTK embedding (engine-agnostic)
157	nwp, ok := wv.(port.NativeWidgetProvider)
158	if !ok {
159		log.Debug().Msg("webview does not support widget embedding")
160		return nil
161	}
162
163	ptr := nwp.NativeWidget()
164	if ptr == 0 {
165		return nil
166	}
167
168	widget := c.widgetFactory.WrapNativeWidget(ptr)
169
170	// Attach gesture handler for mouse button 8/9 navigation
171	if widget != nil {
172		gestureHandler := input.NewGestureHandler(ctx)
173		// Pass WebView directly to preserve user gesture context (like Epiphany)
174		if nav, ok := wv.(input.DirectNavigator); ok {
175			gestureHandler.SetNavigator(nav)
176		}
177		// Keep callback as fallback
178		if c.gestureActionHandler != nil {
179			gestureHandler.SetOnAction(c.gestureActionHandler)
180		}
181		gestureHandler.AttachTo(widget.GtkWidget())
182		log.Debug().Msg("gesture handler attached to webview with direct navigator")
183	}
184
185	return widget
186}
187
188// ActiveWebView returns the WebView for the active pane.
189func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView {
190	log := logging.FromContext(ctx)
191
192	if paneID, ok := c.activePaneOverrideID(); ok {
193		wv := c.getWebViewLocked(paneID)
194		if wv != nil {
195			return wv
196		}
197		log.Debug().Str("pane_id", string(paneID)).Msg("active pane override has no webview, falling back")
198	}
199	if c.getActiveWS == nil {
200		log.Debug().Msg("no active workspace resolver")
201		return nil
202	}
203
204	ws, _ := c.getActiveWS()
205	if ws == nil {
206		log.Debug().Msg("no active workspace")
207		return nil
208	}
209
210	pane := ws.ActivePane()
211	if pane == nil || pane.Pane == nil {
212		log.Debug().Msg("no active pane")
213		return nil
214	}
215
216	return c.getWebViewLocked(pane.Pane.ID)
217}
218
219// GetWebView returns the WebView for a specific pane.
220func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView {
221	return c.getWebViewLocked(paneID)
222}
223
224// RegisterPopupWebView registers a popup WebView that was created externally.
225// This is used when popup tabs are created and the WebView needs to be tracked.
226func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) {
227	if wv != nil && paneID != "" {
228		c.setWebViewLocked(paneID, wv)
229	}
230}
231
232// SyncWebViewViewport requests an explicit viewport resync when the engine
233// supports it. This is useful after widget attachment, reparenting, visibility
234// changes, or layout promotion paths that may not emit a size change.
235func (c *Coordinator) SyncWebViewViewport(ctx context.Context, paneID entity.PaneID, reason string) {
236	if c == nil || paneID == "" {
237		return
238	}
239	wv := c.getWebViewLocked(paneID)
240	if wv == nil {
241		return
242	}
243	syncer, ok := wv.(port.ViewportSyncCapable)
244	if !ok {
245		return
246	}
247	syncer.SyncViewport(ctx, reason)
248}

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