v0.15.0 9006957

workspace_utils.go RawHistoryFolder

1// workspace_utils.go - Utility functions and helpers for workspace management
2package browser
3
4import (
5	"fmt"
6	"github.com/bnema/dumber/internal/logging"
7	"github.com/bnema/dumber/pkg/webkit"
8	"github.com/diamondburned/gotk4/pkg/glib/v2"
9	"github.com/diamondburned/gotk4/pkg/gtk/v4"
10)
11
12// UpdateTitleBar updates the title bar label for a WebView in stacked panes
13func (wm *WorkspaceManager) UpdateTitleBar(webView *webkit.WebView, title string) {
14	// Delegate to StackedPaneManager which has the correct logic
15	if wm.stackedPaneManager != nil {
16		wm.stackedPaneManager.UpdateTitleBar(webView, title)
17	}
18}
19
20// ensureGUIInPane lazily loads GUI components into a pane when it gains focus
21func (wm *WorkspaceManager) ensureGUIInPane(pane *BrowserPane) {
22	if pane == nil {
23		return
24	}
25
26	// GUI is already injected globally via WebKit's enableUserContentManager
27	if !pane.HasGUI() {
28		pane.SetHasGUI(true)
29		pane.SetGUIComponent("manager", true)
30		pane.SetGUIComponent("omnibox", true)
31	}
32}
33
34// mapDirection maps a direction string to GTK orientation and positioning
35// The bool return value indicates whether the existing pane should be the first child (start)
36// In GTK Paned with Vertical orientation: start=top, end=bottom
37// In GTK Paned with Horizontal orientation: start=left, end=right
38func mapDirection(direction string) (gtk.Orientation, bool) {
39	switch direction {
40	case DirectionLeft:
41		// New pane goes to left, existing to right
42		return gtk.OrientationHorizontal, false
43	case DirectionUp:
44		// New pane goes to top (start), existing to bottom (end)
45		// So existingFirst = false
46		return gtk.OrientationVertical, false
47	case DirectionDown:
48		// New pane goes to bottom (end), existing to top (start)
49		// So existingFirst = true
50		return gtk.OrientationVertical, true
51	default:
52		// Right: new pane to right, existing to left
53		return gtk.OrientationHorizontal, true
54	}
55}
56
57// Widget management utilities
58
59// initializePaneWidgets sets up widgets for a paneNode
60func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, widget gtk.Widgetter) {
61	// Only wrap leaf nodes in overlays (not branch nodes like GtkPaned)
62	if node.isLeaf {
63		// Wrap the widget in an overlay with a border (for active pane indication)
64		overlay, borderWidget := wrapPaneInOverlay(widget)
65
66		// Store the overlay as the container and the border widget
67		node.container = overlay
68		node.borderOverlay = borderWidget
69	} else {
70		// Branch nodes (splits, stacks) don't need border overlays
71		node.container = widget
72		node.borderOverlay = nil
73	}
74
75	// Add base CSS class to the container
76	if node.container != nil {
77		webkit.WidgetAddCSSClass(node.container, basePaneClass)
78	}
79
80	// Initialize other widget fields as nil (will be set when needed)
81	node.titleBar = nil
82	node.stackWrapper = nil
83
84	// Initialize cleanup tracking fields
85	node.widgetValid = true
86	node.cleanupGeneration = 0
87}
88
89// setContainer sets the container widget
90func (wm *WorkspaceManager) setContainer(node *paneNode, widget gtk.Widgetter, typeInfo string) {
91	_ = typeInfo // Type info kept for API compatibility but not used
92
93	// Only wrap leaf nodes in overlays (not branch nodes like stacked containers)
94	if node.isLeaf {
95		// Wrap the widget in an overlay with a border (for active pane indication)
96		overlay, borderWidget := wrapPaneInOverlay(widget)
97
98		// Store the overlay as the container and the border widget
99		node.container = overlay
100		node.borderOverlay = borderWidget
101	} else {
102		// Branch nodes (splits, stacks) don't need border overlays
103		node.container = widget
104		node.borderOverlay = nil
105	}
106
107	// Mark widget validity based on container presence so idle guards run for stacks too
108	if widget != nil {
109		node.widgetValid = true
110	} else {
111		node.widgetValid = false
112	}
113}
114
115// setTitleBar sets the titleBar widget
116func (wm *WorkspaceManager) setTitleBar(node *paneNode, widget gtk.Widgetter) {
117	node.titleBar = widget
118}
119
120// setStackWrapper sets the stackWrapper widget
121func (wm *WorkspaceManager) setStackWrapper(node *paneNode, widget gtk.Widgetter) {
122	node.stackWrapper = widget
123}
124
125// Centralized Active Pane Border Management System
126// Handles all pane types: regular, popup, stacked, split panes
127
128// borderTargets returns the widgets that should reflect the pane border state.
129func (wm *WorkspaceManager) borderTargets(node *paneNode) []gtk.Widgetter {
130	if node == nil {
131		return nil
132	}
133
134	targets := make([]gtk.Widgetter, 0, 4)
135	seen := make(map[*gtk.Widget]struct{})
136	add := func(widget gtk.Widgetter) {
137		if widget == nil {
138			return
139		}
140		if base := gtk.BaseWidget(widget); base != nil {
141			if _, exists := seen[base]; exists {
142				return
143			}
144			seen[base] = struct{}{}
145		}
146		targets = append(targets, widget)
147	}
148
149	// Primary container for this node.
150	add(node.container)
151	add(node.stackWrapper)
152
153	// Highlight stacks via their outer container as well.
154	if node.parent != nil && node.parent.isStacked {
155		add(node.parent.container)
156		add(node.parent.stackWrapper)
157	}
158
159	// Fallback to the WebView root widget to cover any edge cases where the
160	// container reference is temporarily nil.
161	if node.pane != nil && node.pane.webView != nil {
162		add(node.pane.webView.RootWidget())
163	}
164
165	if len(targets) == 0 {
166		return nil
167	}
168	return targets
169}
170
171// applyActivePaneBorder shows the border overlay for the active pane.
172func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
173	if wm == nil || node == nil || node.borderOverlay == nil {
174		return
175	}
176
177	// Only show active border if there are 2+ panes
178	leaves := wm.collectLeaves()
179	if len(leaves) < 2 {
180		return
181	}
182
183	webkit.WidgetSetVisible(node.borderOverlay, true)
184	logging.Debug(fmt.Sprintf("[workspace] Showing border overlay for pane %p", node))
185}
186
187// removeActivePaneBorder hides the border overlay for a pane.
188func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
189	if wm == nil || node == nil || node.borderOverlay == nil {
190		return
191	}
192
193	webkit.WidgetSetVisible(node.borderOverlay, false)
194	logging.Debug(fmt.Sprintf("[workspace] Hiding border overlay for pane %p", node))
195}
196
197// Widget cleanup utilities
198
199// cleanupWidget safely invalidates and cleans up a widget
200func (wm *WorkspaceManager) cleanupWidget(widget gtk.Widgetter) {
201	if widget == nil {
202		return
203	}
204
205	// Widget will be destroyed by GTK when unparented/removed
206	// No manual cleanup needed - GTK4 handles reference counting
207}
208
209// scheduleIdleGuarded queues a callback to run on the GTK main loop and tracks it against
210// the provided pane nodes so it can be cancelled safely if the nodes are destroyed.
211func (wm *WorkspaceManager) scheduleIdleGuarded(fn func() bool, nodes ...*paneNode) uintptr {
212	if wm == nil || fn == nil {
213		return 0
214	}
215	guards := uniquePaneNodes(nodes...)
216	var handle uintptr
217	handle = uintptr(glib.IdleAdd(func() bool {
218		defer wm.releaseIdleHandle(handle)
219		for _, node := range guards {
220			if node == nil {
221				continue
222			}
223			if !node.widgetValid {
224				return false
225			}
226		}
227		return fn()
228	}))
229	if handle != 0 {
230		wm.registerIdleHandle(handle, guards...)
231	}
232	return handle
233}
234
235func uniquePaneNodes(nodes ...*paneNode) []*paneNode {
236	if len(nodes) == 0 {
237		return nil
238	}
239	seen := make(map[*paneNode]struct{}, len(nodes))
240	unique := make([]*paneNode, 0, len(nodes))
241	for _, node := range nodes {
242		if node == nil {
243			continue
244		}
245		if _, ok := seen[node]; ok {
246			continue
247		}
248		seen[node] = struct{}{}
249		unique = append(unique, node)
250	}
251	return unique
252}
253
254func (wm *WorkspaceManager) registerIdleHandle(handle uintptr, nodes ...*paneNode) {
255	if wm == nil || handle == 0 {
256		return
257	}
258	unique := uniquePaneNodes(nodes...)
259	if len(unique) == 0 {
260		return
261	}
262	if wm.pendingIdle == nil {
263		wm.pendingIdle = make(map[uintptr][]*paneNode)
264	}
265	wm.pendingIdle[handle] = unique
266	for _, node := range unique {
267		if node.pendingIdleHandles == nil {
268			node.pendingIdleHandles = make(map[uintptr]struct{})
269		}
270		node.pendingIdleHandles[handle] = struct{}{}
271	}
272}
273
274func (wm *WorkspaceManager) releaseIdleHandle(handle uintptr) {
275	if wm == nil || handle == 0 {
276		return
277	}
278	if wm.pendingIdle == nil {
279		return
280	}
281	nodes, ok := wm.pendingIdle[handle]
282	if !ok {
283		return
284	}
285	delete(wm.pendingIdle, handle)
286	for _, node := range nodes {
287		if node == nil || node.pendingIdleHandles == nil {
288			continue
289		}
290		delete(node.pendingIdleHandles, handle)
291		if len(node.pendingIdleHandles) == 0 {
292			node.pendingIdleHandles = nil
293		}
294	}
295}
296
297func (wm *WorkspaceManager) cancelIdleHandle(handle uintptr) {
298	if handle == 0 {
299		return
300	}
301	wm.releaseIdleHandle(handle)
302	glib.SourceRemove(glib.SourceHandle(handle))
303}
304
305func (wm *WorkspaceManager) cancelIdleHandles(node *paneNode) {
306	if wm == nil || node == nil || len(node.pendingIdleHandles) == 0 {
307		return
308	}
309	handles := make([]uintptr, 0, len(node.pendingIdleHandles))
310	for handle := range node.pendingIdleHandles {
311		handles = append(handles, handle)
312	}
313	for _, handle := range handles {
314		wm.cancelIdleHandle(handle)
315	}
316}
317
318// Note: nextCleanupGeneration, updateMainPane, dumpTreeState, paneCloseLogf
319// are defined in workspace_pane_ops.go and workspace_debug.go
320
321// formatPaneInfo returns a debug string for a pane node
322func formatPaneInfo(node *paneNode) string {
323	if node == nil {
324		return "nil"
325	}
326
327	var info string
328	if node.isLeaf {
329		info = fmt.Sprintf("leaf(container=%p", node.container)
330		if node.pane != nil && node.pane.webView != nil {
331			info += fmt.Sprintf(" webview=%d", node.pane.webView.ID())
332		}
333		if node.windowType == webkit.WindowTypePopup {
334			info += " popup"
335		}
336		info += ")"
337	} else if node.isStacked {
338		info = fmt.Sprintf("stack(panes=%d active=%d container=%p)",
339			len(node.stackedPanes), node.activeStackIndex, node.container)
340	} else {
341		info = fmt.Sprintf("branch(container=%p orientation=%v)",
342			node.container, node.orientation)
343	}
344
345	return info
346}

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