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
↓ nina-85cb9c4a.tar.gzatlas at this save
files changed4 files
src/commands.zig+6 −3src/exec.zig+25 −0src/main.zig+15 −8src/output.zig+42 −10src/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 {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;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 {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 "";