Issues #11-13: WithAppName for variant binaries, NewPassthrough builder for flag.FlagSet commands, RegisterCommands test coverage with resetGlobals helper. Fix pre-existing daemon_test.go break. Issue #14: Rich Table with box-drawing borders (Normal, Rounded, Heavy, Double), per-column CellStyleFn, WithMaxWidth responsive truncation. Tree renderer with box-drawing connectors and styled nodes. Parallel TaskTracker with braille spinners, thread-safe concurrent updates, and non-TTY fallback. Streaming text renderer with word-wrap and channel pattern support. Issue #15: Frame live compositional AppShell using HLCRF regions with Model interface, Navigate/Back content swapping, alt-screen live mode, graceful non-TTY fallback. Built-in region components: StatusLine, KeyHints, Breadcrumb. Zero new dependencies — pure ANSI + x/term. 68 tests, all passing with -race. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
2.3 KiB
Go
98 lines
2.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// TreeNode represents a node in a displayable tree structure.
|
|
// Use NewTree to create a root, then Add children.
|
|
//
|
|
// tree := cli.NewTree("core-php")
|
|
// tree.Add("core-tenant").Add("core-bio")
|
|
// tree.Add("core-admin")
|
|
// tree.Add("core-api")
|
|
// fmt.Print(tree)
|
|
// // core-php
|
|
// // ├── core-tenant
|
|
// // │ └── core-bio
|
|
// // ├── core-admin
|
|
// // └── core-api
|
|
type TreeNode struct {
|
|
label string
|
|
style *AnsiStyle
|
|
children []*TreeNode
|
|
}
|
|
|
|
// NewTree creates a new tree with the given root label.
|
|
func NewTree(label string) *TreeNode {
|
|
return &TreeNode{label: label}
|
|
}
|
|
|
|
// Add appends a child node and returns the child for chaining.
|
|
func (n *TreeNode) Add(label string) *TreeNode {
|
|
child := &TreeNode{label: label}
|
|
n.children = append(n.children, child)
|
|
return child
|
|
}
|
|
|
|
// AddStyled appends a styled child node and returns the child for chaining.
|
|
func (n *TreeNode) AddStyled(label string, style *AnsiStyle) *TreeNode {
|
|
child := &TreeNode{label: label, style: style}
|
|
n.children = append(n.children, child)
|
|
return child
|
|
}
|
|
|
|
// AddTree appends an existing tree as a child and returns the parent for chaining.
|
|
func (n *TreeNode) AddTree(child *TreeNode) *TreeNode {
|
|
n.children = append(n.children, child)
|
|
return n
|
|
}
|
|
|
|
// WithStyle sets the style on this node and returns it for chaining.
|
|
func (n *TreeNode) WithStyle(style *AnsiStyle) *TreeNode {
|
|
n.style = style
|
|
return n
|
|
}
|
|
|
|
// String renders the tree with box-drawing characters.
|
|
// Implements fmt.Stringer.
|
|
func (n *TreeNode) String() string {
|
|
var sb strings.Builder
|
|
sb.WriteString(n.renderLabel())
|
|
sb.WriteByte('\n')
|
|
n.writeChildren(&sb, "")
|
|
return sb.String()
|
|
}
|
|
|
|
// Render prints the tree to stdout.
|
|
func (n *TreeNode) Render() {
|
|
fmt.Print(n.String())
|
|
}
|
|
|
|
func (n *TreeNode) renderLabel() string {
|
|
if n.style != nil {
|
|
return n.style.Render(n.label)
|
|
}
|
|
return n.label
|
|
}
|
|
|
|
func (n *TreeNode) writeChildren(sb *strings.Builder, prefix string) {
|
|
for i, child := range n.children {
|
|
last := i == len(n.children)-1
|
|
|
|
connector := "├── "
|
|
next := "│ "
|
|
if last {
|
|
connector = "└── "
|
|
next = " "
|
|
}
|
|
|
|
sb.WriteString(prefix)
|
|
sb.WriteString(connector)
|
|
sb.WriteString(child.renderLabel())
|
|
sb.WriteByte('\n')
|
|
|
|
child.writeChildren(sb, prefix+next)
|
|
}
|
|
}
|