1package browser
2
3import (
4 "fmt"
5 "strings"
6 "sync"
7
8 "github.com/bnema/dumber/internal/logging"
9)
10
11type TreeSnapshot struct {
12 Label string
13 Lines []string
14}
15
16type WorkspaceDiagnostics struct {
17 mu sync.Mutex
18 enabled bool
19 snapshots []TreeSnapshot
20}
21
22func NewWorkspaceDiagnostics(enabled bool) *WorkspaceDiagnostics {
23 return &WorkspaceDiagnostics{enabled: enabled}
24}
25
26func (d *WorkspaceDiagnostics) Enabled() bool {
27 if d == nil {
28 return false
29 }
30 d.mu.Lock()
31 defer d.mu.Unlock()
32 return d.enabled
33}
34
35func (d *WorkspaceDiagnostics) SetEnabled(enabled bool) {
36 if d == nil {
37 return
38 }
39 d.mu.Lock()
40 d.enabled = enabled
41 d.mu.Unlock()
42}
43
44func (d *WorkspaceDiagnostics) Capture(label string, root *paneNode) TreeSnapshot {
45 if d == nil {
46 return TreeSnapshot{}
47 }
48
49 d.mu.Lock()
50 enabled := d.enabled
51 d.mu.Unlock()
52 if !enabled {
53 return TreeSnapshot{}
54 }
55
56 snapshot := TreeSnapshot{Label: label}
57 visited := make(map[*paneNode]bool)
58 buildTreeSnapshot(root, 0, "", visited, &snapshot.Lines)
59 if len(snapshot.Lines) == 0 {
60 snapshot.Lines = append(snapshot.Lines, "<empty workspace>")
61 }
62
63 d.mu.Lock()
64 d.snapshots = append(d.snapshots, snapshot)
65 d.mu.Unlock()
66
67 logging.Info(fmt.Sprintf("[pane-close] snapshot %s\n%s", snapshot.Label, strings.Join(snapshot.Lines, "\n")))
68 return snapshot
69}
70
71func (d *WorkspaceDiagnostics) Snapshots() []TreeSnapshot {
72 if d == nil {
73 return nil
74 }
75 d.mu.Lock()
76 defer d.mu.Unlock()
77
78 out := make([]TreeSnapshot, len(d.snapshots))
79 copy(out, d.snapshots)
80 return out
81}
82
83func buildTreeSnapshot(node *paneNode, depth int, prefix string, visited map[*paneNode]bool, lines *[]string) {
84 indent := strings.Repeat(" ", depth)
85 if node == nil {
86 *lines = append(*lines, fmt.Sprintf("%s%s<nil>", indent, prefix))
87 return
88 }
89
90 if depth > 64 {
91 *lines = append(*lines, fmt.Sprintf("%s%s<max depth reached>", indent, prefix))
92 return
93 }
94
95 if visited[node] {
96 *lines = append(*lines, fmt.Sprintf("%s%s<cycle node=%p>", indent, prefix, node))
97 return
98 }
99
100 visited[node] = true
101 *lines = append(*lines, fmt.Sprintf("%s%s%s", indent, prefix, describePaneNode(node)))
102
103 if node.isStacked {
104 for idx, child := range node.stackedPanes {
105 childPrefix := fmt.Sprintf("[%d] ", idx)
106 buildTreeSnapshot(child, depth+1, childPrefix, visited, lines)
107 }
108 return
109 }
110
111 if node.left != nil || node.right != nil {
112 buildTreeSnapshot(node.left, depth+1, "L: ", visited, lines)
113 buildTreeSnapshot(node.right, depth+1, "R: ", visited, lines)
114 }
115}
116
117func describePaneNode(node *paneNode) string {
118 if node == nil {
119 return "<nil>"
120 }
121
122 parts := []string{fmt.Sprintf("node=%p", node)}
123
124 if node.isLeaf {
125 parts = append(parts, "leaf")
126 if node.pane != nil && node.pane.ID() != "" {
127 parts = append(parts, fmt.Sprintf("pane=%s", node.pane.ID()))
128 }
129 if node.isPopup {
130 parts = append(parts, "popup")
131 }
132 } else if node.isStacked {
133 parts = append(parts, fmt.Sprintf("stack size=%d active=%d", len(node.stackedPanes), node.activeStackIndex))
134 } else {
135 parts = append(parts, fmt.Sprintf("split orientation=%d", node.orientation))
136 }
137
138 if node.container != nil {
139 parts = append(parts, fmt.Sprintf("widget=%p", node.container))
140 }
141
142 if node.parent != nil {
143 parts = append(parts, fmt.Sprintf("parent=%p", node.parent))
144 }
145
146 if !node.isLeaf && !node.isStacked {
147 if node.left != nil {
148 parts = append(parts, fmt.Sprintf("left=%p", node.left))
149 }
150 if node.right != nil {
151 parts = append(parts, fmt.Sprintf("right=%p", node.right))
152 }
153 }
154
155 return strings.Join(parts, " ")
156}