v2.31.1 f37fe67

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}

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 7

Tags 106

↑↓ moveenter openesc close