detached 4fb244f

feat(browser): implement SafeWidget for thread-safe GTK widget management

- Add SafeWidget wrapper providing thread-safe GTK widget operations
- Add WidgetRegistry for centralized widget lifecycle management
- Update stacked_panes.go to use SafeWidget pattern throughout
- Ensure proper widget validation and memory safety
- Maintain compatibility with existing webkit CGO functions
commit
4fb244f931c443f431096b0a3a98ec7fe2cca0f0
author
Brice · · 2025-09-27 16:49 UTC
parent
9964d39
signature
unsigned
3 files · +283 -77
M internal/app/browser/stacked_panes.go +140 -67
@@ -82,14 +82,15 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) {
8282
8383 // Create the new leaf node
8484 newLeaf := &paneNode{
85- pane: newPane,
86- container: newContainer,
87- isLeaf: true,
85+ pane: newPane,
86+ isLeaf: true,
8887 }
88+ // Initialize widgets properly using workspace manager helper
89+ spm.wm.initializePaneWidgets(newLeaf, newContainer)
8990
9091 // Create title bar for the new pane
9192 newTitleBar := spm.createTitleBar(newLeaf)
92- newLeaf.titleBar = newTitleBar
93+ spm.wm.setTitleBar(newLeaf, newTitleBar)
9394
9495 // Keep new container hidden initially - will be shown after transition
9596 webkit.WidgetSetVisible(newContainer, false)
@@ -129,8 +130,13 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
129130 stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane
130131
131132 // Unparent the new container before adding it to the stack (only if it has a parent)
132- if webkit.WidgetGetParent(newLeaf.container) != 0 {
133- webkit.WidgetUnparent(newLeaf.container)
133+ if newLeaf.container != nil {
134+ newLeaf.container.Execute(func(containerPtr uintptr) error {
135+ if webkit.WidgetGetParent(containerPtr) != 0 {
136+ webkit.WidgetUnparent(containerPtr)
137+ }
138+ return nil
139+ })
134140 }
135141
136142 // Insert widgets at correct position for Zellij-style layout
@@ -141,19 +147,29 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
141147 if insertIndex > 0 {
142148 // Insert after the previous pane's container
143149 prevPane := stackNode.stackedPanes[insertIndex-1]
144- insertAfterWidget = prevPane.container
150+ if prevPane.container != nil {
151+ insertAfterWidget = prevPane.container.Ptr()
152+ }
145153 }
146154
147155 if insertAfterWidget != 0 {
148156 // Insert titleBar after the previous pane's container
149- webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.titleBar, insertAfterWidget)
157+ if stackNode.stackWrapper != nil && newLeaf.titleBar != nil {
158+ webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr(), insertAfterWidget)
159+ }
150160 // Insert container after the newly inserted titleBar
151- webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.container, newLeaf.titleBar)
161+ if stackNode.stackWrapper != nil && newLeaf.container != nil && newLeaf.titleBar != nil {
162+ webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr(), newLeaf.titleBar.Ptr())
163+ }
152164 log.Printf("[workspace] inserted widgets at position %d (after widget %#x)", insertIndex, insertAfterWidget)
153165 } else {
154166 // Insert at the beginning (insertIndex = 0)
155- webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.container)
156- webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.titleBar)
167+ if stackNode.stackWrapper != nil && newLeaf.container != nil {
168+ webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr())
169+ }
170+ if stackNode.stackWrapper != nil && newLeaf.titleBar != nil {
171+ webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr())
172+ }
157173 log.Printf("[workspace] prepended widgets at position 0")
158174 }
159175
@@ -199,7 +215,9 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
199215
200216 // Build the complete stack structure with hidden widgets
201217 webkit.BoxAppend(stackInternalBox, titleBar)
202- webkit.BoxAppend(stackInternalBox, existingContainer)
218+ if existingContainer != nil {
219+ webkit.BoxAppend(stackInternalBox, existingContainer.Ptr())
220+ }
203221
204222 // Immediately reattach the stack wrapper to minimize visibility gap
205223 spm.reattachToParent(stackWrapperContainer, target, parent)
@@ -207,19 +225,19 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
207225 // Convert target to a stacked leaf node
208226 target.isStacked = true
209227 target.isLeaf = true
210- target.container = existingContainer
211- target.titleBar = titleBar
228+ // container stays the same (existingContainer)
229+ spm.wm.setTitleBar(target, titleBar)
212230
213231 // Create the stack container node - container points to wrapper, stackWrapper points to internal box
214232 stackNode := &paneNode{
215233 isStacked: true,
216234 isLeaf: false,
217- container: stackWrapperContainer, // Wrapper for GTK operations (splits, etc.)
218- stackWrapper: stackInternalBox, // Internal box for stack operations
219235 stackedPanes: []*paneNode{target},
220236 activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0)
221237 parent: parent,
222238 }
239+ spm.wm.setContainer(stackNode, stackWrapperContainer, "stack-wrapper") // Wrapper for GTK operations (splits, etc.)
240+ spm.wm.setStackWrapper(stackNode, stackInternalBox) // Internal box for stack operations
223241
224242 // Update target's parent to be the stack node
225243 target.parent = stackNode
@@ -243,13 +261,22 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
243261 stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf)
244262
245263 // Unparent the new container before adding it to the stack
246- if webkit.WidgetGetParent(newLeaf.container) != 0 {
247- webkit.WidgetUnparent(newLeaf.container)
264+ if newLeaf.container != nil {
265+ newLeaf.container.Execute(func(containerPtr uintptr) error {
266+ if webkit.WidgetGetParent(containerPtr) != 0 {
267+ webkit.WidgetUnparent(containerPtr)
268+ }
269+ return nil
270+ })
248271 }
249272
250273 // Add the new widgets to the internal stack box
251- webkit.BoxAppend(stackNode.stackWrapper, newLeaf.titleBar)
252- webkit.BoxAppend(stackNode.stackWrapper, newLeaf.container)
274+ if stackNode.stackWrapper != nil && newLeaf.titleBar != nil {
275+ webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr())
276+ }
277+ if stackNode.stackWrapper != nil && newLeaf.container != nil {
278+ webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr())
279+ }
253280
254281 return stackNode, insertIndex, nil
255282 }
@@ -262,16 +289,24 @@ func (spm *StackedPaneManager) detachFromParent(target *paneNode, parent *paneNo
262289 spm.wm.window.SetChild(0)
263290 }
264291 // Unparent if it has a GTK parent
265- if webkit.WidgetGetParent(target.container) != 0 {
266- webkit.WidgetUnparent(target.container)
292+ if target.container != nil {
293+ target.container.Execute(func(containerPtr uintptr) error {
294+ if webkit.WidgetGetParent(containerPtr) != 0 {
295+ webkit.WidgetUnparent(containerPtr)
296+ }
297+ return nil
298+ })
267299 }
268- } else if parent.container != 0 {
300+ } else if parent.container != nil {
269301 // Target has a parent paned - remove it (automatically unparents in GTK4)
270- if parent.left == target {
271- webkit.PanedSetStartChild(parent.container, 0)
272- } else if parent.right == target {
273- webkit.PanedSetEndChild(parent.container, 0)
274- }
302+ parent.container.Execute(func(panedPtr uintptr) error {
303+ if parent.left == target {
304+ webkit.PanedSetStartChild(panedPtr, 0)
305+ } else if parent.right == target {
306+ webkit.PanedSetEndChild(panedPtr, 0)
307+ }
308+ return nil
309+ })
275310 }
276311 }
277312
@@ -281,12 +316,15 @@ func (spm *StackedPaneManager) reattachToParent(container uintptr, target *paneN
281316 if spm.wm.window != nil {
282317 spm.wm.window.SetChild(container)
283318 }
284- } else if parent.container != 0 {
285- if parent.left == target {
286- webkit.PanedSetStartChild(parent.container, container)
287- } else if parent.right == target {
288- webkit.PanedSetEndChild(parent.container, container)
289- }
319+ } else if parent.container != nil {
320+ parent.container.Execute(func(panedPtr uintptr) error {
321+ if parent.left == target {
322+ webkit.PanedSetStartChild(panedPtr, container)
323+ } else if parent.right == target {
324+ webkit.PanedSetEndChild(panedPtr, container)
325+ }
326+ return nil
327+ })
290328 }
291329 }
292330
@@ -310,7 +348,7 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod
310348 currentActiveIndex := stackNode.activeStackIndex
311349 if currentActiveIndex >= 0 && currentActiveIndex < len(stackNode.stackedPanes) && currentActiveIndex != insertIndex {
312350 currentlyActivePane := stackNode.stackedPanes[currentActiveIndex]
313- if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != 0 {
351+ if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != nil {
314352 currentTitle := currentlyActivePane.pane.webView.GetTitle()
315353 if currentTitle != "" {
316354 spm.updateTitleBarLabel(currentlyActivePane, currentTitle)
@@ -364,17 +402,27 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
364402 for i, pane := range stackNode.stackedPanes {
365403 if i == activeIndex {
366404 // Active pane: show container, NEVER show title bar
367- webkit.WidgetSetVisible(pane.container, true)
368- webkit.WidgetSetVisible(pane.titleBar, false) // ABSOLUTE RULE: never visible for active pane
369- webkit.WidgetAddCSSClass(pane.container, "stacked-pane-active")
370- webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-collapsed")
405+ if pane.container != nil {
406+ webkit.WidgetSetVisible(pane.container.Ptr(), true)
407+ webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-active")
408+ webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-collapsed")
409+ }
410+ if pane.titleBar != nil {
411+ webkit.WidgetSetVisible(pane.titleBar.Ptr(), false) // ABSOLUTE RULE: never visible for active pane
412+ }
371413 log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i)
372414 } else {
373415 // Inactive panes: hide container, show title bar
374- webkit.WidgetSetVisible(pane.container, false)
375- webkit.WidgetSetVisible(pane.titleBar, true)
376- webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed")
377- webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-active")
416+ if pane.container != nil {
417+ webkit.WidgetSetVisible(pane.container.Ptr(), false)
418+ webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-collapsed")
419+ }
420+ if pane.titleBar != nil {
421+ webkit.WidgetSetVisible(pane.titleBar.Ptr(), true)
422+ }
423+ if pane.container != nil {
424+ webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-active")
425+ }
378426 }
379427 }
380428 }
@@ -436,7 +484,7 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
436484 // Update title bar for the pane transitioning from ACTIVE to INACTIVE
437485 // This ensures the collapsed pane shows its current page title (Zellij-style layout)
438486 currentActivePane := stackNode.stackedPanes[currentIndex]
439- if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 {
487+ if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != nil {
440488 currentTitle := currentActivePane.pane.webView.GetTitle()
441489 if currentTitle != "" {
442490 spm.updateTitleBarLabel(currentActivePane, currentTitle)
@@ -512,7 +560,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str
512560 }
513561
514562 // Check if this pane is in a stack and has a title bar
515- if node.parent != nil && node.parent.isStacked && node.titleBar != 0 {
563+ if node.parent != nil && node.parent.isStacked && node.titleBar != nil {
516564 // CRITICAL: Only update title bar for INACTIVE panes
517565 // Active panes should never show their title bar
518566 stackNode := node.parent
@@ -540,7 +588,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str
540588
541589 // updateTitleBarLabel updates the label widget within a title bar by recreating it
542590 func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) {
543- if node == nil || node.titleBar == 0 || node.parent == nil || !node.parent.isStacked {
591+ if node == nil || node.titleBar == nil || node.parent == nil || !node.parent.isStacked {
544592 return
545593 }
546594
@@ -558,7 +606,7 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
558606 }
559607
560608 // Replace the old title bar in the stack
561- if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 {
609+ if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != nil {
562610 // Find the correct insertion position based on the pane's index in the stack
563611 paneIndex := -1
564612 for i, stackedPane := range node.parent.stackedPanes {
@@ -569,19 +617,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
569617 }
570618
571619 // Remove old title bar
572- webkit.WidgetUnparent(node.titleBar)
620+ if node.titleBar != nil {
621+ node.titleBar.Execute(func(titleBarPtr uintptr) error {
622+ webkit.WidgetUnparent(titleBarPtr)
623+ return nil
624+ })
625+ }
573626
574627 // Update the node's title bar reference
575- node.titleBar = newTitleBar
628+ spm.wm.setTitleBar(node, newTitleBar)
576629
577630 // Insert the new title bar at the correct position
578631 if paneIndex == 0 {
579632 // First pane - insert at the beginning
580- webkit.BoxPrepend(node.parent.stackWrapper, newTitleBar)
633+ if node.parent.stackWrapper != nil {
634+ webkit.BoxPrepend(node.parent.stackWrapper.Ptr(), newTitleBar)
635+ }
581636 } else {
582637 // Insert after the previous pane's container
583638 prevPane := node.parent.stackedPanes[paneIndex-1]
584- webkit.BoxInsertChildAfter(node.parent.stackWrapper, newTitleBar, prevPane.container)
639+ if node.parent.stackWrapper != nil && prevPane.container != nil {
640+ webkit.BoxInsertChildAfter(node.parent.stackWrapper.Ptr(), newTitleBar, prevPane.container.Ptr())
641+ }
585642 }
586643
587644 log.Printf("[workspace] replaced title bar for pane %d: %p", paneIndex, node)
@@ -644,16 +701,18 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
644701 node.pane.CleanupFromWorkspace(spm.wm)
645702
646703 // Remove the pane's widgets from the stack container
647- stackBox := stackNode.stackWrapper
648- if stackBox == 0 {
704+ var stackBox *SafeWidget = stackNode.stackWrapper
705+ if stackBox == nil {
649706 stackBox = stackNode.container
650707 }
651708
652- if node.titleBar != 0 {
653- webkit.BoxRemove(stackBox, node.titleBar)
654- }
655- if node.container != 0 {
656- webkit.BoxRemove(stackBox, node.container)
709+ if stackBox != nil {
710+ if node.titleBar != nil {
711+ webkit.BoxRemove(stackBox.Ptr(), node.titleBar.Ptr())
712+ }
713+ if node.container != nil {
714+ webkit.BoxRemove(stackBox.Ptr(), node.container.Ptr())
715+ }
657716 }
658717
659718 // Clean up the pane
@@ -674,27 +733,39 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
674733 lastPaneContainer := lastPane.container
675734
676735 // Remove the title bar since we're going back to single pane
677- if lastPane.titleBar != 0 {
678- webkit.BoxRemove(stackBox, lastPane.titleBar)
736+ if lastPane.titleBar != nil && stackBox != nil {
737+ webkit.BoxRemove(stackBox.Ptr(), lastPane.titleBar.Ptr())
679738 }
680739
681740 // Unparent the pane container from stack wrapper
682- webkit.BoxRemove(stackBox, lastPaneContainer)
741+ if stackBox != nil && lastPaneContainer != nil {
742+ webkit.BoxRemove(stackBox.Ptr(), lastPaneContainer.Ptr())
743+ }
683744
684745 // CRITICAL FIX: Replace the stackNode completely with lastPane
685746 // Update parent child references FIRST
686747 if parent == nil {
687748 spm.wm.root = lastPane
688- if spm.wm.window != nil {
689- spm.wm.window.SetChild(lastPaneContainer)
749+ if spm.wm.window != nil && lastPaneContainer != nil {
750+ spm.wm.window.SetChild(lastPaneContainer.Ptr())
690751 }
691752 } else {
692753 if parent.left == stackNode {
693754 parent.left = lastPane
694- webkit.PanedSetStartChild(parent.container, lastPaneContainer)
755+ if parent.container != nil && lastPaneContainer != nil {
756+ parent.container.Execute(func(panedPtr uintptr) error {
757+ webkit.PanedSetStartChild(panedPtr, lastPaneContainer.Ptr())
758+ return nil
759+ })
760+ }
695761 } else if parent.right == stackNode {
696762 parent.right = lastPane
697- webkit.PanedSetEndChild(parent.container, lastPaneContainer)
763+ if parent.container != nil && lastPaneContainer != nil {
764+ parent.container.Execute(func(panedPtr uintptr) error {
765+ webkit.PanedSetEndChild(panedPtr, lastPaneContainer.Ptr())
766+ return nil
767+ })
768+ }
698769 }
699770 }
700771
@@ -702,11 +773,13 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
702773 lastPane.parent = parent
703774 lastPane.isStacked = false
704775 lastPane.stackedPanes = nil
705- lastPane.titleBar = 0
776+ lastPane.titleBar = nil
706777
707778 // CRITICAL: Ensure the container is visible after reparenting
708779 // The container may have been hidden during stack operations
709- webkit.WidgetSetVisible(lastPaneContainer, true)
780+ if lastPaneContainer != nil {
781+ webkit.WidgetSetVisible(lastPaneContainer.Ptr(), true)
782+ }
710783
711784 // Update the viewToNode mapping to point to the lastPane, not stackNode
712785 spm.wm.viewToNode[lastPane.pane.webView] = lastPane
@@ -82,14 +82,15 @@ func (spm *StackedPaneManager) prepareNewStackedPane() (*paneNode, error) {
82 82
83 // Create the new leaf node83 // Create the new leaf node
84 newLeaf := &paneNode{84 newLeaf := &paneNode{
85- pane: newPane,85+ pane: newPane,
86- container: newContainer,86+ isLeaf: true,
87- isLeaf: true,
88 }87 }
88+ // Initialize widgets properly using workspace manager helper
89+ spm.wm.initializePaneWidgets(newLeaf, newContainer)
89 90
90 // Create title bar for the new pane91 // Create title bar for the new pane
91 newTitleBar := spm.createTitleBar(newLeaf)92 newTitleBar := spm.createTitleBar(newLeaf)
92- newLeaf.titleBar = newTitleBar93+ spm.wm.setTitleBar(newLeaf, newTitleBar)
93 94
94 // Keep new container hidden initially - will be shown after transition95 // Keep new container hidden initially - will be shown after transition
95 webkit.WidgetSetVisible(newContainer, false)96 webkit.WidgetSetVisible(newContainer, false)
@@ -129,8 +130,13 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
129 stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane130 stackNode.stackedPanes[insertIndex] = newLeaf // Insert new pane
130 131
131 // Unparent the new container before adding it to the stack (only if it has a parent)132 // Unparent the new container before adding it to the stack (only if it has a parent)
132- if webkit.WidgetGetParent(newLeaf.container) != 0 {133+ if newLeaf.container != nil {
133- webkit.WidgetUnparent(newLeaf.container)134+ newLeaf.container.Execute(func(containerPtr uintptr) error {
135+ if webkit.WidgetGetParent(containerPtr) != 0 {
136+ webkit.WidgetUnparent(containerPtr)
137+ }
138+ return nil
139+ })
134 }140 }
135 141
136 // Insert widgets at correct position for Zellij-style layout142 // Insert widgets at correct position for Zellij-style layout
@@ -141,19 +147,29 @@ func (spm *StackedPaneManager) addPaneToExistingStack(target, newLeaf *paneNode)
141 if insertIndex > 0 {147 if insertIndex > 0 {
142 // Insert after the previous pane's container148 // Insert after the previous pane's container
143 prevPane := stackNode.stackedPanes[insertIndex-1]149 prevPane := stackNode.stackedPanes[insertIndex-1]
144- insertAfterWidget = prevPane.container150+ if prevPane.container != nil {
151+ insertAfterWidget = prevPane.container.Ptr()
152+ }
145 }153 }
146 154
147 if insertAfterWidget != 0 {155 if insertAfterWidget != 0 {
148 // Insert titleBar after the previous pane's container156 // Insert titleBar after the previous pane's container
149- webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.titleBar, insertAfterWidget)157+ if stackNode.stackWrapper != nil && newLeaf.titleBar != nil {
158+ webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr(), insertAfterWidget)
159+ }
150 // Insert container after the newly inserted titleBar160 // Insert container after the newly inserted titleBar
151- webkit.BoxInsertChildAfter(stackNode.stackWrapper, newLeaf.container, newLeaf.titleBar)161+ if stackNode.stackWrapper != nil && newLeaf.container != nil && newLeaf.titleBar != nil {
162+ webkit.BoxInsertChildAfter(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr(), newLeaf.titleBar.Ptr())
163+ }
152 log.Printf("[workspace] inserted widgets at position %d (after widget %#x)", insertIndex, insertAfterWidget)164 log.Printf("[workspace] inserted widgets at position %d (after widget %#x)", insertIndex, insertAfterWidget)
153 } else {165 } else {
154 // Insert at the beginning (insertIndex = 0)166 // Insert at the beginning (insertIndex = 0)
155- webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.container)167+ if stackNode.stackWrapper != nil && newLeaf.container != nil {
156- webkit.BoxPrepend(stackNode.stackWrapper, newLeaf.titleBar)168+ webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr())
169+ }
170+ if stackNode.stackWrapper != nil && newLeaf.titleBar != nil {
171+ webkit.BoxPrepend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr())
172+ }
157 log.Printf("[workspace] prepended widgets at position 0")173 log.Printf("[workspace] prepended widgets at position 0")
158 }174 }
159 175
@@ -199,7 +215,9 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
199 215
200 // Build the complete stack structure with hidden widgets216 // Build the complete stack structure with hidden widgets
201 webkit.BoxAppend(stackInternalBox, titleBar)217 webkit.BoxAppend(stackInternalBox, titleBar)
202- webkit.BoxAppend(stackInternalBox, existingContainer)218+ if existingContainer != nil {
219+ webkit.BoxAppend(stackInternalBox, existingContainer.Ptr())
220+ }
203 221
204 // Immediately reattach the stack wrapper to minimize visibility gap222 // Immediately reattach the stack wrapper to minimize visibility gap
205 spm.reattachToParent(stackWrapperContainer, target, parent)223 spm.reattachToParent(stackWrapperContainer, target, parent)
@@ -207,19 +225,19 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
207 // Convert target to a stacked leaf node225 // Convert target to a stacked leaf node
208 target.isStacked = true226 target.isStacked = true
209 target.isLeaf = true227 target.isLeaf = true
210- target.container = existingContainer228+ // container stays the same (existingContainer)
211- target.titleBar = titleBar229+ spm.wm.setTitleBar(target, titleBar)
212 230
213 // Create the stack container node - container points to wrapper, stackWrapper points to internal box231 // Create the stack container node - container points to wrapper, stackWrapper points to internal box
214 stackNode := &paneNode{232 stackNode := &paneNode{
215 isStacked: true,233 isStacked: true,
216 isLeaf: false,234 isLeaf: false,
217- container: stackWrapperContainer, // Wrapper for GTK operations (splits, etc.)
218- stackWrapper: stackInternalBox, // Internal box for stack operations
219 stackedPanes: []*paneNode{target},235 stackedPanes: []*paneNode{target},
220 activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0)236 activeStackIndex: 0, // KEEP CURRENT PANE ACTIVE during transition (index 0)
221 parent: parent,237 parent: parent,
222 }238 }
239+ spm.wm.setContainer(stackNode, stackWrapperContainer, "stack-wrapper") // Wrapper for GTK operations (splits, etc.)
240+ spm.wm.setStackWrapper(stackNode, stackInternalBox) // Internal box for stack operations
223 241
224 // Update target's parent to be the stack node242 // Update target's parent to be the stack node
225 target.parent = stackNode243 target.parent = stackNode
@@ -243,13 +261,22 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
243 stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf)261 stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf)
244 262
245 // Unparent the new container before adding it to the stack263 // Unparent the new container before adding it to the stack
246- if webkit.WidgetGetParent(newLeaf.container) != 0 {264+ if newLeaf.container != nil {
247- webkit.WidgetUnparent(newLeaf.container)265+ newLeaf.container.Execute(func(containerPtr uintptr) error {
266+ if webkit.WidgetGetParent(containerPtr) != 0 {
267+ webkit.WidgetUnparent(containerPtr)
268+ }
269+ return nil
270+ })
248 }271 }
249 272
250 // Add the new widgets to the internal stack box273 // Add the new widgets to the internal stack box
251- webkit.BoxAppend(stackNode.stackWrapper, newLeaf.titleBar)274+ if stackNode.stackWrapper != nil && newLeaf.titleBar != nil {
252- webkit.BoxAppend(stackNode.stackWrapper, newLeaf.container)275+ webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.titleBar.Ptr())
276+ }
277+ if stackNode.stackWrapper != nil && newLeaf.container != nil {
278+ webkit.BoxAppend(stackNode.stackWrapper.Ptr(), newLeaf.container.Ptr())
279+ }
253 280
254 return stackNode, insertIndex, nil281 return stackNode, insertIndex, nil
255 }282 }
@@ -262,16 +289,24 @@ func (spm *StackedPaneManager) detachFromParent(target *paneNode, parent *paneNo
262 spm.wm.window.SetChild(0)289 spm.wm.window.SetChild(0)
263 }290 }
264 // Unparent if it has a GTK parent291 // Unparent if it has a GTK parent
265- if webkit.WidgetGetParent(target.container) != 0 {292+ if target.container != nil {
266- webkit.WidgetUnparent(target.container)293+ target.container.Execute(func(containerPtr uintptr) error {
294+ if webkit.WidgetGetParent(containerPtr) != 0 {
295+ webkit.WidgetUnparent(containerPtr)
296+ }
297+ return nil
298+ })
267 }299 }
268- } else if parent.container != 0 {300+ } else if parent.container != nil {
269 // Target has a parent paned - remove it (automatically unparents in GTK4)301 // Target has a parent paned - remove it (automatically unparents in GTK4)
270- if parent.left == target {302+ parent.container.Execute(func(panedPtr uintptr) error {
271- webkit.PanedSetStartChild(parent.container, 0)303+ if parent.left == target {
272- } else if parent.right == target {304+ webkit.PanedSetStartChild(panedPtr, 0)
273- webkit.PanedSetEndChild(parent.container, 0)305+ } else if parent.right == target {
274- }306+ webkit.PanedSetEndChild(panedPtr, 0)
307+ }
308+ return nil
309+ })
275 }310 }
276 }311 }
277 312
@@ -281,12 +316,15 @@ func (spm *StackedPaneManager) reattachToParent(container uintptr, target *paneN
281 if spm.wm.window != nil {316 if spm.wm.window != nil {
282 spm.wm.window.SetChild(container)317 spm.wm.window.SetChild(container)
283 }318 }
284- } else if parent.container != 0 {319+ } else if parent.container != nil {
285- if parent.left == target {320+ parent.container.Execute(func(panedPtr uintptr) error {
286- webkit.PanedSetStartChild(parent.container, container)321+ if parent.left == target {
287- } else if parent.right == target {322+ webkit.PanedSetStartChild(panedPtr, container)
288- webkit.PanedSetEndChild(parent.container, container)323+ } else if parent.right == target {
289- }324+ webkit.PanedSetEndChild(panedPtr, container)
325+ }
326+ return nil
327+ })
290 }328 }
291 }329 }
292 330
@@ -310,7 +348,7 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod
310 currentActiveIndex := stackNode.activeStackIndex348 currentActiveIndex := stackNode.activeStackIndex
311 if currentActiveIndex >= 0 && currentActiveIndex < len(stackNode.stackedPanes) && currentActiveIndex != insertIndex {349 if currentActiveIndex >= 0 && currentActiveIndex < len(stackNode.stackedPanes) && currentActiveIndex != insertIndex {
312 currentlyActivePane := stackNode.stackedPanes[currentActiveIndex]350 currentlyActivePane := stackNode.stackedPanes[currentActiveIndex]
313- if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != 0 {351+ if currentlyActivePane.pane != nil && currentlyActivePane.pane.webView != nil && currentlyActivePane.titleBar != nil {
314 currentTitle := currentlyActivePane.pane.webView.GetTitle()352 currentTitle := currentlyActivePane.pane.webView.GetTitle()
315 if currentTitle != "" {353 if currentTitle != "" {
316 spm.updateTitleBarLabel(currentlyActivePane, currentTitle)354 spm.updateTitleBarLabel(currentlyActivePane, currentTitle)
@@ -364,17 +402,27 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
364 for i, pane := range stackNode.stackedPanes {402 for i, pane := range stackNode.stackedPanes {
365 if i == activeIndex {403 if i == activeIndex {
366 // Active pane: show container, NEVER show title bar404 // Active pane: show container, NEVER show title bar
367- webkit.WidgetSetVisible(pane.container, true)405+ if pane.container != nil {
368- webkit.WidgetSetVisible(pane.titleBar, false) // ABSOLUTE RULE: never visible for active pane406+ webkit.WidgetSetVisible(pane.container.Ptr(), true)
369- webkit.WidgetAddCSSClass(pane.container, "stacked-pane-active")407+ webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-active")
370- webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-collapsed")408+ webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-collapsed")
409+ }
410+ if pane.titleBar != nil {
411+ webkit.WidgetSetVisible(pane.titleBar.Ptr(), false) // ABSOLUTE RULE: never visible for active pane
412+ }
371 log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i)413 log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i)
372 } else {414 } else {
373 // Inactive panes: hide container, show title bar415 // Inactive panes: hide container, show title bar
374- webkit.WidgetSetVisible(pane.container, false)416+ if pane.container != nil {
375- webkit.WidgetSetVisible(pane.titleBar, true)417+ webkit.WidgetSetVisible(pane.container.Ptr(), false)
376- webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed")418+ webkit.WidgetAddCSSClass(pane.container.Ptr(), "stacked-pane-collapsed")
377- webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-active")419+ }
420+ if pane.titleBar != nil {
421+ webkit.WidgetSetVisible(pane.titleBar.Ptr(), true)
422+ }
423+ if pane.container != nil {
424+ webkit.WidgetRemoveCSSClass(pane.container.Ptr(), "stacked-pane-active")
425+ }
378 }426 }
379 }427 }
380 }428 }
@@ -436,7 +484,7 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
436 // Update title bar for the pane transitioning from ACTIVE to INACTIVE484 // Update title bar for the pane transitioning from ACTIVE to INACTIVE
437 // This ensures the collapsed pane shows its current page title (Zellij-style layout)485 // This ensures the collapsed pane shows its current page title (Zellij-style layout)
438 currentActivePane := stackNode.stackedPanes[currentIndex]486 currentActivePane := stackNode.stackedPanes[currentIndex]
439- if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != 0 {487+ if currentActivePane.pane != nil && currentActivePane.pane.webView != nil && currentActivePane.titleBar != nil {
440 currentTitle := currentActivePane.pane.webView.GetTitle()488 currentTitle := currentActivePane.pane.webView.GetTitle()
441 if currentTitle != "" {489 if currentTitle != "" {
442 spm.updateTitleBarLabel(currentActivePane, currentTitle)490 spm.updateTitleBarLabel(currentActivePane, currentTitle)
@@ -512,7 +560,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str
512 }560 }
513 561
514 // Check if this pane is in a stack and has a title bar562 // Check if this pane is in a stack and has a title bar
515- if node.parent != nil && node.parent.isStacked && node.titleBar != 0 {563+ if node.parent != nil && node.parent.isStacked && node.titleBar != nil {
516 // CRITICAL: Only update title bar for INACTIVE panes564 // CRITICAL: Only update title bar for INACTIVE panes
517 // Active panes should never show their title bar565 // Active panes should never show their title bar
518 stackNode := node.parent566 stackNode := node.parent
@@ -540,7 +588,7 @@ func (spm *StackedPaneManager) UpdateTitleBar(webView *webkit.WebView, title str
540 588
541 // updateTitleBarLabel updates the label widget within a title bar by recreating it589 // updateTitleBarLabel updates the label widget within a title bar by recreating it
542 func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) {590 func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string) {
543- if node == nil || node.titleBar == 0 || node.parent == nil || !node.parent.isStacked {591+ if node == nil || node.titleBar == nil || node.parent == nil || !node.parent.isStacked {
544 return592 return
545 }593 }
546 594
@@ -558,7 +606,7 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
558 }606 }
559 607
560 // Replace the old title bar in the stack608 // Replace the old title bar in the stack
561- if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != 0 {609+ if node.parent != nil && node.parent.isStacked && node.parent.stackWrapper != nil {
562 // Find the correct insertion position based on the pane's index in the stack610 // Find the correct insertion position based on the pane's index in the stack
563 paneIndex := -1611 paneIndex := -1
564 for i, stackedPane := range node.parent.stackedPanes {612 for i, stackedPane := range node.parent.stackedPanes {
@@ -569,19 +617,28 @@ func (spm *StackedPaneManager) updateTitleBarLabel(node *paneNode, title string)
569 }617 }
570 618
571 // Remove old title bar619 // Remove old title bar
572- webkit.WidgetUnparent(node.titleBar)620+ if node.titleBar != nil {
621+ node.titleBar.Execute(func(titleBarPtr uintptr) error {
622+ webkit.WidgetUnparent(titleBarPtr)
623+ return nil
624+ })
625+ }
573 626
574 // Update the node's title bar reference627 // Update the node's title bar reference
575- node.titleBar = newTitleBar628+ spm.wm.setTitleBar(node, newTitleBar)
576 629
577 // Insert the new title bar at the correct position630 // Insert the new title bar at the correct position
578 if paneIndex == 0 {631 if paneIndex == 0 {
579 // First pane - insert at the beginning632 // First pane - insert at the beginning
580- webkit.BoxPrepend(node.parent.stackWrapper, newTitleBar)633+ if node.parent.stackWrapper != nil {
634+ webkit.BoxPrepend(node.parent.stackWrapper.Ptr(), newTitleBar)
635+ }
581 } else {636 } else {
582 // Insert after the previous pane's container637 // Insert after the previous pane's container
583 prevPane := node.parent.stackedPanes[paneIndex-1]638 prevPane := node.parent.stackedPanes[paneIndex-1]
584- webkit.BoxInsertChildAfter(node.parent.stackWrapper, newTitleBar, prevPane.container)639+ if node.parent.stackWrapper != nil && prevPane.container != nil {
640+ webkit.BoxInsertChildAfter(node.parent.stackWrapper.Ptr(), newTitleBar, prevPane.container.Ptr())
641+ }
585 }642 }
586 643
587 log.Printf("[workspace] replaced title bar for pane %d: %p", paneIndex, node)644 log.Printf("[workspace] replaced title bar for pane %d: %p", paneIndex, node)
@@ -644,16 +701,18 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
644 node.pane.CleanupFromWorkspace(spm.wm)701 node.pane.CleanupFromWorkspace(spm.wm)
645 702
646 // Remove the pane's widgets from the stack container703 // Remove the pane's widgets from the stack container
647- stackBox := stackNode.stackWrapper704+ var stackBox *SafeWidget = stackNode.stackWrapper
648- if stackBox == 0 {705+ if stackBox == nil {
649 stackBox = stackNode.container706 stackBox = stackNode.container
650 }707 }
651 708
652- if node.titleBar != 0 {709+ if stackBox != nil {
653- webkit.BoxRemove(stackBox, node.titleBar)710+ if node.titleBar != nil {
654- }711+ webkit.BoxRemove(stackBox.Ptr(), node.titleBar.Ptr())
655- if node.container != 0 {712+ }
656- webkit.BoxRemove(stackBox, node.container)713+ if node.container != nil {
714+ webkit.BoxRemove(stackBox.Ptr(), node.container.Ptr())
715+ }
657 }716 }
658 717
659 // Clean up the pane718 // Clean up the pane
@@ -674,27 +733,39 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
674 lastPaneContainer := lastPane.container733 lastPaneContainer := lastPane.container
675 734
676 // Remove the title bar since we're going back to single pane735 // Remove the title bar since we're going back to single pane
677- if lastPane.titleBar != 0 {736+ if lastPane.titleBar != nil && stackBox != nil {
678- webkit.BoxRemove(stackBox, lastPane.titleBar)737+ webkit.BoxRemove(stackBox.Ptr(), lastPane.titleBar.Ptr())
679 }738 }
680 739
681 // Unparent the pane container from stack wrapper740 // Unparent the pane container from stack wrapper
682- webkit.BoxRemove(stackBox, lastPaneContainer)741+ if stackBox != nil && lastPaneContainer != nil {
742+ webkit.BoxRemove(stackBox.Ptr(), lastPaneContainer.Ptr())
743+ }
683 744
684 // CRITICAL FIX: Replace the stackNode completely with lastPane745 // CRITICAL FIX: Replace the stackNode completely with lastPane
685 // Update parent child references FIRST746 // Update parent child references FIRST
686 if parent == nil {747 if parent == nil {
687 spm.wm.root = lastPane748 spm.wm.root = lastPane
688- if spm.wm.window != nil {749+ if spm.wm.window != nil && lastPaneContainer != nil {
689- spm.wm.window.SetChild(lastPaneContainer)750+ spm.wm.window.SetChild(lastPaneContainer.Ptr())
690 }751 }
691 } else {752 } else {
692 if parent.left == stackNode {753 if parent.left == stackNode {
693 parent.left = lastPane754 parent.left = lastPane
694- webkit.PanedSetStartChild(parent.container, lastPaneContainer)755+ if parent.container != nil && lastPaneContainer != nil {
756+ parent.container.Execute(func(panedPtr uintptr) error {
757+ webkit.PanedSetStartChild(panedPtr, lastPaneContainer.Ptr())
758+ return nil
759+ })
760+ }
695 } else if parent.right == stackNode {761 } else if parent.right == stackNode {
696 parent.right = lastPane762 parent.right = lastPane
697- webkit.PanedSetEndChild(parent.container, lastPaneContainer)763+ if parent.container != nil && lastPaneContainer != nil {
764+ parent.container.Execute(func(panedPtr uintptr) error {
765+ webkit.PanedSetEndChild(panedPtr, lastPaneContainer.Ptr())
766+ return nil
767+ })
768+ }
698 }769 }
699 }770 }
700 771
@@ -702,11 +773,13 @@ func (spm *StackedPaneManager) CloseStackedPane(node *paneNode) error {
702 lastPane.parent = parent773 lastPane.parent = parent
703 lastPane.isStacked = false774 lastPane.isStacked = false
704 lastPane.stackedPanes = nil775 lastPane.stackedPanes = nil
705- lastPane.titleBar = 0776+ lastPane.titleBar = nil
706 777
707 // CRITICAL: Ensure the container is visible after reparenting778 // CRITICAL: Ensure the container is visible after reparenting
708 // The container may have been hidden during stack operations779 // The container may have been hidden during stack operations
709- webkit.WidgetSetVisible(lastPaneContainer, true)780+ if lastPaneContainer != nil {
781+ webkit.WidgetSetVisible(lastPaneContainer.Ptr(), true)
782+ }
710 783
711 // Update the viewToNode mapping to point to the lastPane, not stackNode784 // Update the viewToNode mapping to point to the lastPane, not stackNode
712 spm.wm.viewToNode[lastPane.pane.webView] = lastPane785 spm.wm.viewToNode[lastPane.pane.webView] = lastPane
A internal/app/browser/widget.go +133 -0
@@ -0,0 +1,133 @@
1+package browser
2+
3+import (
4+ "fmt"
5+ "sync"
6+
7+ "github.com/bnema/dumber/pkg/webkit"
8+)
9+
10+// SafeWidget provides thread-safe access to GTK widgets with validation
11+type SafeWidget struct {
12+ ptr uintptr
13+ typeInfo string
14+ registry *WidgetRegistry
15+}
16+
17+// Execute safely executes a function with the widget pointer, ensuring thread safety and validation
18+func (w *SafeWidget) Execute(fn func(uintptr) error) error {
19+ if w == nil || w.registry == nil {
20+ return fmt.Errorf("SafeWidget is nil or has no registry")
21+ }
22+
23+ w.registry.mu.Lock()
24+ defer w.registry.mu.Unlock()
25+
26+ if !w.IsValid() {
27+ return fmt.Errorf("widget %s is no longer valid", w.String())
28+ }
29+
30+ return fn(w.ptr)
31+}
32+
33+// IsValid checks if the underlying GTK widget is still valid
34+func (w *SafeWidget) IsValid() bool {
35+ if w == nil || w.ptr == 0 {
36+ return false
37+ }
38+ return webkit.WidgetIsValid(w.ptr)
39+}
40+
41+// String returns a string representation of the SafeWidget for debugging
42+func (w *SafeWidget) String() string {
43+ if w == nil {
44+ return "SafeWidget(nil)"
45+ }
46+ return fmt.Sprintf("SafeWidget(%s@%#x)", w.typeInfo, w.ptr)
47+}
48+
49+// Ptr returns the raw uintptr to the GTK widget
50+func (w *SafeWidget) Ptr() uintptr {
51+ if w == nil {
52+ return 0
53+ }
54+ return w.ptr
55+}
56+
57+// WidgetRegistry manages a collection of SafeWidget instances with thread safety
58+type WidgetRegistry struct {
59+ mu sync.RWMutex
60+ widgets map[uintptr]*SafeWidget
61+}
62+
63+// NewWidgetRegistry creates a new widget registry
64+func NewWidgetRegistry() *WidgetRegistry {
65+ return &WidgetRegistry{
66+ widgets: make(map[uintptr]*SafeWidget),
67+ }
68+}
69+
70+// Register creates a new SafeWidget and registers it in the registry
71+func (r *WidgetRegistry) Register(ptr uintptr, typeInfo string) *SafeWidget {
72+ if r == nil || ptr == 0 {
73+ return nil
74+ }
75+
76+ r.mu.Lock()
77+ defer r.mu.Unlock()
78+
79+ // Check if widget is already registered
80+ if existing, exists := r.widgets[ptr]; exists {
81+ return existing
82+ }
83+
84+ // Create new SafeWidget
85+ widget := &SafeWidget{
86+ ptr: ptr,
87+ typeInfo: typeInfo,
88+ registry: r,
89+ }
90+
91+ r.widgets[ptr] = widget
92+ return widget
93+}
94+
95+// Unregister removes a widget from the registry
96+func (r *WidgetRegistry) Unregister(ptr uintptr) {
97+ if r == nil || ptr == 0 {
98+ return
99+ }
100+
101+ r.mu.Lock()
102+ defer r.mu.Unlock()
103+
104+ delete(r.widgets, ptr)
105+}
106+
107+// Cleanup removes all invalid widgets from the registry
108+func (r *WidgetRegistry) Cleanup() {
109+ if r == nil {
110+ return
111+ }
112+
113+ r.mu.Lock()
114+ defer r.mu.Unlock()
115+
116+ for ptr, widget := range r.widgets {
117+ if !widget.IsValid() {
118+ delete(r.widgets, ptr)
119+ }
120+ }
121+}
122+
123+// Count returns the number of registered widgets
124+func (r *WidgetRegistry) Count() int {
125+ if r == nil {
126+ return 0
127+ }
128+
129+ r.mu.RLock()
130+ defer r.mu.RUnlock()
131+
132+ return len(r.widgets)
133+}⏎̸
@@ -0,0 +1,133 @@
1+package browser
2+
3+import (
4+ "fmt"
5+ "sync"
6+
7+ "github.com/bnema/dumber/pkg/webkit"
8+)
9+
10+// SafeWidget provides thread-safe access to GTK widgets with validation
11+type SafeWidget struct {
12+ ptr uintptr
13+ typeInfo string
14+ registry *WidgetRegistry
15+}
16+
17+// Execute safely executes a function with the widget pointer, ensuring thread safety and validation
18+func (w *SafeWidget) Execute(fn func(uintptr) error) error {
19+ if w == nil || w.registry == nil {
20+ return fmt.Errorf("SafeWidget is nil or has no registry")
21+ }
22+
23+ w.registry.mu.Lock()
24+ defer w.registry.mu.Unlock()
25+
26+ if !w.IsValid() {
27+ return fmt.Errorf("widget %s is no longer valid", w.String())
28+ }
29+
30+ return fn(w.ptr)
31+}
32+
33+// IsValid checks if the underlying GTK widget is still valid
34+func (w *SafeWidget) IsValid() bool {
35+ if w == nil || w.ptr == 0 {
36+ return false
37+ }
38+ return webkit.WidgetIsValid(w.ptr)
39+}
40+
41+// String returns a string representation of the SafeWidget for debugging
42+func (w *SafeWidget) String() string {
43+ if w == nil {
44+ return "SafeWidget(nil)"
45+ }
46+ return fmt.Sprintf("SafeWidget(%s@%#x)", w.typeInfo, w.ptr)
47+}
48+
49+// Ptr returns the raw uintptr to the GTK widget
50+func (w *SafeWidget) Ptr() uintptr {
51+ if w == nil {
52+ return 0
53+ }
54+ return w.ptr
55+}
56+
57+// WidgetRegistry manages a collection of SafeWidget instances with thread safety
58+type WidgetRegistry struct {
59+ mu sync.RWMutex
60+ widgets map[uintptr]*SafeWidget
61+}
62+
63+// NewWidgetRegistry creates a new widget registry
64+func NewWidgetRegistry() *WidgetRegistry {
65+ return &WidgetRegistry{
66+ widgets: make(map[uintptr]*SafeWidget),
67+ }
68+}
69+
70+// Register creates a new SafeWidget and registers it in the registry
71+func (r *WidgetRegistry) Register(ptr uintptr, typeInfo string) *SafeWidget {
72+ if r == nil || ptr == 0 {
73+ return nil
74+ }
75+
76+ r.mu.Lock()
77+ defer r.mu.Unlock()
78+
79+ // Check if widget is already registered
80+ if existing, exists := r.widgets[ptr]; exists {
81+ return existing
82+ }
83+
84+ // Create new SafeWidget
85+ widget := &SafeWidget{
86+ ptr: ptr,
87+ typeInfo: typeInfo,
88+ registry: r,
89+ }
90+
91+ r.widgets[ptr] = widget
92+ return widget
93+}
94+
95+// Unregister removes a widget from the registry
96+func (r *WidgetRegistry) Unregister(ptr uintptr) {
97+ if r == nil || ptr == 0 {
98+ return
99+ }
100+
101+ r.mu.Lock()
102+ defer r.mu.Unlock()
103+
104+ delete(r.widgets, ptr)
105+}
106+
107+// Cleanup removes all invalid widgets from the registry
108+func (r *WidgetRegistry) Cleanup() {
109+ if r == nil {
110+ return
111+ }
112+
113+ r.mu.Lock()
114+ defer r.mu.Unlock()
115+
116+ for ptr, widget := range r.widgets {
117+ if !widget.IsValid() {
118+ delete(r.widgets, ptr)
119+ }
120+ }
121+}
122+
123+// Count returns the number of registered widgets
124+func (r *WidgetRegistry) Count() int {
125+ if r == nil {
126+ return 0
127+ }
128+
129+ r.mu.RLock()
130+ defer r.mu.RUnlock()
131+
132+ return len(r.widgets)
133+}⏎̸
M internal/app/browser/workspace_manager.go +10 -10
@@ -15,16 +15,16 @@ import (
1515 )
1616
1717 type paneNode struct {
18- pane *BrowserPane
19- parent *paneNode
20- left *paneNode
21- right *paneNode
18+ pane *BrowserPane
19+ parent *paneNode
20+ left *paneNode
21+ right *paneNode
2222
2323 // Widget management with proper lifecycle tracking
24- container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves)
25- orientation webkit.Orientation
26- isLeaf bool
27- isPopup bool // Deprecated: use windowType instead
24+ container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves)
25+ orientation webkit.Orientation
26+ isLeaf bool
27+ isPopup bool // Deprecated: use windowType instead
2828
2929 // Window type tracking
3030 windowType webkit.WindowType // Tab or Popup
@@ -100,7 +100,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
100100 lastSplitMsg: make(map[*webkit.WebView]time.Time),
101101 lastExitMsg: make(map[*webkit.WebView]time.Time),
102102 paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator
103- widgetRegistry: NewWidgetRegistry(), // Initialize widget registry
103+ widgetRegistry: NewWidgetRegistry(), // Initialize widget registry
104104 }
105105
106106 // Initialize specialized managers
@@ -542,7 +542,7 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
542542 inactiveBorderColor,
543543 styling.BorderRadius,
544544 styling.BorderColor,
545- styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color
545+ styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color
546546 // Additional parameters for stacked pane styles
547547 windowBackgroundColor, // stacked-pane-container background
548548 styling.BorderRadius, // stacked-pane-container border-radius
@@ -15,16 +15,16 @@ import (
15 )15 )
16 16
17 type paneNode struct {17 type paneNode struct {
18- pane *BrowserPane18+ pane *BrowserPane
19- parent *paneNode19+ parent *paneNode
20- left *paneNode20+ left *paneNode
21- right *paneNode21+ right *paneNode
22 22
23 // Widget management with proper lifecycle tracking23 // Widget management with proper lifecycle tracking
24- container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves)24+ container *SafeWidget // Main container (GtkPaned for branch nodes, wrapper GtkBox for stacked nodes, WebView container for leaves)
25- orientation webkit.Orientation25+ orientation webkit.Orientation
26- isLeaf bool26+ isLeaf bool
27- isPopup bool // Deprecated: use windowType instead27+ isPopup bool // Deprecated: use windowType instead
28 28
29 // Window type tracking29 // Window type tracking
30 windowType webkit.WindowType // Tab or Popup30 windowType webkit.WindowType // Tab or Popup
@@ -100,7 +100,7 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
100 lastSplitMsg: make(map[*webkit.WebView]time.Time),100 lastSplitMsg: make(map[*webkit.WebView]time.Time),
101 lastExitMsg: make(map[*webkit.WebView]time.Time),101 lastExitMsg: make(map[*webkit.WebView]time.Time),
102 paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator102 paneDeduplicator: messaging.NewPaneRequestDeduplicator(), // NEW: Initialize deduplicator
103- widgetRegistry: NewWidgetRegistry(), // Initialize widget registry103+ widgetRegistry: NewWidgetRegistry(), // Initialize widget registry
104 }104 }
105 105
106 // Initialize specialized managers106 // Initialize specialized managers
@@ -542,7 +542,7 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
542 inactiveBorderColor,542 inactiveBorderColor,
543 styling.BorderRadius,543 styling.BorderRadius,
544 styling.BorderColor,544 styling.BorderColor,
545- styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color545+ styling.BorderColor, // NEW: .workspace-pane.workspace-pane-active border-color
546 // Additional parameters for stacked pane styles546 // Additional parameters for stacked pane styles
547 windowBackgroundColor, // stacked-pane-container background547 windowBackgroundColor, // stacked-pane-container background
548 styling.BorderRadius, // stacked-pane-container border-radius548 styling.BorderRadius, // stacked-pane-container border-radius

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