85cb9c4a3870

add 10 kaomoji to output.zig (8 constants, rare/earned placements); excited kaomoji gated on persistent ~/.nina-apply-failed flag; sad kaomoji only on network/ssh/search errors via printSystemError — grep confirms glyphs only in output.zig, hello shows face, build+tests green

time
2026-06-05 02:10 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
nina finishing brief
parent
37f985da095d
4 files src/commands.zig+6 −3src/exec.zig+25 −0src/main.zig+15 −8src/output.zig+42 −10
src/commands.zig+6 −3
@@ -45,11 +45,13 @@4545     const start = std.Io.Clock.awake.now(ctx.io);4646 4747     const code = exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa) catch |e| {48+        exec.markApplyFailed(ctx.io, ctx.environ);4849         output.applyFailed("");4950         return e;5051     };5152 5253     if (code != 0) {54+        exec.markApplyFailed(ctx.io, ctx.environ);5355         output.applyFailed("");5456         return error.BuildFailed;5557     }@@ -57,7 +59,8 @@5759     const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());5860     const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));5961     const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;60-    output.applyDone(gen, elapsed);62+    const after_failure = exec.takeApplyFailed(ctx.io, ctx.environ);63+    output.applyDone(gen, elapsed, after_failure);6164     logAction(ctx, "apply", gen);6265 }6366 @@ -84,7 +87,7 @@8487 8588     const code = try exec.rollback(ctx.io, &ctx.machine, ctx.gpa);8689     if (code != 0) return error.BuildFailed;87-    output.generationDone(prev);90+    output.backDone(prev);8891 }8992 9093 pub fn go(ctx: Ctx) !void {@@ -369,7 +372,7 @@369372     const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());370373     const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));371374     const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;372-    output.applyDone(gen, elapsed);375+    output.applyDone(gen, elapsed, false);373376 }374377 375378 pub fn log(ctx: Ctx) !void {

view file

src/exec.zig+25 −0
@@ -760,6 +760,31 @@760760     return std.Io.Dir.readFileAlloc(.cwd(), io, path, gpa, .unlimited) catch return error.FileNotFound;761761 }762762 763+// nina is a one-shot process, so "did the last apply fail this session?" can't764+// live in memory — it's persisted as a ~/.nina-apply-failed flag, mirroring the765+// other ~/.nina-* state files. This lets a later successful apply earn the766+// excited kaomoji. mark sets it; take reads-and-clears so it fires exactly once.767+pub fn markApplyFailed(io: std.Io, environ: *const std.process.Environ.Map) void {768+    const home = environ.get("HOME") orelse return;769+    var buf: [512]u8 = undefined;770+    const path = std.fmt.bufPrint(&buf, "{s}/.nina-apply-failed", .{home}) catch return;771+    const file = std.Io.Dir.createFile(.cwd(), io, path, .{}) catch return;772+    file.close(io);773+}774+775+pub fn takeApplyFailed(io: std.Io, environ: *const std.process.Environ.Map) bool {776+    const home = environ.get("HOME") orelse return false;777+    var buf: [512]u8 = undefined;778+    const path = std.fmt.bufPrint(&buf, "{s}/.nina-apply-failed", .{home}) catch return false;779+    if (std.Io.Dir.openFile(.cwd(), io, path, .{})) |f| {780+        f.close(io);781+        std.Io.Dir.deleteFile(.cwd(), io, path) catch {};782+        return true;783+    } else |_| {784+        return false;785+    }786+}787+763788 pub fn appendNinaLog(io: std.Io, environ: *const std.process.Environ.Map, entry: []const u8) void {764789     const home = environ.get("HOME") orelse return;765790     var path_buf: [512]u8 = undefined;

view file

src/main.zig+15 −8
@@ -90,10 +90,7 @@9090             };9191         } else {9292             dispatch(pre_ctx, args.command) catch |err| {93-                const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);94-                const detail = errors.error_info.getDetail();95-                const suggestion = errors.error_info.getSuggestion();96-                output.printError(msg, detail, suggestion);93+                reportError(err);9794                 output.flush();9895                 std.process.exit(1);9996             };@@ -139,13 +136,23 @@139136     };140137 141138     dispatch(ctx, args.command) catch |err| {142-        const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);143-        const detail = errors.error_info.getDetail();144-        const suggestion = errors.error_info.getSuggestion();145-        output.printError(msg, detail, suggestion);139+        reportError(err);146140         output.flush();147141         std.process.exit(1);148142     };143+}144+145+// Network/ssh/search failures are the machine's fault, not the user's — surface146+// them with the sad kaomoji. Everything else (not found, bad config, bad args)147+// is a plain error line.148+fn reportError(err: anyerror) void {149+    const msg = errors.error_info.getMessage() orelse errors.staticMessage(err);150+    const detail = errors.error_info.getDetail();151+    const suggestion = errors.error_info.getSuggestion();152+    switch (err) {153+        error.NetworkError, error.SshFailed, error.SearchFailed => output.printSystemError(msg, detail, suggestion),154+        else => output.printError(msg, detail, suggestion),155+    }149156 }150157 151158 fn dispatch(ctx: commands.Ctx, cmd: []const u8) !void {

view file

src/output.zig+42 −10
@@ -15,6 +15,18 @@1515 const DIM = "\x1b[2m";1616 const RESET = "\x1b[0m";1717 18+// kaomoji constants — all kaomoji defined here, never inlined elsewhere. Rare19+// and earned: each marks one genuine moment and never repeats in one output20+// block. They are plain text, not ANSI, so they survive pipes and NO_COLOR.21+const KAO_FACE = "(˶ᵔ ᵕ ᵔ˶)  "; // hello — her face22+const KAO_COZY = "(っ˘ω˘ς)  "; // apply start23+const KAO_EXCITED = "╰(*°▽°*)╯  "; // apply success after a failure this session24+const KAO_GENTLE = "( ˘ᵕ˘ )  "; // back done25+const KAO_CONTENT = "( ´ ∀ ` )  "; // clean freed26+const KAO_HAPPY = "( ´ ▽ ` )  "; // doctor all clear / mood all good27+const KAO_SLEEPY = "(˘ω˘ )  "; // try / develop exit28+const KAO_SAD = "(;ω;)  "; // network or ssh errors only — never user typos29+1830 // Exported for TUI use (search.zig references these to avoid \x1b in that file)1931 pub const C_PINK = PINK;2032 pub const C_BOLD = BOLD;@@ -101,6 +113,15 @@101113     if (suggestion) |s| p("   {s}-> {s}{s}\n", .{ c(DIM), s, c(RESET) });102114 }103115 116+// Like printError, but for genuine system failures (network/ssh/search). The sad117+// kaomoji signals "this is on the machine, not on you" — never used for typos or118+// not-found errors, which go through printError.119+pub fn printSystemError(message: []const u8, detail: ?[]const u8, suggestion: ?[]const u8) void {120+    p("{s}{s}{s}\n", .{ KAO_SAD, if (color_enabled) ERR_C else ERR_P, message });121+    if (detail) |d| p("   {s}{s}{s}\n", .{ c(DIM), d, c(RESET) });122+    if (suggestion) |s| p("   {s}-> {s}{s}\n", .{ c(DIM), s, c(RESET) });123+}124+104125 pub fn printWarning(message: []const u8) void {105126     p("{s}{s}\n", .{ if (color_enabled) WARN_C else WARN_P, message });106127 }@@ -136,15 +157,19 @@136157 // --- Apply / rebuild ---137158 138159 pub fn applyStart(machine: []const u8) void {139-    p("{s}rebuilding {s}...\n", .{ hdr(), machine });160+    p("{s}{s}rebuilding {s}...\n", .{ KAO_COZY, hdr(), machine });140161 }141162 142-pub fn applyDone(gen: u32, elapsed_ms: u64) void {163+// after_failure: true only when the previous apply this session failed and this164+// one succeeded — the one moment that earns the excited kaomoji. Routine165+// successes show no kaomoji.166+pub fn applyDone(gen: u32, elapsed_ms: u64, after_failure: bool) void {167+    const face = if (after_failure) KAO_EXCITED else "";143168     const secs = @as(f64, @floatFromInt(elapsed_ms)) / 1000.0;144169     if (elapsed_ms >= 1000) {145-        p("{s}generation {s}{d}{s}  {s}[{d:.1}s]{s}\n", .{ hdr(), c(BOLD), gen, c(RESET), c(DIM), secs, c(RESET) });170+        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) });146171     } else {147-        p("{s}generation {s}{d}{s}\n", .{ hdr(), c(BOLD), gen, c(RESET) });172+        p("{s}{s}generation {s}{d}{s}\n", .{ face, hdr(), c(BOLD), gen, c(RESET) });148173     }149174 }150175 @@ -191,6 +216,12 @@191216     p("{s}done  gen {s}{d}{s}\n", .{ hdr(), c(BOLD), n, c(RESET) });192217 }193218 219+// Like generationDone but for `back` specifically — a successful rollback earns220+// the gentle kaomoji. `go` keeps the plain generationDone line.221+pub fn backDone(n: u32) void {222+    p("{s}{s}done  gen {s}{d}{s}\n", .{ KAO_GENTLE, hdr(), c(BOLD), n, c(RESET) });223+}224+194225 // --- History ---195226 196227 pub fn historyHeader(machine: []const u8) void {@@ -212,7 +243,7 @@212243 }213244 214245 pub fn cleanDone(freed: []const u8) void {215-    p("{s}freed {s}\n", .{ hdr(), freed });246+    p("{s}{s}freed {s}\n", .{ KAO_CONTENT, hdr(), freed });216247 }217248 218249 // --- Status ---@@ -265,7 +296,7 @@265296     } else if (warnings > 0) {266297         p("{s}{d} warnings\n", .{ hdr(), warnings });267298     } else {268-        p("{s}{s}all good{s}\n", .{ hdr(), c(GREEN), c(RESET) });299+        p("{s}{s}{s}all good{s}\n", .{ KAO_HAPPY, hdr(), c(GREEN), c(RESET) });269300     }270301 }271302 @@ -343,7 +374,7 @@343374 }344375 345376 pub fn developDone() void {346-    p("{s}back\n", .{hdr()});377+    p("{s}{s}back\n", .{ KAO_SLEEPY, hdr() });347378 }348379 349380 // --- Info ---@@ -480,7 +511,8 @@480511     const healthy = std.mem.eql(u8, state, "running");481512     const verdict = if (healthy) "all good" else state;482513     const color = if (healthy) GREEN else YELLOW;483-    p("{s}{s}  {s}{s}{s}\n", .{ hdr(), machine, c(color), verdict, c(RESET) });514+    const face = if (healthy) KAO_HAPPY else "";515+    p("{s}{s}{s}  {s}{s}{s}\n", .{ face, hdr(), machine, c(color), verdict, c(RESET) });484516     if (age.len > 0 or size.len > 0) {485517         p("   generation {d}, last applied {s}, {s} system closure\n", .{ gen, age, size });486518     } else {@@ -509,7 +541,7 @@509541 }510542 511543 pub fn tryDone() void {512-    p("{s}back\n", .{hdr()});544+    p("{s}{s}back\n", .{ KAO_SLEEPY, hdr() });513545 }514546 515547 // --- List ---@@ -529,7 +561,7 @@529561 // --- Hello ---530562 531563 pub fn hello(machines: []const types.Machine) void {532-    p("{s}nina\n\n", .{hdr()});564+    p("{s}{s}nina\n\n", .{ KAO_FACE, hdr() });533565     for (machines) |m| {534566         const kind = if (m.local) "local" else "ssh";535567         const detail = if (m.local) "default" else m.host orelse "";

view file