v0.14.1 0da30ba

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

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