* 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>
119 lines
2.8 KiB
Markdown
119 lines
2.8 KiB
Markdown
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)
|
|
}
|
|
}
|
|
}
|