git/orchard/main
tui/internal/ui/detail_render.go
170 lines5 KB last change raw · history
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)
}