v0.28.0 3cb3770

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	"github.com/bnema/puregotk/v4/gtk"
14)
15
16// EnsureWebView acquires or reuses a WebView for the given pane.
17func (c *Coordinator) EnsureWebView(ctx context.Context, paneID entity.PaneID) (port.WebView, error) {
18	log := logging.FromContext(ctx)
19
20	if wv := c.getWebViewLocked(paneID); wv != nil && !wv.IsDestroyed() {
21		return wv, nil
22	}
23
24	if c.pool == nil {
25		return nil, fmt.Errorf("webview pool not configured")
26	}
27
28	// Mark tab_created on first webview (first tab)
29	if c.webViewCount() == 0 {
30		logging.Trace().Mark("tab_created")
31	}
32
33	wv, err := c.pool.Acquire(ctx)
34	if err != nil {
35		return nil, err
36	}
37	logging.Trace().Mark("webview_acquired")
38
39	c.setWebViewLocked(paneID, wv)
40	c.setupWebViewCallbacks(ctx, paneID, wv)
41
42	log.Debug().Str("pane_id", string(paneID)).Msg("webview acquired for pane")
43	return wv, nil
44}
45
46// ReleaseWebView returns the WebView for a pane to the pool.
47func (c *Coordinator) ReleaseWebView(ctx context.Context, paneID entity.PaneID) {
48	log := logging.FromContext(ctx)
49
50	wv := c.deleteWebViewLocked(paneID)
51	if wv == nil {
52		return
53	}
54	c.clearPendingAppearance(paneID)
55
56	// CRITICAL: If this webview was inhibiting idle (fullscreen or audio playing),
57	// we must release the inhibition before destroying the webview.
58	// Otherwise the D-Bus inhibit request stays active forever.
59	if c.idleInhibitor != nil {
60		if wv.IsFullscreen() {
61			log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was fullscreen)")
62			if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
63				log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (fullscreen)")
64			}
65		}
66		if wv.IsPlayingAudio() {
67			log.Debug().Str("pane_id", string(paneID)).Msg("releasing idle inhibition (was playing audio)")
68			if err := c.idleInhibitor.Uninhibit(ctx); err != nil {
69				log.Warn().Err(err).Str("pane_id", string(paneID)).Msg("failed to uninhibit idle on release (audio)")
70			}
71		}
72	}
73
74	// Clean up title tracking
75	c.titleMu.Lock()
76	delete(c.paneTitles, paneID)
77	c.titleMu.Unlock()
78
79	// Clean up navigation origin tracking
80	c.navOriginMu.Lock()
81	delete(c.navOrigins, paneID)
82	c.navOriginMu.Unlock()
83
84	if c.pool != nil {
85		c.pool.Release(wv)
86	} else {
87		wv.Destroy()
88	}
89
90	log.Debug().Str("pane_id", string(paneID)).Msg("webview released")
91}
92
93// AttachToWorkspace ensures each pane in the workspace has a WebView widget attached.
94func (c *Coordinator) AttachToWorkspace(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) {
95	log := logging.FromContext(ctx)
96
97	if ws == nil || wsView == nil || c.widgetFactory == nil {
98		return
99	}
100
101	for _, pane := range ws.AllPanes() {
102		if pane == nil {
103			continue
104		}
105
106		wv, err := c.EnsureWebView(ctx, pane.ID)
107		if err != nil {
108			log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to ensure webview for pane")
109			continue
110		}
111
112		// Load the pane's URI if set and different from current
113		if pane.URI != "" && pane.URI != wv.URI() {
114			if err := wv.LoadURI(ctx, pane.URI); err != nil {
115				log.Warn().Err(err).Str("pane_id", string(pane.ID)).Str("uri", pane.URI).Msg("failed to load pane URI")
116			}
117		}
118
119		widget := c.WrapWidget(ctx, wv)
120		if widget == nil {
121			continue
122		}
123
124		if err := wsView.SetWebViewWidget(pane.ID, widget); err != nil {
125			log.Warn().Err(err).Str("pane_id", string(pane.ID)).Msg("failed to attach webview widget")
126		}
127		logging.Trace().Mark("webview_attached")
128	}
129}
130
131// WrapWidget converts a WebView to a layout.Widget for embedding.
132// It also attaches gesture handlers for mouse button navigation.
133func (c *Coordinator) WrapWidget(ctx context.Context, wv port.WebView) layout.Widget {
134	log := logging.FromContext(ctx)
135
136	if wv == nil || c.widgetFactory == nil {
137		log.Debug().Msg("cannot wrap nil webview or factory")
138		return nil
139	}
140
141	// Use NativeWidgetProvider interface for GTK embedding (engine-agnostic)
142	nwp, ok := wv.(port.NativeWidgetProvider)
143	if !ok {
144		log.Debug().Msg("webview does not support widget embedding")
145		return nil
146	}
147
148	ptr := nwp.NativeWidget()
149	if ptr == 0 {
150		return nil
151	}
152
153	gtkWidget := &gtk.Widget{}
154	gtkWidget.Ptr = ptr
155	widget := c.widgetFactory.WrapWidget(gtkWidget)
156
157	// Attach gesture handler for mouse button 8/9 navigation
158	if widget != nil {
159		gestureHandler := input.NewGestureHandler(ctx)
160		// Pass WebView directly to preserve user gesture context (like Epiphany)
161		if nav, ok := wv.(input.DirectNavigator); ok {
162			gestureHandler.SetNavigator(nav)
163		}
164		// Keep callback as fallback
165		if c.gestureActionHandler != nil {
166			gestureHandler.SetOnAction(c.gestureActionHandler)
167		}
168		gestureHandler.AttachTo(widget.GtkWidget())
169		log.Debug().Msg("gesture handler attached to webview with direct navigator")
170	}
171
172	return widget
173}
174
175// ActiveWebView returns the WebView for the active pane.
176func (c *Coordinator) ActiveWebView(ctx context.Context) port.WebView {
177	log := logging.FromContext(ctx)
178
179	if paneID, ok := c.activePaneOverrideID(); ok {
180		wv := c.getWebViewLocked(paneID)
181		if wv != nil {
182			return wv
183		}
184		log.Debug().Str("pane_id", string(paneID)).Msg("active pane override has no webview, falling back")
185	}
186	if c.getActiveWS == nil {
187		log.Debug().Msg("no active workspace resolver")
188		return nil
189	}
190
191	ws, _ := c.getActiveWS()
192	if ws == nil {
193		log.Debug().Msg("no active workspace")
194		return nil
195	}
196
197	pane := ws.ActivePane()
198	if pane == nil || pane.Pane == nil {
199		log.Debug().Msg("no active pane")
200		return nil
201	}
202
203	return c.getWebViewLocked(pane.Pane.ID)
204}
205
206// GetWebView returns the WebView for a specific pane.
207func (c *Coordinator) GetWebView(paneID entity.PaneID) port.WebView {
208	return c.getWebViewLocked(paneID)
209}
210
211// RegisterPopupWebView registers a popup WebView that was created externally.
212// This is used when popup tabs are created and the WebView needs to be tracked.
213func (c *Coordinator) RegisterPopupWebView(paneID entity.PaneID, wv port.WebView) {
214	if wv != nil && paneID != "" {
215		c.setWebViewLocked(paneID, wv)
216	}
217}

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