feat: add prune subcommand, drop archived/stale entries (#1441)
Deploy to GitHub Pages / build (push) Failing after 51s
Deploy to GitHub Pages / deploy (push) Has been skipped
Pull Requests / Weekly QA / test (push) Failing after 1m13s
Broken Links Report / check-links (push) Failing after 45s

* feat: add prune subcommand, drop archived/stale entries, add container-explorer

Add a new `awesome-docker prune` subcommand that removes README entries
whose repository health status matches a configurable set (default:
archived,stale). URLs are read from the local health cache, or from a
markdown report file via --from-report when the cache is outdated.

Apply it against the issue #1439 health report to remove 5 entries
that survived the recent reorg: stitchocker, docker-consul,
blockbridge-docker-volume, docker-explorer, dockdash.

Add google/container-explorer in the Security section as the actively
maintained successor to the now-archived google/docker-explorer.

Co-Authored-By: Claude <noreply@anthropic.com>

* golangci-lint config

* fix: address golangci-lint findings

Fixes errcheck on bufio.Writer.WriteString, gocritic rangeValCopy via
indexed loops with pointer locals, gosec G703 on user-supplied CLI
output path, noctx by switching to exec.CommandContext with a timeout
in the TUI url opener, prealloc in the scorer test, plus fieldalignment
struct reorders and golines line breaks from --fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Julien Bisconti
2026-05-18 23:46:32 +02:00
committed by GitHub
parent 503e5bd7c7
commit 29222bfcb5
23 changed files with 2548 additions and 2803 deletions
+43 -73
View File
@@ -1,10 +1,12 @@
package tui
import (
"context"
"fmt"
"os/exec"
"runtime"
"strings"
"time"
"unicode/utf8"
tea "charm.land/bubbletea/v2"
@@ -19,25 +21,25 @@ const (
panelList
)
const entryHeight = 5 // lines rendered per entry in the list panel
const scrollOff = 4 // minimum lines/entries kept visible above and below cursor
const (
entryHeight = 5 // lines rendered per entry in the list panel
scrollOff = 4 // minimum lines/entries kept visible above and below cursor
)
// Model is the top-level Bubbletea model.
type Model struct {
roots []*TreeNode
flatTree []FlatNode
filterText string
roots []*TreeNode
flatTree []FlatNode
currentEntries []cache.HealthEntry
activePanel panel
treeCursor int
treeOffset int
listCursor int
listOffset int
currentEntries []cache.HealthEntry
filtering bool
filterText string
width, height int
width int
height int
filtering bool
}
// New creates a new Model from health cache entries.
@@ -135,11 +137,13 @@ func (m *Model) applyFilter() {
query := strings.ToLower(m.filterText)
var filtered []cache.HealthEntry
for _, root := range m.roots {
for _, e := range root.AllEntries() {
entries := root.AllEntries()
for i := range entries {
e := &entries[i]
if strings.Contains(strings.ToLower(e.Name), query) ||
strings.Contains(strings.ToLower(e.Description), query) ||
strings.Contains(strings.ToLower(e.Category), query) {
filtered = append(filtered, e)
filtered = append(filtered, *e)
}
}
}
@@ -176,10 +180,7 @@ func (m Model) handleTreeKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.updateCurrentEntries()
}
case "ctrl+d", "pgdown":
half := m.treePanelHeight() / 2
if half < 1 {
half = 1
}
half := max(m.treePanelHeight()/2, 1)
m.treeCursor += half
if m.treeCursor >= len(m.flatTree) {
m.treeCursor = len(m.flatTree) - 1
@@ -187,10 +188,7 @@ func (m Model) handleTreeKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.adjustTreeScroll()
m.updateCurrentEntries()
case "ctrl+u", "pgup":
half := m.treePanelHeight() / 2
if half < 1 {
half = 1
}
half := max(m.treePanelHeight()/2, 1)
m.treeCursor -= half
if m.treeCursor < 0 {
m.treeCursor = 0
@@ -233,10 +231,7 @@ func (m Model) handleTreeKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
func (m *Model) adjustTreeScroll() {
visible := m.treePanelHeight()
off := scrollOff
if off > visible/2 {
off = visible / 2
}
off := min(scrollOff, visible/2)
if m.treeCursor < m.treeOffset+off {
m.treeOffset = m.treeCursor - off
}
@@ -249,10 +244,10 @@ func (m *Model) adjustTreeScroll() {
}
func (m Model) treePanelHeight() int {
h := m.height - 6 // header, footer, borders, title
if h < 1 {
h = 1
}
h := max(
// header, footer, borders, title
m.height-6, 1,
)
return h
}
@@ -269,20 +264,14 @@ func (m Model) handleListKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.adjustListScroll()
}
case "ctrl+d", "pgdown":
half := m.visibleListEntries() / 2
if half < 1 {
half = 1
}
half := max(m.visibleListEntries()/2, 1)
m.listCursor += half
if m.listCursor >= len(m.currentEntries) {
m.listCursor = len(m.currentEntries) - 1
}
m.adjustListScroll()
case "ctrl+u", "pgup":
half := m.visibleListEntries() / 2
if half < 1 {
half = 1
}
half := max(m.visibleListEntries()/2, 1)
m.listCursor -= half
if m.listCursor < 0 {
m.listCursor = 0
@@ -328,10 +317,7 @@ func (m Model) visibleListEntries() int {
func (m *Model) adjustListScroll() {
visible := m.visibleListEntries()
off := scrollOff
if off > visible/2 {
off = visible / 2
}
off := min(scrollOff, visible/2)
if m.listCursor < m.listOffset+off {
m.listOffset = m.listCursor - off
}
@@ -345,10 +331,7 @@ func (m *Model) adjustListScroll() {
func (m Model) listPanelHeight() int {
// height minus header, footer, borders
h := m.height - 4
if h < 1 {
h = 1
}
h := max(m.height-4, 1)
return h
}
@@ -406,10 +389,7 @@ func (m Model) renderTree(width, height int) string {
b.WriteString("\n\n")
linesUsed := 2
end := m.treeOffset + height - 2
if end > len(m.flatTree) {
end = len(m.flatTree)
}
end := min(m.treeOffset+height-2, len(m.flatTree))
for i := m.treeOffset; i < end; i++ {
fn := m.flatTree[i]
if linesUsed >= height {
@@ -466,16 +446,10 @@ func (m Model) renderList(width, height int) string {
linesUsed := 2
visible := (height - 2) / entryHeight
if visible < 1 {
visible = 1
}
visible := max((height-2)/entryHeight, 1)
start := m.listOffset
end := start + visible
if end > len(m.currentEntries) {
end = len(m.currentEntries)
}
end := min(start+visible, len(m.currentEntries))
for idx := start; idx < end; idx++ {
if linesUsed+entryHeight > height {
@@ -496,19 +470,16 @@ func (m Model) renderList(width, height int) string {
}
name := e.Name
statsW := lipgloss.Width(stats)
maxName := safeWidth - statsW - 2 // 2 for minimum gap
if maxName < 4 {
maxName = 4
}
maxName := max(
// 2 for minimum gap
safeWidth-statsW-2, 4,
)
if lipgloss.Width(name) > maxName {
name = truncateToWidth(name, maxName-1) + "…"
}
nameStr := entryNameStyle.Render(name)
statsStr := entryDescStyle.Render(stats)
padding := safeWidth - lipgloss.Width(nameStr) - lipgloss.Width(statsStr)
if padding < 1 {
padding = 1
}
padding := max(safeWidth-lipgloss.Width(nameStr)-lipgloss.Width(statsStr), 1)
line1 := nameStr + strings.Repeat(" ", padding) + statsStr
// Line 2: URL
@@ -529,15 +500,12 @@ func (m Model) renderList(width, height int) string {
statusStr := statusStyle(e.Status).Render(e.Status)
lastPush := ""
if !e.LastPush.IsZero() {
lastPush = fmt.Sprintf(" Last push: %s", e.LastPush.Format("2006-01-02"))
lastPush = " Last push: " + e.LastPush.Format("2006-01-02")
}
line4 := statusStr + entryDescStyle.Render(lastPush)
// Line 5: separator
sepWidth := safeWidth
if sepWidth < 1 {
sepWidth = 1
}
sepWidth := max(safeWidth, 1)
line5 := entryDescStyle.Render(strings.Repeat("─", sepWidth))
entry := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", line1, line2, line3, line4, line5)
@@ -573,14 +541,16 @@ type openURLMsg struct{ err error }
func openURL(url string) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin":
cmd = exec.Command("open", url)
cmd = exec.CommandContext(ctx, "open", url)
case "windows":
cmd = exec.Command("cmd", "/c", "start", url)
cmd = exec.CommandContext(ctx, "cmd", "/c", "start", url)
default:
cmd = exec.Command("xdg-open", url)
cmd = exec.CommandContext(ctx, "xdg-open", url)
}
return openURLMsg{err: cmd.Run()}
}