123const std = @import("std");4const types = @import("types.zig");5const nina_version = @import("version.zig");6const errors = @import("errors.zig");7const Io = std.Io;8910const PINK = "\x1b[38;2;255;183;197m";11const BOLD = "\x1b[1m";12const GREEN = "\x1b[32m";13const YELLOW = "\x1b[33m";14const RED = "\x1b[31m";15const CYAN = "\x1b[36m";16const DIM = "\x1b[2m";17const RESET = "\x1b[0m";1819202122const KAO_FACE = "(˶ᵔ ᵕ ᵔ˶) "; 23const KAO_COZY = "(っ˘ω˘ς) "; 24const KAO_EXCITED = "╰(*°▽°*)╯ "; 25const KAO_GENTLE = "( ˘ᵕ˘ ) "; 26const KAO_CONTENT = "( ´ ∀ ` ) "; 27const KAO_HAPPY = "( ´ ▽ ` ) "; 28const KAO_SLEEPY = "(˘ω˘ ) "; 29const KAO_READY = "( •̀ᴗ•́ ) "; 30const KAO_SAD = "(;ω;) "; 313233pub const C_PINK = PINK;34pub const C_BOLD = BOLD;35pub const C_GREEN = GREEN;36pub const C_YELLOW = YELLOW;37pub const C_RED = RED;38pub const C_CYAN = CYAN;39pub const C_DIM = DIM;40pub const C_RESET = RESET;41pub const C_CLEAR_EOL = "\x1b[K";42pub const C_CURSOR_UP_FMT = "\x1b[{d}A";434445const HDR_C = PINK ++ ":: " ++ RESET;46const HDR_P = ":: ";47const STEP_C = " " ++ DIM ++ "-> " ++ RESET;48const STEP_P = " -> ";49const TEACH_C = " " ++ DIM ++ "= " ++ RESET;50const TEACH_P = " = ";51const ERR_C = PINK ++ ":: " ++ RED ++ "error:" ++ RESET ++ " ";52const ERR_P = ":: error: ";53const WARN_C = PINK ++ ":: " ++ YELLOW ++ "warning:" ++ RESET ++ " ";54const WARN_P = ":: warning: ";5556var _fw: Io.File.Writer = undefined;57var _w: *Io.Writer = undefined;58var color_enabled: bool = true;59var teach_enabled: bool = false;60616263pub const BuildPanel = struct {64 total: u32 = 0,65 completed: u32 = 0,66 total_mb: f32 = 0,67 active: [32]u8 = [_]u8{0} ** 32,68 active_len: usize = 0,69 mode: enum { fetching, building, activating } = .fetching,70 pulse_pos: u8 = 0,7172 const BAR_WIDTH = 28;7374 pub fn update(self: *BuildPanel, line: []const u8) void {75 76 if (std.mem.indexOf(u8, line, "paths will be fetched") != null) {77 if (parseAfterWord(line, "these ")) |n_str| {78 if (std.fmt.parseInt(u32, n_str, 10) catch null) |n| self.total = n;79 }80 81 if (std.mem.indexOfScalar(u8, line, '(')) |pi| {82 const after = std.mem.trim(u8, line[pi + 1 ..], " \t");83 if (indexOfSpace(after)) |sp| {84 if (std.fmt.parseFloat(f32, after[0..sp]) catch null) |mb| self.total_mb = mb;85 }86 }87 return;88 }8990 91 if (std.mem.indexOf(u8, line, "fetching path '") != null) {92 self.completed +|= 1;93 self.mode = .fetching;94 self.setActiveName(line, false);95 return;96 }9798 99 if (std.mem.indexOf(u8, line, "building '") != null and100 std.mem.indexOf(u8, line, ".drv") != null)101 {102 self.mode = .building;103 self.setActiveName(line, true);104 return;105 }106107 108 if (std.mem.indexOf(u8, line, "activating the configuration") != null or109 std.mem.indexOf(u8, line, "activating configuration") != null)110 {111 self.mode = .activating;112 if (self.total > 0) self.completed = self.total;113 const msg = "activating configuration...";114 const copy_len = @min(msg.len, self.active.len);115 @memcpy(self.active[0..copy_len], msg[0..copy_len]);116 self.active_len = copy_len;117 return;118 }119 }120121 pub fn render(self: *BuildPanel) void {122 if (!color_enabled) return;123124 125 p("\x1b[2A", .{});126127 128 p("\r\x1b[K", .{});129 const name = self.active[0..self.active_len];130 switch (self.mode) {131 .fetching => p(" {s}fetching{s} {s}{s}{s}", .{ c(DIM), c(RESET), c(CYAN), name, c(RESET) }),132 .building => p(" {s}building{s} {s}{s}{s}", .{ c(DIM), c(RESET), c(YELLOW), name, c(RESET) }),133 .activating => p(" {s}{s}{s}", .{ c(GREEN), name, c(RESET) }),134 }135 p("\n", .{});136137 138 p("\r\x1b[K", .{});139 if (self.total == 0) {140 self.drawPulse();141 } else {142 self.drawCountedBar();143 }144 p("\n", .{});145146 flush();147 }148149 fn drawCountedBar(self: *BuildPanel) void {150 const filled: u32 = if (self.total > 0)151 @min(BAR_WIDTH, (self.completed * BAR_WIDTH) / self.total)152 else153 0;154 const empty: u32 = BAR_WIDTH - filled;155156 const blk_color = switch (self.mode) {157 .fetching => CYAN,158 .building => YELLOW,159 .activating => GREEN,160 };161162 p(" ", .{});163 if (filled > 0) {164 p("{s}", .{c(blk_color)});165 for (0..filled) |_| p("█", .{});166 p("{s}", .{c(RESET)});167 }168 if (empty > 0) {169 p("{s}", .{c(DIM)});170 for (0..empty) |_| p("░", .{});171 p("{s}", .{c(RESET)});172 }173 p(" {s}{d} / {d} paths{s}", .{ c(DIM), self.completed, self.total, c(RESET) });174 if (self.total_mb > 0) {175 const done_mb = self.total_mb * @as(f32, @floatFromInt(self.completed)) /176 @as(f32, @floatFromInt(@max(self.total, 1)));177 p(" {d:.1} MB / {d:.1} MB", .{ done_mb, self.total_mb });178 }179 }180181 fn drawPulse(self: *BuildPanel) void {182 const PULSE_SIZE: u8 = 4;183 const blk_color = switch (self.mode) {184 .fetching => CYAN,185 .building => YELLOW,186 .activating => GREEN,187 };188 p(" ", .{});189 for (0..BAR_WIDTH) |i| {190 const pos: u8 = @intCast(i);191 const in_pulse = pos >= self.pulse_pos and pos < self.pulse_pos + PULSE_SIZE;192 if (in_pulse) {193 p("{s}█{s}", .{ c(blk_color), c(RESET) });194 } else {195 p("{s}░{s}", .{ c(DIM), c(RESET) });196 }197 }198 self.pulse_pos = (self.pulse_pos + 1) % BAR_WIDTH;199 }200201 fn setActiveName(self: *BuildPanel, line: []const u8, is_drv: bool) void {202 const store = "/nix/store/";203 const idx = std.mem.indexOf(u8, line, store) orelse return;204 const after = line[idx + store.len ..];205 if (after.len < 33) return;206 var name = after[33..]; 207 if (is_drv) {208 if (std.mem.endsWith(u8, name, ".drv")) name = name[0 .. name.len - 4];209 }210 211 name = std.mem.trimEnd(u8, name, "' \t\r\n");212 const copy_len = @min(name.len, self.active.len);213 @memcpy(self.active[0..copy_len], name[0..copy_len]);214 self.active_len = copy_len;215 }216};217218fn parseAfterWord(line: []const u8, word: []const u8) ?[]const u8 {219 const idx = std.mem.indexOf(u8, line, word) orelse return null;220 const after = line[idx + word.len ..];221 const end = indexOfSpace(after) orelse return null;222 return after[0..end];223}224225fn indexOfSpace(s: []const u8) ?usize {226 for (s, 0..) |b, i| if (b == ' ' or b == '\t') return i;227 return null;228}229230pub fn buildPanelInit() void {231 if (!color_enabled) return;232 p("\n\n\n", .{});233 flush();234}235236pub fn buildPanelClear() void {237 if (!color_enabled) return;238 p("\n", .{});239 flush();240}241242pub fn init(writer: *Io.Writer, io: std.Io, environ: *const std.process.Environ.Map, color_config: bool) void {243 _w = writer;244 color_enabled = color_config and245 (std.Io.File.stdout().isTty(io) catch false) and246 !environ.contains("NO_COLOR");247}248249pub fn setTeach(enabled: bool) void {250 teach_enabled = enabled;251}252253pub fn flush() void {254 _w.flush() catch {};255}256257258259pub fn colorEnabled() bool {260 return color_enabled;261}262263fn c(code: []const u8) []const u8 {264 return if (color_enabled) code else "";265}266267fn hdr() []const u8 {268 return if (color_enabled) HDR_C else HDR_P;269}270271fn step() []const u8 {272 return if (color_enabled) STEP_C else STEP_P;273}274275fn teach_pfx() []const u8 {276 return if (color_enabled) TEACH_C else TEACH_P;277}278279280pub fn raw(bytes: []const u8) void {281 _w.writeAll(bytes) catch {};282}283284285pub fn p(comptime fmt: []const u8, args: anytype) void {286 _w.print(fmt, args) catch {};287}288289pub fn cursorUp(n: usize) void {290 if (n > 0) p("\x1b[{d}A", .{n});291}292293pub fn clearLine() void {294 p("\r\x1b[K", .{});295}296297298299pub fn printError(message: []const u8, detail: ?[]const u8, suggestion: ?[]const u8) void {300 p("{s}{s}\n", .{ if (color_enabled) ERR_C else ERR_P, message });301 if (detail) |d| p(" {s}{s}{s}\n", .{ c(DIM), d, c(RESET) });302 303 if (suggestion) |s| p(" {s}-> {s}{s}\n", .{ c(CYAN), s, c(RESET) });304}305306307308309pub fn printSystemError(message: []const u8, detail: ?[]const u8, suggestion: ?[]const u8) void {310 p("{s}{s}{s}\n", .{ KAO_SAD, if (color_enabled) ERR_C else ERR_P, message });311 if (detail) |d| p(" {s}{s}{s}\n", .{ c(DIM), d, c(RESET) });312 if (suggestion) |s| p(" {s}-> {s}{s}\n", .{ c(CYAN), s, c(RESET) });313}314315pub fn printWarning(message: []const u8) void {316 p("{s}{s}\n", .{ if (color_enabled) WARN_C else WARN_P, message });317}318319320321322323pub fn buildError(err: errors.TranslatedError) void {324 if (err.is_system) {325 326 p("{s}{s}{s}\n\n", .{ KAO_SAD, if (color_enabled) ERR_C else ERR_P, err.title });327 } else {328 p("{s}{s}\n\n", .{ if (color_enabled) ERR_C else ERR_P, err.title });329 }330331 var body_lines = std.mem.splitScalar(u8, err.body, '\n');332 while (body_lines.next()) |line| {333 if (line.len == 0) continue;334 p(" {s}\n", .{line});335 }336337 if (err.location) |loc| {338 p("{s} {s}{s}\n", .{ c(DIM), loc, c(RESET) });339 }340341 p("\n", .{});342343 var sug_lines = std.mem.splitScalar(u8, err.suggestion, '\n');344 var first = true;345 while (sug_lines.next()) |line| {346 if (line.len == 0) continue;347 if (first) {348 p("{s} -> {s}{s}\n", .{ c(CYAN), line, c(RESET) });349 first = false;350 } else {351 p("{s} {s}{s}\n", .{ c(DIM), line, c(RESET) });352 }353 }354}355356357358pub fn buildWarnings(warnings: []const []const u8) void {359 for (warnings) |w| {360 p("{s} \u{26a0} {s}{s}\n", .{ c(YELLOW), w, c(RESET) });361 }362}363364pub fn aborted() void {365 p("{s}aborted\n", .{hdr()});366}367368369pub fn configMigrated(path: []const u8) void {370 p("{s}{s}config migrated to {s}{s}\n", .{ step(), c(DIM), path, c(RESET) });371}372373pub fn printTeach(cmd: []const u8) void {374 if (teach_enabled) p("{s}{s}{s}\n", .{ teach_pfx(), cmd, c(RESET) });375}376377pub fn printSubstep(comptime fmt: []const u8, args: anytype) void {378 p("{s}", .{step()});379 p(fmt, args);380 p("\n", .{});381}382383pub fn hookFailed(name: []const u8, code: u8, hook_output: []const u8) void {384 p("{s}{s} hook exited with code {d}{s}\n\n", .{ hdr(), name, code, c(RESET) });385386 if (hook_output.len > 0) {387 var lines = std.mem.splitScalar(u8, hook_output, '\n');388 var shown: [5][]const u8 = undefined;389 var count: usize = 0;390 while (lines.next()) |line| {391 const trimmed = std.mem.trim(u8, line, " \t\r");392 if (trimmed.len == 0) continue;393 shown[count % shown.len] = trimmed;394 count += 1;395 }396397 const display_count = @min(count, shown.len);398 const start = count - display_count;399 for (0..display_count) |i| {400 const line = shown[(start + i) % shown.len];401 p(" {s}{s}{s}\n", .{ c(DIM), line, c(RESET) });402 }403 p("\n", .{});404 }405}406407pub fn hookWarning(name: []const u8, code: u8, hook_output: []const u8) void {408 _ = hook_output;409 p("{s} warning: {s} hook exited with code {d}{s}\n", .{ c(YELLOW), name, code, c(RESET) });410}411412413414pub fn confirm(prompt: []const u8) bool {415 p(" {s}continue? [Y/n] ", .{prompt});416 flush();417 var buf: [64]u8 = undefined;418 var i: usize = 0;419 while (i < buf.len) {420 const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;421 if (n == 0) break;422 if (buf[i] == '\n') break;423 i += 1;424 }425 const input = std.mem.trim(u8, buf[0..i], " \t");426 if (input.len == 0) return true;427 return input[0] != 'n' and input[0] != 'N';428}429430431432pub fn confirmDefaultNo(question: []const u8) bool {433 p(" {s} [y/N] ", .{question});434 flush();435 var buf: [64]u8 = undefined;436 var i: usize = 0;437 while (i < buf.len) {438 const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;439 if (n == 0) break;440 if (buf[i] == '\n') break;441 i += 1;442 }443 const input = std.mem.trim(u8, buf[0..i], " \t");444 if (input.len == 0) return false;445 return input[0] == 'y' or input[0] == 'Y';446}447448449450pub fn applyStart(machine: []const u8) void {451 p("{s}{s}rebuilding {s}...\n", .{ KAO_COZY, hdr(), machine });452}453454455456457pub fn applyDone(gen: u32, elapsed_ms: u64, after_failure: bool) void {458 const face = if (after_failure) KAO_EXCITED else "";459 const secs = @as(f64, @floatFromInt(elapsed_ms)) / 1000.0;460 if (elapsed_ms >= 1000) {461 p("{s}{s}generation {s}{d}{s} {s}[{d:.1}s]{s}\n", .{ face, hdr(), c(BOLD), gen, c(RESET), c(DIM), secs, c(RESET) });462 } else {463 p("{s}{s}generation {s}{d}{s}\n", .{ face, hdr(), c(BOLD), gen, c(RESET) });464 }465}466467pub fn applyFailed(translated: []const u8) void {468 p("{s}build failed\n", .{if (color_enabled) ERR_C else ERR_P});469 if (translated.len > 0) p(" {s}{s}{s}\n", .{ c(DIM), translated, c(RESET) });470 p(" {s}-> nina doctor{s}\n", .{ c(DIM), c(RESET) });471}472473pub fn applyDry(machine: []const u8) void {474 p("{s}dry run {s}\n", .{ hdr(), machine });475}476477pub fn applyDryDone() void {478 p("{s}no changes applied\n", .{hdr()});479}480481482483pub fn checkStart(machine: []const u8) void {484 p("{s}checking {s}...\n", .{ hdr(), machine });485}486487pub fn checkOk() void {488 p("{s}{s}ok{s}\n", .{ hdr(), c(GREEN), c(RESET) });489}490491pub fn checkFailed(msg: []const u8) void {492 p("{s}config invalid\n", .{if (color_enabled) ERR_C else ERR_P});493 if (msg.len > 0) p(" {s}{s}{s}\n", .{ c(DIM), msg, c(RESET) });494}495496497498pub fn backStart(machine: []const u8, from: u32, to: u32) void {499 p("{s}rolling back {s} gen {d} -> gen {d}\n", .{ hdr(), machine, from, to });500}501502pub fn goStart(machine: []const u8, current: u32, target: u32) void {503 p("{s}switching {s} gen {d} -> gen {d}\n", .{ hdr(), machine, current, target });504}505506pub fn generationDone(n: u32) void {507 p("{s}done gen {s}{d}{s}\n", .{ hdr(), c(BOLD), n, c(RESET) });508}509510511512513pub fn alreadyOldest() void {514 p("{s}already at the oldest generation\n", .{if (color_enabled) ERR_C else ERR_P});515}516517518519pub fn backDone(n: u32) void {520 p("{s}{s}done gen {s}{d}{s}\n", .{ KAO_GENTLE, hdr(), c(BOLD), n, c(RESET) });521}522523524525pub fn historyHeader(machine: []const u8) void {526 p("{s}generations {s}\n\n", .{ hdr(), machine });527}528529pub fn historyRow(gen: types.GenerationInfo) void {530 if (gen.current) {531 p(" {s}{d}{s} {s} {s} {s}current{s}\n", .{ c(BOLD), gen.number, c(RESET), gen.date, gen.time, c(BOLD), c(RESET) });532 } else {533 p(" {s}{d} {s} {s}{s}\n", .{ c(DIM), gen.number, gen.date, gen.time, c(RESET) });534 }535}536537538539pub fn cleanStart(machine: []const u8, keep: u32, total: u32) void {540 p("{s}cleaning {s} keeping {d} of {d}\n", .{ hdr(), machine, keep, total });541}542543544545pub fn cleanNothing(machine: []const u8, total: u32) void {546 p("{s}cleaning {s}\n\n", .{ hdr(), machine });547 p(" only {d} generation{s} — nothing to remove.\n", .{ total, if (total == 1) "" else "s" });548}549550pub fn cleanDone() void {551 p("{s}{s}done\n", .{ KAO_CONTENT, hdr() });552}553554555556pub fn statusHeader() void {557 p("{s}status\n\n", .{hdr()});558}559560pub fn statusRow(machine: []const u8, gen: u32, time: []const u8, size: []const u8, ok: bool) void {561 if (ok) {562 p(" {s}{s}{s} gen {s}{d}{s} {s}{s} {s}{s}\n", .{ c(DIM), machine, c(RESET), c(BOLD), gen, c(RESET), c(DIM), time, size, c(RESET) });563 } else {564 p(" {s}{s} {s}unreachable{s}\n", .{ c(RED), machine, "", c(RESET) });565 }566}567568569570pub fn statusHomeRow(gen: u32, detail: []const u8) void {571 if (detail.len > 0) {572 p(" {s} hm gen {d} {s}{s}\n", .{ c(DIM), gen, detail, c(RESET) });573 } else {574 p(" {s} hm gen {d}{s}\n", .{ c(DIM), gen, c(RESET) });575 }576}577578579580pub fn diffHeader(a: u32, b: u32, machine: []const u8) void {581 p("{s}gen {d} -> gen {d} {s}\n\n", .{ hdr(), a, b, machine });582}583584pub fn diffRow(entry: types.DiffEntry) void {585 switch (entry.op) {586 .add => p(" {s}+{s} {s} {s}\n", .{ c(GREEN), c(RESET), entry.package, entry.new_version orelse "" }),587 .remove => p(" {s}-{s} {s} {s}\n", .{ c(RED), c(RESET), entry.package, entry.old_version orelse "" }),588 .change => p(" {s}^{s} {s} {s} -> {s}\n", .{ c(CYAN), c(RESET), entry.package, entry.old_version orelse "", entry.new_version orelse "" }),589 }590}591592593594pub fn doctorHeader(machine: []const u8) void {595 p("{s}diagnosing {s}\n\n", .{ hdr(), machine });596}597598pub fn doctorRow(check: types.DoctorCheck) void {599 const status_str = switch (check.status) {600 .ok => if (color_enabled) GREEN ++ "ok" ++ RESET else "ok",601 .warn => if (color_enabled) YELLOW ++ "warn" ++ RESET else "warn",602 .fail => if (color_enabled) RED ++ "fail" ++ RESET else "fail",603 };604 const note_str = check.note orelse "";605 p(" {s:<16} {s} {s}\n", .{ check.name, status_str, note_str });606}607608pub fn doctorSummary(warnings: u32, failures: u32) void {609 if (failures > 0) {610 p("{s}{d} problems\n", .{ hdr(), failures });611 } else if (warnings > 0) {612 p("{s}{d} warnings\n", .{ hdr(), warnings });613 } else {614 p("{s}{s}{s}all good{s}\n", .{ KAO_HAPPY, hdr(), c(GREEN), c(RESET) });615 }616}617618619620pub fn updateStart(machine: []const u8) void {621 p("{s}updating channels {s}\n\n", .{ hdr(), machine });622}623624pub fn updateChannel(name: []const u8, old: ?[]const u8, new: ?[]const u8) void {625 if (old != null and new != null and !std.mem.eql(u8, old.?, new.?)) {626 p(" {s} {s} -> {s}\n", .{ name, old.?, new.? });627 } else {628 p(" {s} {s}no change{s}\n", .{ name, c(DIM), c(RESET) });629 }630}631632pub fn updateDone() void {633 p("\n{s}done\n", .{hdr()});634}635636637638pub fn upgradeStart(machine: []const u8) void {639 p("{s}upgrading {s}\n", .{ hdr(), machine });640}641642643644pub fn logHeader(machine: []const u8) void {645 p("{s}history {s}\n\n", .{ hdr(), machine });646}647648pub fn logRow(date: []const u8, time: []const u8, action: []const u8, detail: []const u8) void {649 p(" {s}{s} {s}{s} {s:<16} {s}\n", .{ c(DIM), date, time, c(RESET), action, detail });650}651652653654pub fn editOpen(path: []const u8, editor: []const u8) void {655 p("{s}opening {s}\n", .{ hdr(), path });656 printSubstep("{s}", .{editor});657 p("\n {s}run nina check to validate before applying{s}\n", .{ c(DIM), c(RESET) });658}659660661662pub fn fmtStart(path: []const u8) void {663 p("{s}formatting {s}\n", .{ hdr(), path });664}665666pub fn fmtDone() void {667 p("{s}done\n", .{hdr()});668}669670pub fn fmtNeedsFormat() void {671 p("{s}needs formatting {s}run nina fmt to fix{s}\n", .{ hdr(), c(DIM), c(RESET) });672}673674675676pub fn replStart() void {677 p("{s}nix repl {s}nixpkgs loaded{s} :q to exit\n", .{ hdr(), c(DIM), c(RESET) });678}679680681682pub fn developStart(pkgs: []const []const u8) void {683 p("{s}entering dev shell\n\n ", .{hdr()});684 for (pkgs, 0..) |pkg, i| {685 if (i > 0) p(" ", .{});686 p("{s}", .{pkg});687 }688 p("\n\n {s}-> press Ctrl+C or type 'exit' to return{s}\n\n", .{ c(DIM), c(RESET) });689}690691pub fn developDone() void {692 p("{s}{s}back\n", .{ KAO_SLEEPY, hdr() });693}694695696697pub fn infoHeader(machine: []const u8) void {698 p("{s}{s}\n\n", .{ hdr(), machine });699}700701pub fn infoRow(key: []const u8, val: []const u8) void {702 p(" {s:<12} {s}\n", .{ key, val });703}704705706707pub fn bootHeader(machine: []const u8) void {708 p("{s}boot entries {s}\n\n", .{ hdr(), machine });709}710711pub fn bootEntry(entry: types.BootEntry, idx: usize) void {712 if (entry.current) {713 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) });714 } else {715 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) });716 }717 _ = idx;718}719720721722pub fn serviceListHeader(machine: []const u8) void {723 p("{s}services {s}\n\n", .{ hdr(), machine });724}725726pub fn serviceRow(svc: types.ServiceInfo) void {727 const state_str = switch (svc.state) {728 .active => if (color_enabled) GREEN ++ "active" ++ RESET else "active",729 .failed => if (color_enabled) RED ++ "failed" ++ RESET else "failed",730 .inactive => "inactive",731 .unknown => "unknown",732 };733 const uptime = svc.uptime orelse "";734 p(" {s:<24} {s:<14} {s}\n", .{ svc.name, state_str, uptime });735}736737pub fn serviceVerb(verb: []const u8, name: []const u8, machine: []const u8) void {738 p("{s}{s} {s} {s}\n", .{ hdr(), verb, name, machine });739}740741pub fn serviceDone() void {742 p("{s}done\n", .{hdr()});743}744745pub fn serviceLogsHeader(name: []const u8, n: u32, machine: []const u8) void {746 p("{s}{s} last {d} lines {s}\n\n", .{ hdr(), name, n, machine });747}748749750751pub fn storeInfo(info: types.StoreInfo) void {752 p("{s}store\n\n", .{hdr()});753 p(" {s:<16} {s}\n", .{ "total", info.total_size });754 p(" {s:<16} {d} paths\n", .{ "live", info.live_paths });755 p(" {s:<16} {s}{d} paths{s} {s}run nina clean{s}\n", .{ "reclaimable", c(DIM), info.reclaimable_paths, c(RESET), c(CYAN), c(RESET) });756}757758759760pub fn channelListHeader(machine: []const u8) void {761 p("{s}channels {s}\n\n", .{ hdr(), machine });762}763764pub fn channelRow(ch: types.ChannelInfo) void {765 p(" {s:<16} {s}\n", .{ ch.name, ch.url });766}767768769770pub fn flakeUpdateStart() void {771 p("{s}updating inputs\n\n", .{hdr()});772}773774pub fn flakeInput(input: types.FlakeInput) void {775 if (input.changed) {776 const old = if (input.old_rev) |r| r[0..@min(7, r.len)] else "?";777 const new = if (input.new_rev) |r| r[0..@min(7, r.len)] else "?";778 p(" {s:<20} {s} -> {s}\n", .{ input.name, old, new });779 } else {780 p(" {s:<20} {s}no change{s}\n", .{ input.name, c(DIM), c(RESET) });781 }782}783784pub fn flakeLockWritten() void {785 p("\n{s}flake.lock written\n", .{hdr()});786}787788pub fn flakeValid() void {789 p("{s}{s}valid{s}\n", .{ hdr(), c(GREEN), c(RESET) });790}791792pub fn flakeCheckStart() void {793 p("{s}checking flake\n", .{hdr()});794}795796797798pub fn profileListHeader() void {799 p("{s}profile packages\n\n", .{hdr()});800}801802pub fn profileRow(attr: []const u8, pkg_version: []const u8, description: []const u8) void {803 p(" {s:<24} {s:<12} {s}\n", .{ attr, pkg_version, description });804}805806807808809810pub fn didYouMean(query: []const u8, suggestion: []const u8) void {811 p("{s}{s}'{s}' not found{s} — did you mean {s}{s}{s}?\n", .{ hdr(), c(DIM), query, c(RESET), c(PINK), suggestion, c(RESET) });812}813814pub fn installStart(pkg: []const u8, path: types.InstallPath) void {815 const path_str = switch (path) {816 .profile => "profile",817 .system => "system",818 };819 p("{s}installing {s}{s}{s} {s}\n", .{ hdr(), c(PINK), pkg, c(RESET), path_str });820}821822pub fn installDone(pkg: []const u8) void {823 p("{s}{s}{s}{s} installed {s}available now{s}\n", .{ hdr(), c(PINK), pkg, c(RESET), c(DIM), c(RESET) });824}825826pub fn removeStart(pkg: []const u8) void {827 p("{s}removing {s}{s}{s}\n", .{ hdr(), c(PINK), pkg, c(RESET) });828}829830831pub fn mood(machine: []const u8, state: []const u8, gen: u32, age: []const u8, size: []const u8) void {832 const healthy = std.mem.eql(u8, state, "running");833 const verdict = if (healthy) "all good" else state;834 const color = if (healthy) GREEN else YELLOW;835 const face = if (healthy) KAO_HAPPY else "";836 p("{s}{s}{s} {s}{s}{s}\n", .{ face, hdr(), machine, c(color), verdict, c(RESET) });837 if (age.len > 0 or size.len > 0) {838 p(" generation {d}, last applied {s}, {s} system closure\n", .{ gen, age, size });839 } else {840 p(" generation {d}\n", .{gen});841 }842}843844pub fn installEditorPrompt(pkg: []const u8, config_path: []const u8, line: u32, editor: []const u8) void {845 p("{s}opening {s} add {s}{s}{s} to systemPackages\n", .{ hdr(), config_path, c(PINK), pkg, c(RESET) });846 const cmd = std.fmt.allocPrint(std.heap.page_allocator, "{s} {s} +{d}", .{ editor, config_path, line }) catch editor;847 printSubstep("{s}", .{cmd});848 p("\n {s}save and close when done{s}\n\n", .{ c(DIM), c(RESET) });849}850851pub fn installSelectPrompt(pkg: []const u8, pkg_version: []const u8) void {852 p("{s}{s}{s}{s} {s}\n\n", .{ hdr(), c(PINK), pkg, c(RESET), pkg_version });853 p(" [i] profile install {s}instant no rebuild{s}\n", .{ c(DIM), c(RESET) });854 p(" [s] system install {s}opens editor requires apply{s}\n", .{ c(DIM), c(RESET) });855 p(" [t] try now {s}exits when done{s}\n\n", .{ c(DIM), c(RESET) });856 p(" > ", .{});857 flush();858}859860pub fn tryStart(pkg: []const u8) void {861 p("{s}trying {s}{s}{s} exit when done\n", .{ hdr(), c(PINK), pkg, c(RESET) });862}863864pub fn tryExitHint() void {865 p(" {s}-> press Ctrl+C or type 'exit' to return{s}\n", .{ c(DIM), c(RESET) });866}867868pub fn tryDone() void {869 p("{s}{s}back\n", .{ KAO_SLEEPY, hdr() });870}871872873874pub fn listHeader(machine: []const u8) void {875 p("{s}packages {s}\n\n", .{ hdr(), machine });876}877878pub fn listSection(title: []const u8) void {879 p(" {s}{s}{s}\n", .{ c(DIM), title, c(RESET) });880}881882883pub fn listPkg(name: []const u8, pkg_version: []const u8) void {884 p(" {s}{s:<16}{s} {s}{s}{s}\n", .{ c(PINK), name, c(RESET), c(DIM), pkg_version, c(RESET) });885}886887pub fn blankLine() void {888 p("\n", .{});889}890891892893pub fn hello(machines: []const types.Machine) void {894 p("{s}{s}nina\n\n", .{ KAO_FACE, hdr() });895 for (machines) |m| {896 const kind = if (m.local) "local" else "ssh";897 const detail = if (m.local) "default" else m.host orelse "";898 p(" {s}{s}{s} {s} {s}\n", .{ c(DIM), m.name, c(RESET), kind, detail });899 }900 p("\n", .{});901 const quick = [_][2][]const u8{902 .{ "apply", "rebuild and switch" },903 .{ "search <q>", "find packages" },904 .{ "install <pkg>", "add to profile or system" },905 .{ "status", "machine health" },906 .{ "back", "roll back one generation" },907 .{ "home apply", "apply home config" },908 .{ "home back", "roll back home config" },909 .{ "home history", "list home generations" },910 .{ "home check", "validate home config" },911 .{ "home diff", "compare home generations" },912 .{ "home packages", "list managed packages" },913 };914 for (quick) |cmd| {915 p(" {s:<16} {s}{s}{s}\n", .{ cmd[0], c(DIM), cmd[1], c(RESET) });916 }917 p("\n", .{});918}919920921922pub fn help() void {923 p("{s}nina {s}NixOS Intuitive Navigation Assistant by Asha Software{s}\n\n", .{ hdr(), c(DIM), c(RESET) });924 const cmds = [_][2][]const u8{925 .{ "apply", "rebuild and switch" },926 .{ "back", "roll back one generation" },927 .{ "go <n>", "switch to generation n" },928 .{ "history", "list all generations" },929 .{ "clean", "remove old generations" },930 .{ "search <q>", "find packages" },931 .{ "install <pkg>", "add to profile or system" },932 .{ "remove <pkg>", "remove from profile or system" },933 .{ "try <pkg>", "test without installing" },934 .{ "list", "show installed packages" },935 .{ "edit", "open configuration.nix" },936 .{ "check", "validate config" },937 .{ "diff", "changes between generations" },938 .{ "status", "machine health" },939 .{ "update", "refresh channels" },940 .{ "upgrade", "update and rebuild" },941 .{ "log", "operation history" },942 .{ "doctor", "diagnose issues" },943 .{ "mood", "plain-language health" },944 .{ "develop", "enter dev shell" },945 .{ "flake", "flake management" },946 .{ "build", "build a package" },947 .{ "run", "run without installing" },948 .{ "service", "manage services" },949 .{ "store", "nix store tools" },950 .{ "channel", "manage channels" },951 .{ "profile", "user profile packages" },952 .{ "home", "home manager commands (apply/back/history/edit/check/diff/packages)" },953 .{ "info", "system information" },954 .{ "boot", "boot entries" },955 .{ "fmt", "format configuration.nix" },956 .{ "repl", "nix repl with nixpkgs" },957 .{ "option <q>", "search nixos options" },958 .{ "pkg", "inspect packages" },959 .{ "why <pkg>", "what pulled a package in" },960 .{ "pin", "pin flake inputs" },961 .{ "hash", "compute nix hash" },962 .{ "fetch", "fetch and hash a url" },963 .{ "goodbye", "uninstall nina" },964 .{ "hello", "list machines" },965 .{ "help", "this message" },966 };967 for (cmds) |cmd| {968 p(" {s:<18} {s}{s}{s}\n", .{ cmd[0], c(DIM), cmd[1], c(RESET) });969 }970 p("\n {s}flags: --on <machine> --dry --check --no-apply --last <n> --all --json --version{s}\n", .{ c(DIM), c(RESET) });971}972973974975pub fn subcommandHelp(group: []const u8, rows: []const [2][]const u8, footer: ?[]const u8) void {976 p("{s}{s} subcommands\n\n", .{ hdr(), group });977 for (rows) |row| {978 p(" {s:<16} {s}{s}{s}\n", .{ row[0], c(DIM), row[1], c(RESET) });979 }980 if (footer) |f| p("\n {s}{s}{s}\n", .{ c(DIM), f, c(RESET) });981 p("\n nina {s} help {s}for this message{s}\n", .{ group, c(DIM), c(RESET) });982}983984pub fn version() void {985 p("{s}nina v{s} zig 0.16 zero dependencies\n", .{ hdr(), nina_version.VERSION });986}987988989990pub fn searchResult(pkg: types.NixPackage) void {991 p(" {s}{s}{s} {s}{s}{s} {s}\n", .{ c(PINK), pkg.pname, c(RESET), c(DIM), pkg.version, c(RESET), pkg.description });992}993994pub fn searchHeader(query: []const u8, count: usize) void {995 p("{s}search nixpkgs {s} {s}{d} results{s}\n\n", .{ hdr(), query, c(DIM), count, c(RESET) });996}9979989991000pub fn genCurrent(n: u32) void {1001 p("{d}\n", .{n});1002}10031004pub fn done() void {1005 p("{s}done\n", .{hdr()});1006}10071008pub fn runStart(pkg: []const u8) void {1009 p("{s}running {s}{s}{s}\n", .{ hdr(), c(PINK), pkg, c(RESET) });1010}10111012pub fn buildStart(attr: []const u8) void {1013 p("{s}building {s}\n", .{ hdr(), attr });1014}10151016pub fn buildDone() void {1017 p("{s}done\n", .{hdr()});1018}1019102010211022pub fn updateSelfStart() void {1023 p("{s}{s}checking for updates\n", .{ KAO_READY, hdr() });1024}10251026pub fn updateSelfCurrent(ver: []const u8) void {1027 p("{s}{s}nina {s} is up to date{s}\n", .{ hdr(), c(GREEN), ver, c(RESET) });1028}10291030pub fn updateSelfAvailable(current: []const u8, latest: []const u8) void {1031 p("{s}update available\n", .{hdr()});1032 p("{s}current {s}{s}{s}\n", .{ step(), c(DIM), current, c(RESET) });1033 p("{s}latest {s}{s}{s}{s}\n", .{ step(), c(BOLD), c(GREEN), latest, c(RESET) });1034}10351036fn releaseNotesHeading(line: []const u8) []const u8 {1037 var i: usize = 0;1038 while (i < line.len and line[i] == '#') : (i += 1) {}1039 return std.mem.trim(u8, line[i..], " \t");1040}10411042fn releaseNotesBullet(line: []const u8) ?[]const u8 {1043 if (line.len < 2) return null;1044 const marker = line[0];1045 if ((marker == '-' or marker == '*' or marker == '+') and line[1] == ' ') return line[2..];1046 if (std.ascii.isDigit(marker)) {1047 var i: usize = 1;1048 while (i < line.len and std.ascii.isDigit(line[i])) : (i += 1) {}1049 if (i + 1 < line.len and line[i] == '.' and line[i + 1] == ' ') return line[i + 2 ..];1050 }1051 return null;1052}10531054pub fn updateSelfReleaseNotes(notes: []const u8) void {1055 const trimmed = std.mem.trim(u8, notes, " \t\r\n");1056 if (trimmed.len == 0) return;10571058 p("{s}changelog\n", .{hdr()});1059 var lines = std.mem.splitScalar(u8, trimmed, '\n');1060 while (lines.next()) |raw_line| {1061 const line = if (raw_line.len > 0 and raw_line[raw_line.len - 1] == '\r') raw_line[0 .. raw_line.len - 1] else raw_line;1062 const text = std.mem.trim(u8, line, " \t");1063 if (text.len == 0) {1064 p("\n", .{});1065 continue;1066 }1067 if (text[0] == '#') {1068 const title = releaseNotesHeading(text);1069 if (title.len == 0) continue;1070 p(" {s}{s}{s}{s}\n", .{ c(BOLD), c(CYAN), title, c(RESET) });1071 continue;1072 }1073 if (releaseNotesBullet(text)) |item| {1074 p(" {s}- {s}{s}\n", .{ c(CYAN), item, c(RESET) });1075 continue;1076 }1077 p(" {s}\n", .{text});1078 }1079}10801081pub fn updateSelfDownloading(ver: []const u8, platform: []const u8) void {1082 p("{s}downloading nina {s} for {s}\n", .{ step(), ver, platform });1083}10841085pub fn updateSelfVerifying() void {1086 p("{s}verifying checksum\n", .{step()});1087}10881089pub fn updateSelfInstalling(install_path: []const u8) void {1090 p("{s}installing to {s}\n", .{ step(), install_path });1091}10921093pub fn updateSelfDone(old_ver: []const u8, new_ver: []const u8) void {1094 p("{s}updated {s} {s}→{s} {s}\n", .{ hdr(), old_ver, c(DIM), c(RESET), new_ver });1095}10961097pub fn updateSelfNoPlatform(platform: []const u8) void {1098 p("{s}no pre-built binary for {s}\n", .{ hdr(), platform });1099 p("{s}check kepr.uk/nina/releases for available platforms\n", .{step()});1100}11011102pub fn updateSelfNetworkError() void {1103 p("{s}{s}\n", .{ if (color_enabled) WARN_C else WARN_P, "could not reach kepr.uk — check your connection" });1104}11051106pub fn updateSelfChecksumFailed() void {1107 p("{s}checksum mismatch — download may be corrupt\n", .{if (color_enabled) ERR_C else ERR_P});1108 p("{s}try again or download manually from kepr.uk/nina/releases\n", .{step()});1109}11101111pub fn updateSelfNixManaged(new_ver: []const u8) void {1112 p("{s}upgrading to {s}{s}{s} via nix profile\n", .{ step(), c(BOLD), new_ver, c(RESET) });1113}11141115pub fn updateSelfNixFailed() void {1116 p("{s}nix profile upgrade failed\n", .{if (color_enabled) ERR_C else ERR_P});1117 p("{s}try manually: nix profile upgrade '.*nina.*'\n", .{step()});1118}11191120pub fn updateSelfNotInProfile() void {1121 p("{s}nina is in the nix store but not found as a profile element\n", .{hdr()});1122 p("{s}reinstall via 'nix profile add' to enable self-update\n", .{step()});1123}1124112511261127pub fn goodbyeStart() void {1128 p("{s}uninstalling nina\n", .{hdr()});1129}11301131pub fn goodbyeFile(path: []const u8) void {1132 p("{s}will remove {s}\n", .{ step(), path });1133}11341135pub fn goodbyeNix() void {1136 p("{s}will remove nina from nix profile\n", .{step()});1137 p("{s}command: nix profile remove nina\n", .{step()});1138}11391140pub fn goodbyeDone() void {1141 p("{s}goodbye! see kepr.uk/nina to reinstall\n", .{hdr()});1142}11431144pub fn goodbyeFailed(msg: []const u8) void {1145 p("{s}{s}\n", .{ if (color_enabled) ERR_C else ERR_P, msg });1146}1147114811491150pub fn setupCheck() void {1151 p("{s}checking nix features\n", .{hdr()});1152}11531154pub fn setupFeatures(nix_command: bool, flakes: bool) void {1155 p("{s}nix-command {s}{s}{s}\n", .{1156 step(),1157 if (nix_command) c(GREEN) else "",1158 if (nix_command) "enabled" else "not enabled",1159 if (nix_command) c(RESET) else "",1160 });1161 p("{s}flakes {s}{s}{s}\n", .{1162 step(),1163 if (flakes) c(GREEN) else "",1164 if (flakes) "enabled" else "not enabled",1165 if (flakes) c(RESET) else "",1166 });1167}11681169pub fn setupAllSet() void {1170 p("{s}all set\n", .{hdr()});1171}1172117311741175pub fn pinningRegistry() void {1176 p("{s}pinning nixpkgs for fast local search...\n", .{hdr()});1177 flush();1178}117911801181118211831184pub fn searchPlainHeader(mode_str: []const u8, query: []const u8, count: u64) void {1185 p("{s}search {s} {s} {s}{d} results{s}\n", .{ hdr(), mode_str, query, c(DIM), count, c(RESET) });1186}11871188pub fn searchPlainNoResults() void {1189 p(" no results\n", .{});1190}11911192pub fn searchPlainPackage(pname: []const u8, pkg_version: []const u8, description: []const u8, unfree: bool) void {1193 if (unfree) {1194 p(" {s} {s}{s}{s} {s}unfree{s} {s}\n", .{ pname, c(CYAN), pkg_version, c(RESET), c(YELLOW), c(RESET), description });1195 } else {1196 p(" {s} {s}{s}{s} {s}\n", .{ pname, c(CYAN), pkg_version, c(RESET), description });1197 }1198}11991200pub fn searchPlainOption(name: []const u8, type_str: []const u8) void {1201 p(" {s} {s}{s}{s}\n", .{ name, c(DIM), type_str, c(RESET) });1202}12031204pub fn searchEmptyQuery() void {1205 p("{s}search needs a query {s}e.g. nina search firefox{s}\n", .{ hdr(), c(DIM), c(RESET) });1206}12071208pub fn setupEnabling() void {1209 p("{s}enabling experimental features\n", .{hdr()});1210}12111212pub fn setupDone(path: []const u8) void {1213 p("{s}wrote {s}\n", .{ step(), path });1214 p("{s}done open a new shell to pick up the change\n", .{hdr()});1215}1216121712181219pub fn setupNixosOffer(features_line: []const u8, conf_path: []const u8) void {1220 p("{s}on NixOS these go in your system config\n", .{hdr()});1221 p(" nina can add this line to {s}{s}{s}:\n\n", .{ c(DIM), conf_path, c(RESET) });1222 p(" {s}{s}{s}\n\n", .{ c(CYAN), features_line, c(RESET) });1223}12241225pub fn setupNixosWrote(conf_path: []const u8) void {1226 p("{s}added the line to {s} {s}(backup: {s}.nina-bak){s}\n", .{ step(), conf_path, c(DIM), conf_path, c(RESET) });1227 p("{s}now run {s}sudo nixos-rebuild switch{s} — then nina's ready\n", .{ hdr(), c(BOLD), c(RESET) });1228}12291230pub fn setupNixosManual(features_line: []const u8, conf_path: []const u8) void {1231 p("{s}no problem — add this line inside the {s}{{ }}{s} in {s}:\n\n", .{ hdr(), c(DIM), c(RESET), conf_path });1232 p(" {s}{s}{s}\n\n", .{ c(CYAN), features_line, c(RESET) });1233 p(" then run {s}sudo nixos-rebuild switch{s} — then nina's ready\n", .{ c(BOLD), c(RESET) });1234}12351236pub fn setupNixosNeedsRebuild(conf_path: []const u8) void {1237 p("{s}already declared in {s}\n", .{ hdr(), conf_path });1238 p(" just run {s}sudo nixos-rebuild switch{s} to activate it\n", .{ c(BOLD), c(RESET) });1239}12401241pub fn pinDone(input: []const u8, commit: []const u8) void {1242 p("{s}pinned {s} -> {s}\n", .{ hdr(), input, commit });1243}12441245pub fn unpinDone(input: []const u8) void {1246 p("{s}unpinned {s}\n", .{ hdr(), input });1247}12481249pub fn hashResult(hash: []const u8) void {1250 p("{s}\n", .{hash});1251}12521253pub fn fetchResult(hash: []const u8, url: []const u8) void {1254 _ = url;1255 p("{s}\n", .{hash});1256}12571258pub fn storeRepairStart() void {1259 p("{s}repairing store\n", .{hdr()});1260}12611262pub fn storeVerifyStart() void {1263 p("{s}verifying store\n", .{hdr()});1264}12651266pub fn storeGcDone(freed: []const u8) void {1267 p("{s}freed {s}\n", .{ hdr(), freed });1268}12691270pub fn storePath(path: []const u8) void {1271 p("{s}\n", .{path});1272}12731274pub fn pkgResult(key: []const u8, val: []const u8) void {1275 p(" {s:<16} {s}\n", .{ key, val });1276}12771278pub fn channelDone() void {1279 p("{s}done\n", .{hdr()});1280}12811282pub fn flakeShowHeader(name: []const u8) void {1283 p("{s}{s}\n\n", .{ hdr(), name });1284}12851286pub fn flakeShowEntry(kind: []const u8, name: []const u8, desc: []const u8) void {1287 p(" {s:<12} {s:<24} {s}\n", .{ kind, name, desc });1288}1289129012911292pub fn homeNotInstalled() void {1293 p("{s}home manager not detected {s}nothing to do{s}\n", .{ hdr(), c(DIM), c(RESET) });1294}129512961297pub fn homeModuleManaged() void {1298 p("{s}home manager is managed by your system config\n", .{hdr()});1299 p(" {s}-> run {s}nina apply{s}{s} to rebuild both together{s}\n", .{ c(CYAN), c(BOLD), c(RESET), c(CYAN), c(RESET) });1300}13011302pub fn homeApplyStart(machine: []const u8) void {1303 p("{s}{s}applying home config {s}\n", .{ KAO_COZY, hdr(), machine });1304}13051306pub fn homeApplyDone(gen: u32, elapsed_ms: u64) void {1307 const secs = @as(f64, @floatFromInt(elapsed_ms)) / 1000.0;1308 1309 1310 if (gen == 0) {1311 if (elapsed_ms >= 1000) {1312 p("{s}done {s}[{d:.1}s]{s}\n", .{ hdr(), c(DIM), secs, c(RESET) });1313 } else {1314 p("{s}done\n", .{hdr()});1315 }1316 return;1317 }1318 if (elapsed_ms >= 1000) {1319 p("{s}generation {s}{d}{s} {s}[{d:.1}s]{s}\n", .{ hdr(), c(BOLD), gen, c(RESET), c(DIM), secs, c(RESET) });1320 } else {1321 p("{s}generation {s}{d}{s}\n", .{ hdr(), c(BOLD), gen, c(RESET) });1322 }1323}1324132513261327pub fn homeLocalOnly(machine: []const u8) void {1328 p("{s}home manager commands run on the local machine only\n", .{hdr()});1329 p(" {s}-> '{s}' is remote; run nina home on that machine directly{s}\n", .{ c(DIM), machine, c(RESET) });1330}13311332pub fn homeApplyDry(machine: []const u8) void {1333 p("{s}home dry run {s}\n", .{ hdr(), machine });1334}13351336pub fn homeApplyDryDone() void {1337 p("{s}{s}ok{s} no changes applied\n", .{ hdr(), c(GREEN), c(RESET) });1338}13391340pub fn homeBackStart(machine: []const u8, from: u32, to: u32) void {1341 p("{s}rolling back home config {s} gen {d} -> gen {d}\n", .{ hdr(), machine, from, to });1342}13431344pub fn homeBackDone(n: u32) void {1345 p("{s}{s}done gen {s}{d}{s}\n", .{ KAO_GENTLE, hdr(), c(BOLD), n, c(RESET) });1346}13471348pub fn homeHistoryHeader(machine: []const u8) void {1349 p("{s}home generations {s}\n\n", .{ hdr(), machine });1350}13511352pub fn homeHistoryRow(number: u32, date: []const u8, time: []const u8, current: bool) void {1353 if (current) {1354 p(" {s}{d}{s} {s} {s} {s}current{s}\n", .{ c(BOLD), number, c(RESET), date, time, c(BOLD), c(RESET) });1355 } else {1356 p(" {s}{d} {s} {s}{s}\n", .{ c(DIM), number, date, time, c(RESET) });1357 }1358}13591360pub fn homeEditOpen(path: []const u8, editor: []const u8) void {1361 p("{s}opening {s}\n", .{ hdr(), path });1362 printSubstep("{s}", .{editor});1363}13641365pub fn homeCheckStart(machine: []const u8) void {1366 p("{s}checking home config {s}\n", .{ hdr(), machine });1367}13681369pub fn homeCheckOk() void {1370 p("{s}{s}ok{s}\n", .{ hdr(), c(GREEN), c(RESET) });1371}13721373pub fn homeDiffHeader(a: u32, b: u32, machine: []const u8) void {1374 p("{s}home gen {d} -> gen {d} {s}\n\n", .{ hdr(), a, b, machine });1375}13761377pub fn homeNothingToDiff() void {1378 p("{s}only one home generation — nothing to diff yet\n", .{hdr()});1379}13801381pub fn homePackagesHeader(machine: []const u8) void {1382 p("{s}home packages {s}\n\n", .{ hdr(), machine });1383}1384138513861387pub fn firstRunBanner() void {1388 p("{s}{s}nina \u{2014} first run\n\n", .{ KAO_FACE, hdr() });1389}13901391pub fn firstRunNoConfig() void {1392 p(" no config found. let me get set up.\n\n", .{});1393}13941395139613971398pub fn firstRunPrompt(label: []const u8, default: []const u8, buf: []u8) []const u8 {1399 p(" {s:<14} {s}> {s}", .{ label, c(DIM), c(RESET) });1400 flush();1401 var i: usize = 0;1402 while (i < buf.len - 1) {1403 const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;1404 if (n == 0) break;1405 if (buf[i] == '\n') break;1406 i += 1;1407 }1408 const input = std.mem.trim(u8, buf[0..i], " \t\r");1409 if (input.len == 0) {1410 1411 p("{s}{s}{s}\n", .{ c(DIM), default, c(RESET) });1412 flush();1413 return default;1414 }1415 return input;1416}14171418pub fn firstRunScanning() void {1419 p("\n {s}:: scanning for config...{s}\n", .{ c(DIM), c(RESET) });1420}142114221423pub fn firstRunDetected(path: []const u8, is_flake: bool) void {1424 const kind = if (is_flake) "flake-based system" else "channel-based system";1425 p(" {s}-> found {s}{s} {s}({s}){s}\n\n", .{ c(CYAN), path, c(RESET), c(DIM), kind, c(RESET) });1426}14271428pub fn firstRunNoneFound() void {1429 p(" {s}-> nothing found at /etc/nixos{s}\n\n", .{ c(DIM), c(RESET) });1430}1431143214331434pub fn firstRunPromptDefault(label: []const u8, default: []const u8, detected: bool, buf: []u8) []const u8 {1435 if (default.len > 0) {1436 p(" {s:<14} {s}[{s}]{s} > ", .{ label, c(DIM), default, c(RESET) });1437 if (detected) p("{s}enter to confirm{s} ", .{ c(DIM), c(RESET) });1438 } else {1439 p(" {s:<14} {s}> {s}", .{ label, c(DIM), c(RESET) });1440 }1441 flush();1442 var i: usize = 0;1443 while (i < buf.len - 1) {1444 const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;1445 if (n == 0) break;1446 if (buf[i] == '\n') break;1447 i += 1;1448 }1449 const input = std.mem.trim(u8, buf[0..i], " \t\r");1450 if (input.len == 0) {1451 p("{s}{s}{s}\n", .{ c(DIM), default, c(RESET) });1452 flush();1453 return default;1454 }1455 return input;1456}1457145814591460pub fn flakeChannelNote() void {1461 p("{s}{s}note: flake-based system — use {s}nina flake update{s}{s} to update inputs{s}\n", .{ hdr(), c(DIM), c(CYAN), c(RESET), c(DIM), c(RESET) });1462}14631464pub fn firstRunWritten(path: []const u8) void {1465 p("\n {s}written to {s}{s}\n\n", .{ c(DIM), path, c(RESET) });1466}14671468pub fn firstRunSkipped() void {1469 p("\n {s}skipping setup in non-interactive mode{s}\n", .{ c(DIM), c(RESET) });1470}