// output.zig — the only file that writes to stdout // All ANSI codes live here. No raw escape sequences elsewhere. const std = @import("std"); const types = @import("types.zig"); const Io = std.Io; // Color constants — all ANSI codes live here only const PINK = "\x1b[38;2;255;183;197m"; const BOLD = "\x1b[1m"; const GREEN = "\x1b[32m"; const YELLOW = "\x1b[33m"; const RED = "\x1b[31m"; const CYAN = "\x1b[36m"; const DIM = "\x1b[2m"; const RESET = "\x1b[0m"; // Exported for TUI use (search.zig references these to avoid \x1b in that file) pub const C_PINK = PINK; pub const C_BOLD = BOLD; pub const C_GREEN = GREEN; pub const C_YELLOW = YELLOW; pub const C_RED = RED; pub const C_CYAN = CYAN; pub const C_DIM = DIM; pub const C_RESET = RESET; pub const C_CLEAR_EOL = "\x1b[K"; pub const C_CURSOR_UP_FMT = "\x1b[{d}A"; // Precomputed prefix strings const HDR_C = PINK ++ ":: " ++ RESET; const HDR_P = ":: "; const STEP_C = " " ++ DIM ++ "-> " ++ RESET; const STEP_P = " -> "; const TEACH_C = " " ++ DIM ++ "= " ++ RESET; const TEACH_P = " = "; const ERR_C = PINK ++ ":: " ++ RED ++ "error:" ++ RESET ++ " "; const ERR_P = ":: error: "; const WARN_C = PINK ++ ":: " ++ YELLOW ++ "warning:" ++ RESET ++ " "; const WARN_P = ":: warning: "; var _fw: Io.File.Writer = undefined; var _w: *Io.Writer = undefined; var color_enabled: bool = true; var teach_enabled: bool = false; pub fn init(writer: *Io.Writer, environ: *const std.process.Environ.Map, color_config: bool) void { _w = writer; color_enabled = color_config and isStdoutTty() and !environ.contains("NO_COLOR"); } // libc-free tty check: tcgetattr fails with NotATerminal on a non-tty. // Avoiding std.c.isatty keeps the binary cross-compilable without libc. fn isStdoutTty() bool { _ = std.posix.tcgetattr(std.posix.STDOUT_FILENO) catch return false; return true; } pub fn setTeach(enabled: bool) void { teach_enabled = enabled; } pub fn flush() void { _w.flush() catch {}; } fn c(code: []const u8) []const u8 { return if (color_enabled) code else ""; } fn hdr() []const u8 { return if (color_enabled) HDR_C else HDR_P; } fn step() []const u8 { return if (color_enabled) STEP_C else STEP_P; } fn teach_pfx() []const u8 { return if (color_enabled) TEACH_C else TEACH_P; } // Raw write — used by TUI pub fn raw(bytes: []const u8) void { _w.writeAll(bytes) catch {}; } // Formatted print — used by TUI for in-place rendering pub fn p(comptime fmt: []const u8, args: anytype) void { _w.print(fmt, args) catch {}; } pub fn cursorUp(n: usize) void { if (n > 0) p("\x1b[{d}A", .{n}); } pub fn clearLine() void { p("\r\x1b[K", .{}); } // --- Error and warning output --- pub fn printError(message: []const u8, detail: ?[]const u8, suggestion: ?[]const u8) void { p("{s}{s}\n", .{ if (color_enabled) ERR_C else ERR_P, message }); if (detail) |d| p(" {s}{s}{s}\n", .{ c(DIM), d, c(RESET) }); if (suggestion) |s| p(" {s}-> {s}{s}\n", .{ c(DIM), s, c(RESET) }); } pub fn printWarning(message: []const u8) void { p("{s}{s}\n", .{ if (color_enabled) WARN_C else WARN_P, message }); } pub fn printTeach(cmd: []const u8) void { if (teach_enabled) p("{s}{s}{s}\n", .{ teach_pfx(), cmd, c(RESET) }); } pub fn printSubstep(comptime fmt: []const u8, args: anytype) void { p("{s}", .{step()}); p(fmt, args); p("\n", .{}); } // --- Confirmation prompt (returns true = yes) --- pub fn confirm(prompt: []const u8) bool { p(" {s}continue? [Y/n] ", .{prompt}); flush(); var buf: [64]u8 = undefined; var i: usize = 0; while (i < buf.len) { const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break; if (n == 0) break; if (buf[i] == '\n') break; i += 1; } const input = std.mem.trim(u8, buf[0..i], " \t"); if (input.len == 0) return true; return input[0] != 'n' and input[0] != 'N'; } // --- Apply / rebuild --- pub fn applyStart(machine: []const u8) void { p("{s}rebuilding {s}...\n", .{ hdr(), machine }); } pub fn applyDone(gen: u32, elapsed_ms: u64) void { const secs = @as(f64, @floatFromInt(elapsed_ms)) / 1000.0; if (elapsed_ms >= 1000) { p("{s}generation {s}{d}{s} {s}[{d:.1}s]{s}\n", .{ hdr(), c(BOLD), gen, c(RESET), c(DIM), secs, c(RESET) }); } else { p("{s}generation {s}{d}{s}\n", .{ hdr(), c(BOLD), gen, c(RESET) }); } } pub fn applyFailed(translated: []const u8) void { p("{s}build failed\n", .{if (color_enabled) ERR_C else ERR_P}); if (translated.len > 0) p(" {s}{s}{s}\n", .{ c(DIM), translated, c(RESET) }); p(" {s}-> nina doctor{s}\n", .{ c(DIM), c(RESET) }); } pub fn applyDry(machine: []const u8) void { p("{s}dry run {s}\n", .{ hdr(), machine }); } pub fn applyDryDone() void { p("{s}no changes applied\n", .{hdr()}); } // --- Check --- pub fn checkStart(machine: []const u8) void { p("{s}checking {s}...\n", .{ hdr(), machine }); } pub fn checkOk() void { p("{s}{s}ok{s}\n", .{ hdr(), c(GREEN), c(RESET) }); } pub fn checkFailed(msg: []const u8) void { p("{s}config invalid\n", .{if (color_enabled) ERR_C else ERR_P}); if (msg.len > 0) p(" {s}{s}{s}\n", .{ c(DIM), msg, c(RESET) }); } // --- Back / go --- pub fn backStart(machine: []const u8, from: u32, to: u32) void { p("{s}rolling back {s} gen {d} -> gen {d}\n", .{ hdr(), machine, from, to }); } pub fn goStart(machine: []const u8, current: u32, target: u32) void { p("{s}switching {s} gen {d} -> gen {d}\n", .{ hdr(), machine, current, target }); } pub fn generationDone(n: u32) void { p("{s}done gen {s}{d}{s}\n", .{ hdr(), c(BOLD), n, c(RESET) }); } // --- History --- pub fn historyHeader(machine: []const u8) void { p("{s}generations {s}\n\n", .{ hdr(), machine }); } pub fn historyRow(gen: types.GenerationInfo) void { if (gen.current) { p(" {s}{d}{s} {s} {s} {s}current{s}\n", .{ c(BOLD), gen.number, c(RESET), gen.date, gen.time, c(BOLD), c(RESET) }); } else { p(" {s}{d} {s} {s}{s}\n", .{ c(DIM), gen.number, gen.date, gen.time, c(RESET) }); } } // --- Clean --- pub fn cleanStart(machine: []const u8, keep: u32, total: u32) void { p("{s}cleaning {s} keeping {d} of {d}\n", .{ hdr(), machine, keep, total }); } pub fn cleanDone(freed: []const u8) void { p("{s}freed {s}\n", .{ hdr(), freed }); } // --- Status --- pub fn statusHeader() void { p("{s}status\n\n", .{hdr()}); } pub fn statusRow(machine: []const u8, gen: u32, time: []const u8, size: []const u8, ok: bool) void { if (ok) { p(" {s}{s} gen {d} {s} {s}{s}\n", .{ c(DIM), machine, gen, time, size, c(RESET) }); } else { p(" {s}{s} {s}unreachable{s}\n", .{ c(RED), machine, "", c(RESET) }); } } // --- Diff --- pub fn diffHeader(a: u32, b: u32, machine: []const u8) void { p("{s}gen {d} -> gen {d} {s}\n\n", .{ hdr(), a, b, machine }); } pub fn diffRow(entry: types.DiffEntry) void { switch (entry.op) { .add => p(" {s}+{s} {s} {s}\n", .{ c(GREEN), c(RESET), entry.package, entry.new_version orelse "" }), .remove => p(" {s}-{s} {s} {s}\n", .{ c(RED), c(RESET), entry.package, entry.old_version orelse "" }), .change => p(" {s}^{s} {s} {s} -> {s}\n", .{ c(CYAN), c(RESET), entry.package, entry.old_version orelse "", entry.new_version orelse "" }), } } // --- Doctor --- pub fn doctorHeader(machine: []const u8) void { p("{s}diagnosing {s}\n\n", .{ hdr(), machine }); } pub fn doctorRow(check: types.DoctorCheck) void { const status_str = switch (check.status) { .ok => if (color_enabled) GREEN ++ "ok" ++ RESET else "ok", .warn => if (color_enabled) YELLOW ++ "warn" ++ RESET else "warn", .fail => if (color_enabled) RED ++ "fail" ++ RESET else "fail", }; const note_str = check.note orelse ""; p(" {s:<16} {s} {s}\n", .{ check.name, status_str, note_str }); } pub fn doctorSummary(warnings: u32, failures: u32) void { if (failures > 0) { p("{s}{d} problems\n", .{ hdr(), failures }); } else if (warnings > 0) { p("{s}{d} warnings\n", .{ hdr(), warnings }); } else { p("{s}{s}all good{s}\n", .{ hdr(), c(GREEN), c(RESET) }); } } // --- Update --- pub fn updateStart(machine: []const u8) void { p("{s}updating channels {s}\n\n", .{ hdr(), machine }); } pub fn updateChannel(name: []const u8, old: ?[]const u8, new: ?[]const u8) void { if (old != null and new != null and !std.mem.eql(u8, old.?, new.?)) { p(" {s} {s} -> {s}\n", .{ name, old.?, new.? }); } else { p(" {s} {s}no change{s}\n", .{ name, c(DIM), c(RESET) }); } } pub fn updateDone() void { p("\n{s}done\n", .{hdr()}); } // --- Upgrade --- pub fn upgradeStart(machine: []const u8) void { p("{s}upgrading {s}\n", .{ hdr(), machine }); } // --- Log --- pub fn logHeader(machine: []const u8) void { p("{s}history {s}\n\n", .{ hdr(), machine }); } pub fn logRow(date: []const u8, time: []const u8, action: []const u8, detail: []const u8) void { p(" {s} {s} {s:<12} {s}\n", .{ date, time, action, detail }); } // --- Edit --- pub fn editOpen(path: []const u8, editor: []const u8) void { p("{s}opening {s}\n", .{ hdr(), path }); printSubstep("{s}", .{editor}); p("\n {s}run nina check to validate before applying{s}\n", .{ c(DIM), c(RESET) }); } // --- Fmt --- pub fn fmtStart(path: []const u8) void { p("{s}formatting {s}\n", .{ hdr(), path }); } pub fn fmtDone() void { p("{s}done\n", .{hdr()}); } pub fn fmtNeedsFormat() void { p("{s}needs formatting {s}run nina fmt to fix{s}\n", .{ hdr(), c(DIM), c(RESET) }); } // --- Repl --- pub fn replStart() void { p("{s}nix repl {s}nixpkgs loaded{s} :q to exit\n", .{ hdr(), c(DIM), c(RESET) }); } // --- Develop --- pub fn developStart(pkgs: []const []const u8) void { p("{s}entering dev shell\n\n ", .{hdr()}); for (pkgs, 0..) |pkg, i| { if (i > 0) p(" ", .{}); p("{s}", .{pkg}); } p("\n\n {s}exit to return{s}\n\n", .{ c(DIM), c(RESET) }); } pub fn developDone() void { p("{s}back\n", .{hdr()}); } // --- Info --- pub fn infoHeader(machine: []const u8) void { p("{s}{s}\n\n", .{ hdr(), machine }); } pub fn infoRow(key: []const u8, val: []const u8) void { p(" {s:<12} {s}\n", .{ key, val }); } // --- Boot --- pub fn bootHeader(machine: []const u8) void { p("{s}boot entries {s}\n\n", .{ hdr(), machine }); } pub fn bootEntry(entry: types.BootEntry, idx: usize) void { if (entry.current) { p(" {s}> {s}{s}{s}\n", .{ c(BOLD), entry.title, if (entry.date) |d| std.fmt.allocPrint(std.heap.page_allocator, " {s}", .{d}) catch " " else "", c(RESET) }); } else { p(" {s} {s}{s}{s}\n", .{ c(DIM), entry.title, if (entry.date) |d| std.fmt.allocPrint(std.heap.page_allocator, " {s}", .{d}) catch " " else "", c(RESET) }); } _ = idx; } // --- Service --- pub fn serviceListHeader(machine: []const u8) void { p("{s}services {s}\n\n", .{ hdr(), machine }); } pub fn serviceRow(svc: types.ServiceInfo) void { const state_str = switch (svc.state) { .active => if (color_enabled) GREEN ++ "active" ++ RESET else "active", .failed => if (color_enabled) RED ++ "failed" ++ RESET else "failed", .inactive => "inactive", .unknown => "unknown", }; const uptime = svc.uptime orelse ""; p(" {s:<24} {s:<14} {s}\n", .{ svc.name, state_str, uptime }); } pub fn serviceVerb(verb: []const u8, name: []const u8, machine: []const u8) void { p("{s}{s} {s} {s}\n", .{ hdr(), verb, name, machine }); } pub fn serviceDone() void { p("{s}done\n", .{hdr()}); } pub fn serviceLogsHeader(name: []const u8, n: u32, machine: []const u8) void { p("{s}{s} last {d} lines {s}\n\n", .{ hdr(), name, n, machine }); } // --- Store --- pub fn storeInfo(info: types.StoreInfo) void { p("{s}store\n\n", .{hdr()}); p(" {s:<16} {s}\n", .{ "total", info.total_size }); p(" {s:<16} {d} paths\n", .{ "live", info.live_paths }); p(" {s:<16} {s}{d} paths{s} run nina clean\n", .{ "reclaimable", c(DIM), info.reclaimable_paths, c(RESET) }); } // --- Channel --- pub fn channelListHeader(machine: []const u8) void { p("{s}channels {s}\n\n", .{ hdr(), machine }); } pub fn channelRow(ch: types.ChannelInfo) void { p(" {s:<16} {s}\n", .{ ch.name, ch.url }); } // --- Flake --- pub fn flakeUpdateStart() void { p("{s}updating inputs\n\n", .{hdr()}); } pub fn flakeInput(input: types.FlakeInput) void { if (input.changed) { const old = if (input.old_rev) |r| r[0..@min(7, r.len)] else "?"; const new = if (input.new_rev) |r| r[0..@min(7, r.len)] else "?"; p(" {s:<20} {s} -> {s}\n", .{ input.name, old, new }); } else { p(" {s:<20} {s}no change{s}\n", .{ input.name, c(DIM), c(RESET) }); } } pub fn flakeLockWritten() void { p("\n{s}flake.lock written\n", .{hdr()}); } pub fn flakeValid() void { p("{s}{s}valid{s}\n", .{ hdr(), c(GREEN), c(RESET) }); } pub fn flakeCheckStart() void { p("{s}checking flake\n", .{hdr()}); } // --- Profile --- pub fn profileListHeader() void { p("{s}profile packages\n\n", .{hdr()}); } pub fn profileRow(attr: []const u8, version: []const u8, description: []const u8) void { p(" {s:<24} {s:<12} {s}\n", .{ attr, version, description }); } // --- Install / Remove / Try --- pub fn installStart(pkg: []const u8, path: types.InstallPath) void { const path_str = switch (path) { .profile => "profile", .system => "system", }; p("{s}installing {s}{s}{s} {s}\n", .{ hdr(), c(PINK), pkg, c(RESET), path_str }); } pub fn installDone(pkg: []const u8) void { p("{s}{s}{s}{s} installed {s}available now{s}\n", .{ hdr(), c(PINK), pkg, c(RESET), c(DIM), c(RESET) }); } pub fn installEditorPrompt(pkg: []const u8, config_path: []const u8, line: u32, editor: []const u8) void { p("{s}opening {s} add {s}{s}{s} to systemPackages\n", .{ hdr(), config_path, c(PINK), pkg, c(RESET) }); const cmd = std.fmt.allocPrint(std.heap.page_allocator, "{s} {s} +{d}", .{ editor, config_path, line }) catch editor; printSubstep("{s}", .{cmd}); p("\n {s}save and close when done{s}\n\n", .{ c(DIM), c(RESET) }); } pub fn installSelectPrompt(pkg: []const u8, version: []const u8) void { p("{s}{s}{s}{s} {s}\n\n", .{ hdr(), c(PINK), pkg, c(RESET), version }); p(" [i] profile install {s}instant no rebuild{s}\n", .{ c(DIM), c(RESET) }); p(" [s] system install {s}opens editor requires apply{s}\n", .{ c(DIM), c(RESET) }); p(" [t] try now {s}exits when done{s}\n\n", .{ c(DIM), c(RESET) }); p(" > ", .{}); flush(); } pub fn tryStart(pkg: []const u8) void { p("{s}trying {s}{s}{s} exit when done\n", .{ hdr(), c(PINK), pkg, c(RESET) }); } pub fn tryDone() void { p("{s}back\n", .{hdr()}); } // --- List --- pub fn listHeader() void { p("{s}installed packages\n\n", .{hdr()}); } pub fn listSection(title: []const u8) void { p(" {s}{s}{s}\n", .{ c(DIM), title, c(RESET) }); } pub fn listPkg(name: []const u8, version: []const u8) void { p(" {s}{s}{s} {s}{s}{s}\n", .{ c(PINK), name, c(RESET), c(DIM), version, c(RESET) }); } // --- Hello --- pub fn hello(machines: []const types.Machine) void { p("{s}nina\n\n", .{hdr()}); for (machines) |m| { const kind = if (m.local) "local" else "ssh"; const detail = if (m.local) "default" else m.host orelse ""; p(" {s}{s}{s} {s} {s}\n", .{ c(DIM), m.name, c(RESET), kind, detail }); } p("\n", .{}); } // --- Help --- pub fn help() void { p("{s}nina {s}nixos helper{s}\n\n", .{ hdr(), c(DIM), c(RESET) }); const cmds = [_][2][]const u8{ .{ "apply", "rebuild and switch" }, .{ "back", "roll back one generation" }, .{ "go ", "switch to generation n" }, .{ "history", "list all generations" }, .{ "clean", "remove old generations" }, .{ "search ", "find packages" }, .{ "install ", "add to profile or system" }, .{ "remove ", "remove from profile or system" }, .{ "try ", "test without installing" }, .{ "list", "show installed packages" }, .{ "edit", "open configuration.nix" }, .{ "check", "validate config" }, .{ "diff", "changes between generations" }, .{ "status", "machine health" }, .{ "update", "refresh channels" }, .{ "upgrade", "update and rebuild" }, .{ "log", "operation history" }, .{ "doctor", "diagnose issues" }, .{ "develop", "enter dev shell" }, .{ "flake", "flake management" }, .{ "build", "build a package" }, .{ "run", "run without installing" }, .{ "service", "manage services" }, .{ "store", "nix store tools" }, .{ "channel", "manage channels" }, .{ "profile", "user profile packages" }, .{ "info", "system information" }, .{ "boot", "boot entries" }, .{ "fmt", "format configuration.nix" }, .{ "repl", "nix repl with nixpkgs" }, .{ "option ", "search nixos options" }, .{ "pkg", "inspect packages" }, .{ "pin", "pin flake inputs" }, .{ "hash", "compute nix hash" }, .{ "fetch", "fetch and hash a url" }, .{ "hello", "list machines" }, .{ "help", "this message" }, }; for (cmds) |cmd| { p(" {s:<18} {s}{s}{s}\n", .{ cmd[0], c(DIM), cmd[1], c(RESET) }); } p("\n {s}flags: --on --dry --check --no-apply --last --all --json{s}\n", .{ c(DIM), c(RESET) }); } // --- Search (text output before TUI) --- pub fn searchResult(pkg: types.NixPackage) void { p(" {s}{s}{s} {s}{s}{s} {s}\n", .{ c(PINK), pkg.pname, c(RESET), c(DIM), pkg.version, c(RESET), pkg.description }); } pub fn searchHeader(query: []const u8, count: usize) void { p("{s}search nixpkgs {s} {s}{d} results{s}\n\n", .{ hdr(), query, c(DIM), count, c(RESET) }); } // --- Gen commands --- pub fn genCurrent(n: u32) void { p("{d}\n", .{n}); } pub fn done() void { p("{s}done\n", .{hdr()}); } pub fn runStart(pkg: []const u8) void { p("{s}running {s}{s}{s}\n", .{ hdr(), c(PINK), pkg, c(RESET) }); } pub fn buildStart(attr: []const u8) void { p("{s}building {s}\n", .{ hdr(), attr }); } pub fn buildDone() void { p("{s}done\n", .{hdr()}); } // --- Self-update (nina update) --- pub fn updateSelfStart() void { p("{s}checking for updates\n", .{hdr()}); } pub fn updateSelfCurrent(ver: []const u8) void { p("{s}{s}nina {s} is up to date{s}\n", .{ hdr(), c(GREEN), ver, c(RESET) }); } pub fn updateSelfAvailable(current: []const u8, latest: []const u8) void { p("{s}current {s}\n", .{ step(), current }); p("{s}latest {s}{s}{s}\n", .{ step(), c(BOLD), latest, c(RESET) }); } pub fn updateSelfDownloading(ver: []const u8, platform: []const u8) void { p("{s}downloading nina {s} for {s}\n", .{ step(), ver, platform }); } pub fn updateSelfVerifying() void { p("{s}verifying checksum\n", .{step()}); } pub fn updateSelfInstalling(install_path: []const u8) void { p("{s}installing to {s}\n", .{ step(), install_path }); } pub fn updateSelfDone(old_ver: []const u8, new_ver: []const u8) void { p("{s}updated {s} {s}→{s} {s}\n", .{ hdr(), old_ver, c(DIM), c(RESET), new_ver }); } pub fn updateSelfNoPlatform(platform: []const u8) void { p("{s}no pre-built binary for {s}\n", .{ hdr(), platform }); p("{s}check kepr.uk/nina/releases for available platforms\n", .{step()}); } pub fn updateSelfNetworkError() void { p("{s}{s}\n", .{ if (color_enabled) WARN_C else WARN_P, "could not reach kepr.uk — check your connection" }); } pub fn updateSelfChecksumFailed() void { p("{s}checksum mismatch — download may be corrupt\n", .{if (color_enabled) ERR_C else ERR_P}); p("{s}try again or download manually from kepr.uk/nina/releases\n", .{step()}); } // --- Setup --- pub fn setupCheck() void { p("{s}checking nix features\n", .{hdr()}); } pub fn setupFeatures(nix_command: bool, flakes: bool) void { p("{s}nix-command {s}{s}{s}\n", .{ step(), if (nix_command) c(GREEN) else "", if (nix_command) "enabled" else "not enabled", if (nix_command) c(RESET) else "", }); p("{s}flakes {s}{s}{s}\n", .{ step(), if (flakes) c(GREEN) else "", if (flakes) "enabled" else "not enabled", if (flakes) c(RESET) else "", }); } pub fn setupAllSet() void { p("{s}all set\n", .{hdr()}); } pub fn setupEnabling() void { p("{s}enabling experimental features\n", .{hdr()}); } pub fn setupDone(path: []const u8, is_nixos: bool) void { p("{s}wrote {s}\n", .{ step(), path }); p("{s}done open a new shell to pick up the change\n", .{hdr()}); if (is_nixos) { printSubstep("to persist across nixos-rebuild, add to configuration.nix:", .{}); printSubstep(" nix.settings.experimental-features = [ \"nix-command\" \"flakes\" ];", .{}); } } pub fn pinDone(input: []const u8, commit: []const u8) void { p("{s}pinned {s} -> {s}\n", .{ hdr(), input, commit }); } pub fn unpinDone(input: []const u8) void { p("{s}unpinned {s}\n", .{ hdr(), input }); } pub fn hashResult(hash: []const u8) void { p("{s}\n", .{hash}); } pub fn fetchResult(hash: []const u8, url: []const u8) void { _ = url; p("{s}\n", .{hash}); } pub fn storeRepairStart() void { p("{s}repairing store\n", .{hdr()}); } pub fn storeVerifyStart() void { p("{s}verifying store\n", .{hdr()}); } pub fn storeGcDone(freed: []const u8) void { p("{s}freed {s}\n", .{ hdr(), freed }); } pub fn storePath(path: []const u8) void { p("{s}\n", .{path}); } pub fn pkgResult(key: []const u8, val: []const u8) void { p(" {s:<16} {s}\n", .{ key, val }); } pub fn channelDone() void { p("{s}done\n", .{hdr()}); } pub fn flakeShowHeader(name: []const u8) void { p("{s}{s}\n\n", .{ hdr(), name }); } pub fn flakeShowEntry(kind: []const u8, name: []const u8, desc: []const u8) void { p(" {s:<12} {s:<24} {s}\n", .{ kind, name, desc }); }