v0.11.0 dc50343

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

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