/** * KHAELOR * File: src/tui/components/diff.ts * Description: Inline edit summaries and unified diff blocks — +/− glyphs preserved for monochrome (TUI_DESIGN §7.1). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { highlightLine } from "../markdown/highlight.js"; import { truncateAnsi } from "../renderer/ansi.js"; import { rule } from "./tool-line.js"; import type { Theme } from "../theme.js"; export interface DiffStats { added: number; removed: number; } /** Never bare "Edited file": `✓ path +31 −12 d expand diff`. */ export function renderEditSummary( path: string, stats: DiffStats, width: number, theme: Theme, hint = true, ): string { let line = ` ${theme.paint("success", "✓")} ${path} ` + `${theme.paint("success", `+${stats.added}`)} ${theme.paint("error", `−${stats.removed}`)}`; if (hint) line += theme.paint("dim", " d expand diff"); return truncateAnsi(line, width); } const EXT_LANG: Record = { ts: "ts", tsx: "tsx", js: "js", jsx: "jsx", py: "python", sh: "bash", rs: "rust", go: "go", json: "json", yaml: "yaml", yml: "yaml", toml: "toml", }; function langFor(path: string): string | null { const ext = /\.([a-z]+)$/i.exec(path)?.[1]?.toLowerCase(); return ext !== undefined ? (EXT_LANG[ext] ?? null) : null; } function highlight(code: string, lang: string | null, theme: Theme): string { return highlightLine(code, lang) .map((s) => (s.role === "text" ? s.text : theme.paint(s.role, s.text))) .join(""); } /** * Unified diff as a settled block: `+` lines in the added color, `−` in the * removed color, glyphs preserved so monochrome terminals keep the meaning. * Syntax highlighting rides on the same theme palette. */ export function renderDiffBlock( path: string, unifiedDiff: string, stats: DiffStats, width: number, theme: Theme, ): string[] { const lang = langFor(path); const out: string[] = [rule(` diff · ${path} · +${stats.added} −${stats.removed} `, width, theme)]; for (const raw of unifiedDiff.split("\n")) { if (raw === "" || raw.startsWith("diff ") || raw.startsWith("index ")) continue; let line: string; if (raw.startsWith("+++") || raw.startsWith("---")) { line = theme.paint("dim", raw); } else if (raw.startsWith("@@")) { line = theme.paint("dim", raw); } else if (raw.startsWith("+")) { line = theme.tintLine("add", theme.paint("success", "+") + highlight(raw.slice(1), lang, theme)); } else if (raw.startsWith("-")) { line = theme.tintLine("del", theme.paint("error", "-") + highlight(raw.slice(1), lang, theme)); } else { line = theme.paint("dim", raw); } out.push(truncateAnsi(" " + line, width)); } out.push(theme.paint("dim", "─".repeat(Math.max(10, Math.min(width - 1, 60))))); return out; }