mirror of
https://github.com/veggiemonk/awesome-docker.git
synced 2026-07-02 03:20:32 +02:00
feat: add prune subcommand, drop archived/stale entries (#1441)
* 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:
+43
-73
@@ -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()}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user