v0.32.0 0617278

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

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