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) }