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:
Julien Bisconti
2026-03-10 16:20:41 +01:00
committed by GitHub
parent 05266bd8ac
commit a68d6f826d
12 changed files with 2555 additions and 645 deletions

View File

@@ -14,6 +14,7 @@ import (
"github.com/veggiemonk/awesome-docker/internal/linter"
"github.com/veggiemonk/awesome-docker/internal/parser"
"github.com/veggiemonk/awesome-docker/internal/scorer"
"github.com/veggiemonk/awesome-docker/internal/tui"
)
const (
@@ -49,6 +50,7 @@ func main() {
reportCmd(),
validateCmd(),
ciCmd(),
browseCmd(),
)
if err := root.Execute(); err != nil {
@@ -82,6 +84,24 @@ func collectURLs(sections []parser.Section, urls *[]string) {
}
}
type entryMeta struct {
Category string
Description string
}
func collectEntriesWithCategory(sections []parser.Section, parentPath string, out map[string]entryMeta) {
for _, s := range sections {
path := s.Title
if parentPath != "" {
path = parentPath + " > " + s.Title
}
for _, e := range s.Entries {
out[e.URL] = entryMeta{Category: path, Description: e.Description}
}
collectEntriesWithCategory(s.Children, path, out)
}
}
func runLinkChecks(prMode bool) (checkSummary, error) {
doc, err := parseReadme()
if err != nil {
@@ -159,6 +179,16 @@ func runHealth(ctx context.Context) error {
}
scored := scorer.ScoreAll(infos)
meta := make(map[string]entryMeta)
collectEntriesWithCategory(doc.Sections, "", meta)
for i := range scored {
if m, ok := meta[scored[i].URL]; ok {
scored[i].Category = m.Category
scored[i].Description = m.Description
}
}
cacheEntries := scorer.ToCacheEntries(scored)
hc, err := cache.LoadHealthCache(healthCachePath)
@@ -186,12 +216,15 @@ func scoredFromCache() ([]scorer.ScoredEntry, error) {
scored := make([]scorer.ScoredEntry, 0, len(hc.Entries))
for _, e := range hc.Entries {
scored = append(scored, scorer.ScoredEntry{
URL: e.URL,
Name: e.Name,
Status: scorer.Status(e.Status),
Stars: e.Stars,
HasLicense: e.HasLicense,
LastPush: e.LastPush,
URL: e.URL,
Name: e.Name,
Status: scorer.Status(e.Status),
Stars: e.Stars,
Forks: e.Forks,
HasLicense: e.HasLicense,
LastPush: e.LastPush,
Category: e.Category,
Description: e.Description,
})
}
return scored, nil
@@ -628,3 +661,23 @@ func ciHealthReportCmd() *cobra.Command {
cmd.Flags().BoolVar(&strict, "strict", false, "Return non-zero when health/report fails")
return cmd
}
func browseCmd() *cobra.Command {
var cachePath string
cmd := &cobra.Command{
Use: "browse",
Short: "Interactive TUI browser for awesome-docker resources",
RunE: func(cmd *cobra.Command, args []string) error {
hc, err := cache.LoadHealthCache(cachePath)
if err != nil {
return fmt.Errorf("load cache: %w", err)
}
if len(hc.Entries) == 0 {
return fmt.Errorf("no cache data; run 'awesome-docker health' first")
}
return tui.Run(hc.Entries)
},
}
cmd.Flags().StringVar(&cachePath, "cache", healthCachePath, "Path to health cache YAML")
return cmd
}