fetch_ml/cli/src/core/output.zig
Jeremie Fraeys 2b7319dc2e
refactor(cli): Simplify output system and add terminal utilities
Remove colors dependency from output.zig

Add terminal.zig for TTY detection and terminal width

Update flags.zig with color flag support

Simplify colors.zig to basic ANSI codes

Update main.zig and utils.zig exports
2026-02-23 14:11:59 -05:00

137 lines
4.3 KiB
Zig

const std = @import("std");
const terminal = @import("../utils/terminal.zig");
/// Output mode: JSON for structured data, text for TSV
pub const Mode = enum { json, text };
pub var mode: Mode = .text;
pub fn setMode(m: Mode) void {
mode = m;
}
/// Escape a value for TSV output (replace tabs/newlines with spaces for xargs safety)
fn escapeTSV(val: []const u8) []const u8 {
// For xargs usability, we need single-line output with no tabs/newlines in values
// This returns the same slice if no escaping needed, but we process to ensure safety
// In practice, we just use the value directly since the caller should sanitize
return val;
}
/// Check if value needs TSV escaping
fn needsTSVEscape(val: []const u8) bool {
for (val) |c| {
if (c == '\t' or c == '\n' or c == '\r') return true;
}
return false;
}
/// Print error to stderr
pub fn err(msg: []const u8) void {
std.debug.print("Error: {s}\n", .{msg});
}
/// Print line in current mode (JSON or TSV)
pub fn line(values: []const []const u8) void {
switch (mode) {
.json => {
std.debug.print("{{", .{});
// Assume alternating key-value pairs
var i: usize = 0;
while (i < values.len) : (i += 2) {
if (i > 0) std.debug.print(",", .{});
const key = values[i];
const val = if (i + 1 < values.len) values[i + 1] else "";
std.debug.print("\"{s}\":\"{s}\"", .{ key, val });
}
std.debug.print("}}\n", .{});
},
.text => {
for (values, 0..) |val, i| {
if (i > 0) std.debug.print("\t", .{});
// For TSV/xargs safety: if value contains tabs/newlines, we need to handle it
// Simple approach: print as-is but replace internal tabs with spaces
if (needsTSVEscape(val)) {
for (val) |c| {
if (c == '\t' or c == '\n' or c == '\r') {
std.debug.print(" ", .{});
} else {
std.debug.print("{c}", .{c});
}
}
} else {
std.debug.print("{s}", .{val});
}
}
std.debug.print("\n", .{});
},
}
}
/// Print raw JSON array
pub fn jsonArray(items: []const []const u8) void {
std.debug.print("[", .{});
for (items, 0..) |item, i| {
if (i > 0) std.debug.print(",", .{});
std.debug.print("\"{s}\"", .{item});
}
std.debug.print("]\n", .{});
}
/// Print raw JSON object from key-value pairs
pub fn jsonObject(pairs: []const []const u8) void {
std.debug.print("{{", .{});
var i: usize = 0;
while (i < pairs.len) : (i += 2) {
if (i > 0) std.debug.print(",", .{});
const key = pairs[i];
const val = if (i + 1 < pairs.len) pairs[i + 1] else "";
std.debug.print("\"{s}\":\"{s}\"", .{ key, val });
}
std.debug.print("}}\n", .{});
}
/// Print success response (JSON only)
pub fn success(comptime cmd: []const u8) void {
if (mode == .json) {
std.debug.print("{{\"success\":true,\"command\":\"{s}\"}}\n", .{cmd});
}
}
/// Print success with data
pub fn successData(comptime cmd: []const u8, pairs: []const []const u8) void {
if (mode == .json) {
std.debug.print("{{\"success\":true,\"command\":\"{s}\",\"data\":{{", .{cmd});
var i: usize = 0;
while (i < pairs.len) : (i += 2) {
if (i > 0) std.debug.print(",", .{});
const key = pairs[i];
const val = if (i + 1 < pairs.len) pairs[i + 1] else "";
std.debug.print("\"{s}\":\"{s}\"", .{ key, val });
}
std.debug.print("}}}}\n", .{});
} else {
for (pairs, 0..) |val, i| {
if (i > 0) std.debug.print("\t", .{});
std.debug.print("{s}", .{val});
}
std.debug.print("\n", .{});
}
}
/// Print usage information
pub fn usage(comptime cmd: []const u8, comptime u: []const u8) void {
std.debug.print("Usage: {s} {s}\n", .{ cmd, u });
}
/// Print plain value (text mode only)
pub fn value(v: []const u8) void {
if (mode == .text) {
std.debug.print("{s}\n", .{v});
}
}
/// Get terminal width for formatting
pub fn getTerminalWidth() ?usize {
return terminal.getWidth();
}