v0.12.0 d035193

workspace_utils.go RawHistoryFolder

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

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