zfin/src/stderr.zig

93 lines
3.7 KiB
Zig

//! 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();
}