tree.go RawHistoryFolder
1package components
2
3import (
4 "strings"
5
6 "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
7)
8
9// Node represents a tree node with a title, optional subtitle, and children.
10type Node struct {
11 Title string
12 Subtitle string
13 Children []*Node
14}
15
16// AddChild adds a child node under this node.
17func (n *Node) AddChild(title, subtitle string) *Node {
18 child := &Node{Title: title, Subtitle: subtitle}
19 n.Children = append(n.Children, child)
20 return child
21}
22
23// Group represents a named group of nodes rendered with a vertical bar connector.
24type Group struct {
25 Name string
26 Nodes []*Node
27}
28
29// AddNode adds a node inside this group.
30func (g *Group) AddNode(title, subtitle string) *Node {
31 node := &Node{Title: title, Subtitle: subtitle}
32 g.Nodes = append(g.Nodes, node)
33 return node
34}
35
36// treeItem is a union type for ordering nodes and groups.
37type treeItem struct {
38 node *Node
39 group *Group
40}
41
42// Tree is a renderable tree structure for terminal output.
43type Tree struct {
44 items []treeItem
45}
46
47// NewTree creates a new empty tree.
48func NewTree() *Tree {
49 return &Tree{}
50}
51
52// AddNode adds a top-level node with a pre-styled title and a plain subtitle.
53func (t *Tree) AddNode(title, subtitle string) *Node {
54 node := &Node{Title: title, Subtitle: subtitle}
55 t.items = append(t.items, treeItem{node: node})
56 return node
57}
58
59// AddGroup adds a named group header.
60func (t *Tree) AddGroup(name string) *Group {
61 group := &Group{Name: name}
62 t.items = append(t.items, treeItem{group: group})
63 return group
64}
65
66// Render returns the full styled string output.
67func (t *Tree) Render() string {
68 if len(t.items) == 0 {
69 return ""
70 }
71
72 var b strings.Builder
73 for _, item := range t.items {
74 if item.group != nil {
75 t.renderGroup(&b, item.group)
76 } else {
77 t.renderNode(&b, item.node, " ")
78 }
79 }
80 return b.String()
81}
82
83func (t *Tree) renderGroup(b *strings.Builder, group *Group) {
84 // Group header: ◆ group-name
85 icon := styles.Theme.Highlight.Render(styles.IconGroup)
86 name := styles.Theme.Bold.Render(group.Name)
87 b.WriteString(" " + icon + " " + name + "\n")
88
89 if len(group.Nodes) == 0 {
90 return
91 }
92
93 vertBar := styles.Theme.Muted.Render(styles.IconTreeVert)
94 b.WriteString(" " + vertBar + "\n")
95
96 for _, node := range group.Nodes {
97 t.renderNode(b, node, " "+vertBar+" ")
98 }
99}
100
101func (t *Tree) renderNode(b *strings.Builder, node *Node, prefix string) {
102 // Title line
103 b.WriteString(prefix + node.Title + "\n")
104
105 // Subtitle line (muted, with tree connector aligned under container icon)
106 if node.Subtitle != "" {
107 var connectorGlyph string
108 if len(node.Children) > 0 {
109 connectorGlyph = styles.IconTreeBranch + styles.IconTreeLine // ├─ more items below
110 } else {
111 connectorGlyph = styles.IconTreeLast + styles.IconTreeLine // └─ last item
112 }
113 connector := styles.Theme.Muted.Render(connectorGlyph)
114 subtitle := styles.Theme.Muted.Render(node.Subtitle)
115 b.WriteString(prefix + " " + connector + " " + subtitle + "\n")
116 }
117
118 // Children
119 for i, child := range node.Children {
120 isLast := i == len(node.Children)-1
121
122 var branch string
123 var childPrefix string
124 if isLast {
125 branch = styles.Theme.Muted.Render(styles.IconTreeLast + styles.IconTreeLine)
126 childPrefix = " "
127 } else {
128 branch = styles.Theme.Muted.Render(styles.IconTreeBranch + styles.IconTreeLine)
129 childPrefix = styles.Theme.Muted.Render(styles.IconTreeVert) + " "
130 }
131
132 // Child title
133 b.WriteString(prefix + " " + branch + " " + child.Title + "\n")
134
135 // Child subtitle (with tree connector under child name)
136 if child.Subtitle != "" {
137 connector := styles.Theme.Muted.Render(styles.IconTreeLast + styles.IconTreeLine)
138 subtitle := styles.Theme.Muted.Render(child.Subtitle)
139 b.WriteString(prefix + " " + childPrefix + " " + connector + " " + subtitle + "\n")
140 }
141 }
142}