//! Best-effort stderr writers. //! //! All three functions (`print`, `progress`, `rateLimitWait`) are //! non-throwing on purpose. A stderr-write failure shouldn't //! propagate as an error to a CLI command's logic — the user's //! command should still complete (or fail for its own reasons), //! not get derailed because we couldn't paint a hint message. //! Secondary failures get logged at debug level for forensics. //! //! Lives at the top level (not under `commands/`) so the portfolio //! loader and the TUI can use it without a "TUI calls into //! commands/" import smell. //! //! Under `zig build test` the writes are suppressed entirely: //! tests that exercise error paths emit the same usage/hint //! strings on every run, and that noise is more annoying than //! useful. Real CLI users always reach the real stderr. const std = @import("std"); const builtin = @import("builtin"); const fmt = @import("format.zig"); /// Default muted-text color used for progress headers. Matches the /// CLI / TUI palette used elsewhere; defined here so this module /// has no dependency on `commands/common.zig`. const CLR_MUTED = [3]u8{ 0x80, 0x80, 0x80 }; /// Default red used for rate-limit warnings. const CLR_NEGATIVE = [3]u8{ 0xe0, 0x6c, 0x75 }; pub fn print(io: std.Io, msg: []const u8) void { if (builtin.is_test) return; var buf: [1024]u8 = undefined; var writer = std.Io.File.stderr().writer(io, &buf); const out = &writer.interface; out.writeAll(msg) catch |err| { std.log.debug("stderr.print writeAll failed: {t}", .{err}); return; }; out.flush() catch |err| { std.log.debug("stderr.print flush failed: {t}", .{err}); }; } /// Print progress line to stderr: " [N/M] SYMBOL (status)". pub fn progress(io: std.Io, symbol: []const u8, status: []const u8, current: usize, total: usize, color: bool) void { if (builtin.is_test) return; progressImpl(io, symbol, status, current, total, color) catch |err| { std.log.debug("stderr.progress failed: {t}", .{err}); }; } fn progressImpl(io: std.Io, symbol: []const u8, status: []const u8, current: usize, total: usize, color: bool) !void { var buf: [256]u8 = undefined; var writer = std.Io.File.stderr().writer(io, &buf); const out = &writer.interface; if (color) try fmt.ansiSetFg(out, CLR_MUTED[0], CLR_MUTED[1], CLR_MUTED[2]); try out.print(" [{d}/{d}] ", .{ current, total }); if (color) try fmt.ansiReset(out); try out.print("{s}", .{symbol}); if (color) try fmt.ansiSetFg(out, CLR_MUTED[0], CLR_MUTED[1], CLR_MUTED[2]); try out.print("{s}\n", .{status}); if (color) try fmt.ansiReset(out); try out.flush(); } /// Print rate-limit wait message to stderr. pub fn rateLimitWait(io: std.Io, wait_seconds: u64, color: bool) void { if (builtin.is_test) return; rateLimitWaitImpl(io, wait_seconds, color) catch |err| { std.log.debug("stderr.rateLimitWait failed: {t}", .{err}); }; } fn rateLimitWaitImpl(io: std.Io, wait_seconds: u64, color: bool) !void { var buf: [256]u8 = undefined; var writer = std.Io.File.stderr().writer(io, &buf); const out = &writer.interface; if (color) try fmt.ansiSetFg(out, CLR_NEGATIVE[0], CLR_NEGATIVE[1], CLR_NEGATIVE[2]); if (wait_seconds >= 60) { const mins = wait_seconds / 60; const secs = wait_seconds % 60; if (secs > 0) { try out.print(" (rate limit -- waiting {d}m {d}s)\n", .{ mins, secs }); } else { try out.print(" (rate limit -- waiting {d}m)\n", .{mins}); } } else { try out.print(" (rate limit -- waiting {d}s)\n", .{wait_seconds}); } if (color) try fmt.ansiReset(out); try out.flush(); }