Feat/tui browse (#1266)
* Add interactive TUI browser command using Bubbletea v2 Adds `awesome-docker browse` to interactively explore the curated list in a terminal UI with a category tree (left panel) and detailed resource view (right panel). Enriches health_cache.yaml with category and description fields so the cache is self-contained for the TUI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add TUI pagination, scrolloff, and fix visual overflow Add pagination keybindings (Ctrl+D/PgDn, Ctrl+U/PgUp, g/Home, G/End) to both tree and list panels. Implement scrolloff (4 lines) to keep context visible around the cursor. Fix list panel overflow caused by Unicode characters (★, ⑂) rendering wider than lipgloss measures, which pushed the footer off-screen. Improve selection highlight visibility with background colors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
118
internal/tui/tree.go
Normal file
118
internal/tui/tree.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/veggiemonk/awesome-docker/internal/cache"
|
||||
)
|
||||
|
||||
// TreeNode represents a node in the category tree.
|
||||
type TreeNode struct {
|
||||
Name string // display name (leaf segment, e.g. "Networking")
|
||||
Path string // full path (e.g. "Container Operations > Networking")
|
||||
Children []*TreeNode
|
||||
Expanded bool
|
||||
Entries []cache.HealthEntry
|
||||
}
|
||||
|
||||
// FlatNode is a visible tree node with its indentation depth.
|
||||
type FlatNode struct {
|
||||
Node *TreeNode
|
||||
Depth int
|
||||
}
|
||||
|
||||
// HasChildren returns true if this node has child categories.
|
||||
func (n *TreeNode) HasChildren() bool {
|
||||
return len(n.Children) > 0
|
||||
}
|
||||
|
||||
// TotalEntries returns the count of entries in this node and all descendants.
|
||||
func (n *TreeNode) TotalEntries() int {
|
||||
count := len(n.Entries)
|
||||
for _, c := range n.Children {
|
||||
count += c.TotalEntries()
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// AllEntries returns entries from this node and all descendants.
|
||||
func (n *TreeNode) AllEntries() []cache.HealthEntry {
|
||||
result := make([]cache.HealthEntry, 0, n.TotalEntries())
|
||||
result = append(result, n.Entries...)
|
||||
for _, c := range n.Children {
|
||||
result = append(result, c.AllEntries()...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BuildTree constructs a tree from flat HealthEntry slice, grouping by Category.
|
||||
func BuildTree(entries []cache.HealthEntry) []*TreeNode {
|
||||
root := &TreeNode{Name: "root"}
|
||||
nodeMap := map[string]*TreeNode{}
|
||||
|
||||
for _, e := range entries {
|
||||
cat := e.Category
|
||||
if cat == "" {
|
||||
cat = "Uncategorized"
|
||||
}
|
||||
|
||||
node := ensureNode(root, nodeMap, cat)
|
||||
node.Entries = append(node.Entries, e)
|
||||
}
|
||||
|
||||
// Sort children at every level
|
||||
sortTree(root)
|
||||
return root.Children
|
||||
}
|
||||
|
||||
func ensureNode(root *TreeNode, nodeMap map[string]*TreeNode, path string) *TreeNode {
|
||||
if n, ok := nodeMap[path]; ok {
|
||||
return n
|
||||
}
|
||||
|
||||
parts := strings.Split(path, " > ")
|
||||
current := root
|
||||
for i, part := range parts {
|
||||
subpath := strings.Join(parts[:i+1], " > ")
|
||||
if n, ok := nodeMap[subpath]; ok {
|
||||
current = n
|
||||
continue
|
||||
}
|
||||
child := &TreeNode{
|
||||
Name: part,
|
||||
Path: subpath,
|
||||
}
|
||||
current.Children = append(current.Children, child)
|
||||
nodeMap[subpath] = child
|
||||
current = child
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func sortTree(node *TreeNode) {
|
||||
sort.Slice(node.Children, func(i, j int) bool {
|
||||
return node.Children[i].Name < node.Children[j].Name
|
||||
})
|
||||
for _, c := range node.Children {
|
||||
sortTree(c)
|
||||
}
|
||||
}
|
||||
|
||||
// FlattenVisible returns visible nodes in depth-first order for rendering.
|
||||
func FlattenVisible(roots []*TreeNode) []FlatNode {
|
||||
var result []FlatNode
|
||||
for _, r := range roots {
|
||||
flattenNode(r, 0, &result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func flattenNode(node *TreeNode, depth int, result *[]FlatNode) {
|
||||
*result = append(*result, FlatNode{Node: node, Depth: depth})
|
||||
if node.Expanded {
|
||||
for _, c := range node.Children {
|
||||
flattenNode(c, depth+1, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user