package ui
import (
"fmt"
"strings"
"updater-tui/internal/plan"
)
// field renders a label and value on one line, with the label in the muted role
// so the values are what the eye lands on.
func field(label, value string) string {
if value == "" {
value = "—"
}
return Muted.Render(fmt.Sprintf(" %-12s", label)) + Text.Render(value)
}
// advisoryLines renders a tracker record. Everything worth knowing is here so
// that following the link is genuinely optional.
func advisoryLines(d *plan.AdvisoryDetail) []string {
if !d.Available {
return []string{
Err.Render("the tracker could not be reached"),
"",
Muted.Render(d.Reason),
"",
Muted.Render("The advisory is still listed by arch-audit; only the"),
Muted.Render("extra detail needs the network."),
}
}
// severity decides the colour; whether a fix exists is its own field
style := StylesByRole(advisoryRole(d.Severity))
// the title already carries the id, so do not repeat it here
out := []string{
style.Render(d.Severity) + " " + Muted.Render(d.Status),
"",
field("type", d.Kind),
field("affected", d.Affected),
field("fixed", fixedLabel(d.Fixed)),
field("packages", strings.Join(d.Packages, ", ")),
field("ticket", d.Ticket),
field("notes", d.Notes),
}
if len(d.Advisories) > 0 {
out = append(out, field("see also", strings.Join(d.Advisories, ", ")))
}
out = append(out, "", Title.Render(fmt.Sprintf("%d CVE(s)", len(d.Issues))), "")
if len(d.Issues) == 0 {
out = append(out, Muted.Render(" the tracker recorded no CVE identifiers"))
}
for _, issue := range d.Issues {
out = append(out, " "+Ver.Render(issue))
}
if len(d.References) > 0 {
out = append(out, "", Title.Render("REFERENCES"), "")
for _, ref := range d.References {
out = append(out, " "+Muted.Render(ref))
}
}
out = append(out, "", Muted.Render(" "+d.URL))
return out
}
func fixedLabel(fixed string) string {
if fixed == "" {
return "no fix available for this version"
}
return fixed
}
// securityLines renders a fresh arch-audit run, fixable first.
func securityLines(r *plan.SecurityReport) []string {
if !r.Available {
return []string{Err.Render("arch-audit could not run"), "",
Muted.Render(r.Reason)}
}
out := []string{
Title.Render(fmt.Sprintf("%d advisories", r.Total)) + " " +
Muted.Render(fmt.Sprintf("%d fixable · %d with no fix",
r.Fixable, r.Unfixable)),
"",
}
if r.Total == 0 {
return append(out, OK.Render(" nothing affects this system"))
}
for _, a := range r.Advisories {
// severity decides the colour; fixability is said in words
style := StylesByRole(advisoryRole(a.Severity))
fix := "no fix available"
if a.HasFix() {
fix = "fixed in " + a.Fixed
}
out = append(out, " "+style.Render(fmt.Sprintf("%-13s", a.Severity))+
Title.Render(fmt.Sprintf("%-11s", a.ID))+
Muted.Render(fix))
if len(a.Packages) > 0 {
out = append(out, " "+Ver.Render(strings.Join(a.Packages, ", "))+
Muted.Render(fmt.Sprintf(" %d CVE(s)", len(a.Issues))))
}
}
out = append(out, "", Muted.Render(" press enter on a Security row for the full record"))
return out
}
// queueMarker returns the glyph and role for a stage, so the eye can tell a
// command from shell structure from something the audit flagged.
func queueMarker(stage plan.Stage) (string, string) {
switch stage.Level {
case "danger":
return "!!", "err"
case "warn":
return " !", "warn"
case "hook":
return "~ ", "muted"
case "note":
return ". ", "muted"
default:
return "> ", "text"
}
}
// phaseLabel names a group of stages. Per-package phases carry the package in
// the name, so "FETCH" does not appear eight times with nothing to tell them
// apart.
func phaseLabel(phase string) string {
if name, pkg, ok := strings.Cut(phase, ":"); ok && pkg != "" {
return phaseGroup(name) + " " + Muted.Render(pkg)
}
return phaseGroup(phase)
}
func phaseGroup(phase string) string {
switch {
case phase == "snapshot":
return "SNAPSHOT"
case phase == "sync":
return "SYNC"
case phase == "resolve":
return "RESOLVE"
case phase == "download":
return "DOWNLOAD"
case phase == "verify":
return "VERIFY"
case phase == "install":
return "INSTALL"
case phase == "hooks":
return "HOOKS"
case strings.HasPrefix(phase, "fetch"):
return "FETCH"
case strings.HasPrefix(phase, "source"):
return "SOURCES"
case strings.HasPrefix(phase, "prepare"):
return "PREPARE"
case strings.HasPrefix(phase, "build"):
return "BUILD"
case strings.HasPrefix(phase, "check"):
return "CHECK"
case strings.HasPrefix(phase, "package"):
return "PACKAGE"
case strings.HasPrefix(phase, "pack"):
return "PACK"
}
return strings.ToUpper(phase)
}