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
This commit is contained in:
parent
a1988de8b1
commit
2b7319dc2e
6 changed files with 198 additions and 278 deletions
|
|
@ -6,6 +6,7 @@ pub const CommonFlags = struct {
|
|||
help: bool = false,
|
||||
verbose: bool = false,
|
||||
dry_run: bool = false,
|
||||
color: ?bool = null, // null = auto, true = force on, false = disable
|
||||
};
|
||||
|
||||
/// Parse common flags from command arguments
|
||||
|
|
@ -27,6 +28,17 @@ pub fn parseCommon(allocator: std.mem.Allocator, args: []const []const u8, flags
|
|||
flags.verbose = true;
|
||||
} else if (std.mem.eql(u8, arg, "--dry-run")) {
|
||||
flags.dry_run = true;
|
||||
} else if (std.mem.eql(u8, arg, "--no-color") or std.mem.eql(u8, arg, "--no-colour")) {
|
||||
flags.color = false;
|
||||
} else if (std.mem.startsWith(u8, arg, "--color=")) {
|
||||
const val = arg[8..];
|
||||
if (std.mem.eql(u8, val, "always") or std.mem.eql(u8, val, "yes")) {
|
||||
flags.color = true;
|
||||
} else if (std.mem.eql(u8, val, "never") or std.mem.eql(u8, val, "no")) {
|
||||
flags.color = false;
|
||||
} else if (std.mem.eql(u8, val, "auto")) {
|
||||
flags.color = null;
|
||||
}
|
||||
} else if (std.mem.eql(u8, arg, "--")) {
|
||||
// End of flags, rest are positional
|
||||
i += 1;
|
||||
|
|
|
|||
|
|
@ -1,129 +1,137 @@
|
|||
const std = @import("std");
|
||||
const colors = @import("../utils/colors.zig");
|
||||
const terminal = @import("../utils/terminal.zig");
|
||||
|
||||
/// Output mode for commands
|
||||
pub const OutputMode = enum {
|
||||
text,
|
||||
json,
|
||||
};
|
||||
/// Output mode: JSON for structured data, text for TSV
|
||||
pub const Mode = enum { json, text };
|
||||
|
||||
/// Global output mode - set by main based on --json flag
|
||||
pub var global_mode: OutputMode = .text;
|
||||
pub var mode: Mode = .text;
|
||||
|
||||
/// Initialize output mode from command flags
|
||||
pub fn init(mode: OutputMode) void {
|
||||
global_mode = mode;
|
||||
pub fn setMode(m: Mode) void {
|
||||
mode = m;
|
||||
}
|
||||
|
||||
/// Print error in appropriate format
|
||||
pub fn errorMsg(comptime command: []const u8, message: []const u8) void {
|
||||
switch (global_mode) {
|
||||
.json => std.debug.print(
|
||||
"{{\"success\":false,\"command\":\"{s}\",\"error\":\"{s}\"}}\n",
|
||||
.{ command, message },
|
||||
),
|
||||
.text => colors.printError("{s}\n", .{message}),
|
||||
/// 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 with additional details in appropriate format
|
||||
pub fn errorMsgDetailed(comptime command: []const u8, message: []const u8, details: []const u8) void {
|
||||
switch (global_mode) {
|
||||
.json => std.debug.print(
|
||||
"{{\"success\":false,\"command\":\"{s}\",\"error\":\"{s}\",\"details\":\"{s}\"}}\n",
|
||||
.{ command, message, details },
|
||||
),
|
||||
.text => {
|
||||
colors.printError("{s}\n", .{message});
|
||||
std.debug.print("Details: {s}\n", .{details});
|
||||
},
|
||||
}
|
||||
/// Print error to stderr
|
||||
pub fn err(msg: []const u8) void {
|
||||
std.debug.print("Error: {s}\n", .{msg});
|
||||
}
|
||||
|
||||
/// Print success response in appropriate format (no data)
|
||||
pub fn success(comptime command: []const u8) void {
|
||||
switch (global_mode) {
|
||||
.json => std.debug.print("{{\"success\":true,\"command\":\"{s}\"}}\n", .{command}),
|
||||
.text => {}, // No output for text mode on simple success
|
||||
}
|
||||
}
|
||||
|
||||
/// Print success with string data
|
||||
pub fn successString(comptime command: []const u8, comptime data_key: []const u8, value: []const u8) void {
|
||||
switch (global_mode) {
|
||||
.json => std.debug.print(
|
||||
"{{\"success\":true,\"command\":\"{s}\",\"data\":{{\"{s}\":\"{s}\"}}}}\n",
|
||||
.{ command, data_key, value },
|
||||
),
|
||||
.text => std.debug.print("{s}\n", .{value}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print success with formatted string data
|
||||
pub fn successFmt(comptime command: []const u8, comptime fmt_str: []const u8, args: anytype) void {
|
||||
switch (global_mode) {
|
||||
/// Print line in current mode (JSON or TSV)
|
||||
pub fn line(values: []const []const u8) void {
|
||||
switch (mode) {
|
||||
.json => {
|
||||
// Use stack buffer to avoid allocation
|
||||
var buf: [4096]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&buf, fmt_str, args) catch {
|
||||
std.debug.print("{{\"success\":true,\"command\":\"{s}\",\"data\":null}}\n", .{command});
|
||||
return;
|
||||
};
|
||||
std.debug.print("{{\"success\":true,\"command\":\"{s}\",\"data\":{s}}}\n", .{ command, msg });
|
||||
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", .{});
|
||||
},
|
||||
.text => std.debug.print(fmt_str ++ "\n", args),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print informational message (text mode only)
|
||||
pub fn info(comptime fmt_str: []const u8, args: anytype) void {
|
||||
if (global_mode == .text) {
|
||||
std.debug.print(fmt_str ++ "\n", args);
|
||||
/// 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 usage_str: []const u8) void {
|
||||
switch (global_mode) {
|
||||
.json => std.debug.print(
|
||||
"{{\"success\":false,\"command\":\"{s}\",\"error\":\"Invalid arguments\",\"usage\":\"{s}\"}}\n",
|
||||
.{ cmd, usage_str },
|
||||
),
|
||||
.text => {
|
||||
std.debug.print("Usage: {s}\n", .{usage_str});
|
||||
},
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
/// Print unknown command error
|
||||
pub fn unknownCommand(comptime command: []const u8, unknown: []const u8) void {
|
||||
switch (global_mode) {
|
||||
.json => std.debug.print(
|
||||
"{{\"success\":false,\"command\":\"{s}\",\"error\":\"Unknown command: {s}\"}}\n",
|
||||
.{ command, unknown },
|
||||
),
|
||||
.text => colors.printError("Unknown command: {s}\n", .{unknown}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print table header (text mode only)
|
||||
pub fn tableHeader(comptime cols: []const []const u8) void {
|
||||
if (global_mode == .json) return;
|
||||
|
||||
for (cols, 0..) |col, i| {
|
||||
if (i > 0) std.debug.print("\t", .{});
|
||||
std.debug.print("{s}", .{col});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
}
|
||||
|
||||
/// Print table row (text mode only)
|
||||
pub fn tableRow(values: []const []const u8) void {
|
||||
if (global_mode == .json) return;
|
||||
|
||||
for (values, 0..) |val, i| {
|
||||
if (i > 0) std.debug.print("\t", .{});
|
||||
std.debug.print("{s}", .{val});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
/// Get terminal width for formatting
|
||||
pub fn getTerminalWidth() ?usize {
|
||||
return terminal.getWidth();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
const std = @import("std");
|
||||
const colors = @import("utils/colors.zig");
|
||||
|
||||
// Handle unknown command - prints error and exits
|
||||
fn handleUnknownCommand(cmd: []const u8) noreturn {
|
||||
colors.printError("Unknown command: {s}\n", .{cmd});
|
||||
std.debug.print("Error: Unknown command: {s}\n", .{cmd});
|
||||
printUsage();
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
// Initialize colors based on environment
|
||||
colors.initColors();
|
||||
|
||||
// Use c_allocator for better performance on Linux
|
||||
const allocator = std.heap.c_allocator;
|
||||
|
||||
|
|
@ -83,7 +79,7 @@ pub fn main() !void {
|
|||
try @import("commands/watch.zig").run(allocator, args[2..]);
|
||||
} else handleUnknownCommand(command),
|
||||
else => {
|
||||
colors.printError("Unknown command: {s}\n", .{args[1]});
|
||||
std.debug.print("Error: Unknown command: {s}\n", .{args[1]});
|
||||
printUsage();
|
||||
return error.InvalidCommand;
|
||||
},
|
||||
|
|
@ -92,17 +88,17 @@ pub fn main() !void {
|
|||
|
||||
// Optimized usage printer
|
||||
fn printUsage() void {
|
||||
colors.printInfo("ML Experiment Manager\n\n", .{});
|
||||
std.debug.print("ML Experiment Manager\n\n", .{});
|
||||
std.debug.print("Usage: ml <command> [options]\n\n", .{});
|
||||
std.debug.print("Commands:\n", .{});
|
||||
std.debug.print(" init Initialize project with config (use --local for SQLite)\n", .{});
|
||||
std.debug.print(" run [args] Execute a run locally (forks, captures, parses metrics)\n", .{});
|
||||
std.debug.print(" queue <job> Queue job on server (--rerun <id> to re-queue local run)\n", .{});
|
||||
std.debug.print(" annotate <id> Add metadata annotations (hypothesis/outcome/confidence)\n", .{});
|
||||
std.debug.print(" experiment Manage experiments (create, list, show)\n", .{});
|
||||
std.debug.print(" logs <id> Fetch or stream run logs (--follow for live tail)\n", .{});
|
||||
std.debug.print(" sync [id] Push local runs to server (sync_run + sync_ack protocol)\n", .{});
|
||||
std.debug.print(" cancel <id> Cancel local run (SIGTERM/SIGKILL by PID)\n", .{});
|
||||
std.debug.print(" init Initialize project with config\n", .{});
|
||||
std.debug.print(" run [args] Execute a run locally\n", .{});
|
||||
std.debug.print(" queue <job> Queue job on server\n", .{});
|
||||
std.debug.print(" annotate <id> Add metadata annotations\n", .{});
|
||||
std.debug.print(" experiment Manage experiments (create, list, show)\n", .{});
|
||||
std.debug.print(" logs <id> Fetch or stream run logs\n", .{});
|
||||
std.debug.print(" sync [id] Push local runs to server\n", .{});
|
||||
std.debug.print(" cancel <id> Cancel local run\n", .{});
|
||||
std.debug.print(" watch [--sync] Watch directory with optional auto-sync\n", .{});
|
||||
std.debug.print(" status Get system status\n", .{});
|
||||
std.debug.print(" dataset Manage datasets\n", .{});
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ pub const rsync = @import("utils/rsync.zig");
|
|||
pub const rsync_embedded = @import("utils/rsync_embedded.zig");
|
||||
pub const rsync_embedded_binary = @import("utils/rsync_embedded_binary.zig");
|
||||
pub const storage = @import("utils/storage.zig");
|
||||
pub const terminal = @import("utils/terminal.zig");
|
||||
|
|
|
|||
|
|
@ -1,166 +1,34 @@
|
|||
// Minimal color output utility optimized for size
|
||||
// Minimal color codes for CLI - no formatting, just basic ANSI
|
||||
const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
// Color codes - only essential ones
|
||||
const colors = struct {
|
||||
pub const reset = "\x1b[0m";
|
||||
pub const red = "\x1b[31m";
|
||||
pub const green = "\x1b[32m";
|
||||
pub const yellow = "\x1b[33m";
|
||||
pub const blue = "\x1b[34m";
|
||||
pub const bold = "\x1b[1m";
|
||||
};
|
||||
pub const reset = "\x1b[0m";
|
||||
pub const red = "\x1b[31m";
|
||||
pub const green = "\x1b[32m";
|
||||
pub const yellow = "\x1b[33m";
|
||||
pub const blue = "\x1b[34m";
|
||||
pub const bold = "\x1b[1m";
|
||||
|
||||
// Check if colors should be disabled
|
||||
var colors_disabled: bool = false;
|
||||
/// Check if colors should be used based on: flag > NO_COLOR > CLICOLOR_FORCE > TTY
|
||||
pub fn shouldUseColor(force_flag: ?bool) bool {
|
||||
// Flag takes precedence
|
||||
if (force_flag) |forced| return forced;
|
||||
|
||||
pub fn disableColors() void {
|
||||
colors_disabled = true;
|
||||
}
|
||||
|
||||
pub fn enableColors() void {
|
||||
colors_disabled = false;
|
||||
}
|
||||
|
||||
// Fast color-aware printing functions
|
||||
pub fn printError(comptime fmt: anytype, args: anytype) void {
|
||||
if (!colors_disabled) {
|
||||
std.debug.print(colors.red ++ colors.bold ++ "Error: " ++ colors.reset, .{});
|
||||
} else {
|
||||
std.debug.print("Error: ", .{});
|
||||
}
|
||||
std.debug.print(fmt, args);
|
||||
}
|
||||
|
||||
pub fn printSuccess(comptime fmt: anytype, args: anytype) void {
|
||||
if (!colors_disabled) {
|
||||
std.debug.print(colors.green ++ colors.bold ++ "✓ " ++ colors.reset, .{});
|
||||
} else {
|
||||
std.debug.print("✓ ", .{});
|
||||
}
|
||||
std.debug.print(fmt, args);
|
||||
}
|
||||
|
||||
pub fn printInfo(comptime fmt: anytype, args: anytype) void {
|
||||
if (!colors_disabled) {
|
||||
std.debug.print(colors.blue ++ "ℹ " ++ colors.reset, .{});
|
||||
} else {
|
||||
std.debug.print("ℹ ", .{});
|
||||
}
|
||||
std.debug.print(fmt, args);
|
||||
}
|
||||
|
||||
pub fn printWarning(comptime fmt: anytype, args: anytype) void {
|
||||
if (!colors_disabled) {
|
||||
std.debug.print(colors.yellow ++ colors.bold ++ "⚠ " ++ colors.reset, .{});
|
||||
} else {
|
||||
std.debug.print("⚠ ", .{});
|
||||
}
|
||||
std.debug.print(fmt, args);
|
||||
}
|
||||
|
||||
// Auto-detect if colors should be disabled
|
||||
pub fn initColors() void {
|
||||
// Disable colors if NO_COLOR environment variable is set
|
||||
// Check NO_COLOR (any value disables colors)
|
||||
if (std.process.getEnvVarOwned(std.heap.page_allocator, "NO_COLOR")) |_| {
|
||||
disableColors();
|
||||
} else |_| {
|
||||
// Default to enabling colors for simplicity
|
||||
colors_disabled = false;
|
||||
}
|
||||
return false;
|
||||
} else |_| {}
|
||||
|
||||
// Check CLICOLOR_FORCE (any value enables colors)
|
||||
if (std.process.getEnvVarOwned(std.heap.page_allocator, "CLICOLOR_FORCE")) |_| {
|
||||
return true;
|
||||
} else |_| {}
|
||||
|
||||
// Default: color if TTY
|
||||
return terminal.isTTY();
|
||||
}
|
||||
|
||||
// Fast string formatting for common cases
|
||||
pub fn formatDuration(seconds: u64) [16]u8 {
|
||||
var result: [16]u8 = undefined;
|
||||
var offset: usize = 0;
|
||||
|
||||
if (seconds >= 3600) {
|
||||
const hours = seconds / 3600;
|
||||
offset += std.fmt.formatIntBuf(result[offset..], hours, 10, .lower, .{});
|
||||
result[offset] = 'h';
|
||||
offset += 1;
|
||||
const minutes = (seconds % 3600) / 60;
|
||||
if (minutes > 0) {
|
||||
offset += std.fmt.formatIntBuf(result[offset..], minutes, 10, .lower, .{});
|
||||
result[offset] = 'm';
|
||||
offset += 1;
|
||||
}
|
||||
} else if (seconds >= 60) {
|
||||
const minutes = seconds / 60;
|
||||
offset += std.fmt.formatIntBuf(result[offset..], minutes, 10, .lower, .{});
|
||||
result[offset] = 'm';
|
||||
offset += 1;
|
||||
const secs = seconds % 60;
|
||||
if (secs > 0) {
|
||||
offset += std.fmt.formatIntBuf(result[offset..], secs, 10, .lower, .{});
|
||||
result[offset] = 's';
|
||||
offset += 1;
|
||||
}
|
||||
} else {
|
||||
offset += std.fmt.formatIntBuf(result[offset..], seconds, 10, .lower, .{});
|
||||
result[offset] = 's';
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
result[offset] = 0;
|
||||
return result;
|
||||
// Legacy function - uses auto-detection
|
||||
pub fn shouldUseColorAuto() bool {
|
||||
return shouldUseColor(null);
|
||||
}
|
||||
|
||||
// Progress bar for long operations
|
||||
pub const ProgressBar = struct {
|
||||
width: usize,
|
||||
current: usize,
|
||||
total: usize,
|
||||
|
||||
pub fn init(total: usize) ProgressBar {
|
||||
return ProgressBar{
|
||||
.width = 50,
|
||||
.current = 0,
|
||||
.total = total,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn update(self: *ProgressBar, current: usize) void {
|
||||
self.current = current;
|
||||
self.render();
|
||||
}
|
||||
|
||||
pub fn render(self: ProgressBar) void {
|
||||
const percentage = if (self.total > 0)
|
||||
@as(f64, @floatFromInt(self.current)) * 100.0 / @as(f64, @floatFromInt(self.total))
|
||||
else
|
||||
0.0;
|
||||
|
||||
const filled = @as(usize, @intFromFloat(percentage * @as(f64, @floatFromInt(self.width)) / 100.0));
|
||||
const empty = self.width - filled;
|
||||
|
||||
if (!colors_disabled) {
|
||||
std.debug.print("\r[" ++ colors.green, .{});
|
||||
} else {
|
||||
std.debug.print("\r[", .{});
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < filled) : (i += 1) {
|
||||
std.debug.print("=", .{});
|
||||
}
|
||||
|
||||
if (!colors_disabled) {
|
||||
std.debug.print(colors.reset, .{});
|
||||
}
|
||||
|
||||
i = 0;
|
||||
while (i < empty) : (i += 1) {
|
||||
std.debug.print(" ", .{});
|
||||
}
|
||||
|
||||
std.debug.print("] {d:.1}%\r", .{percentage});
|
||||
}
|
||||
|
||||
pub fn finish(self: ProgressBar) void {
|
||||
self.current = self.total;
|
||||
self.render();
|
||||
std.debug.print("\n", .{});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
35
cli/src/utils/terminal.zig
Normal file
35
cli/src/utils/terminal.zig
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Check if stdout is a TTY
|
||||
pub fn isTTY() bool {
|
||||
return std.posix.isatty(std.posix.STDOUT_FILENO);
|
||||
}
|
||||
|
||||
/// Get terminal width from COLUMNS env var
|
||||
pub fn getWidth() ?usize {
|
||||
const allocator = std.heap.page_allocator;
|
||||
if (std.process.getEnvVarOwned(allocator, "COLUMNS")) |cols| {
|
||||
defer allocator.free(cols);
|
||||
return std.fmt.parseInt(usize, cols, 10) catch null;
|
||||
} else |_| {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Table formatting mode
|
||||
pub const TableMode = enum { truncate, wrap, auto };
|
||||
|
||||
/// Get table formatting mode from env var
|
||||
pub fn getTableMode() TableMode {
|
||||
const allocator = std.heap.page_allocator;
|
||||
const mode_str = std.process.getEnvVarOwned(allocator, "ML_TABLE_MODE") catch return .truncate;
|
||||
defer allocator.free(mode_str);
|
||||
if (std.mem.eql(u8, mode_str, "wrap")) return .wrap;
|
||||
if (std.mem.eql(u8, mode_str, "auto")) return .auto;
|
||||
return .truncate;
|
||||
}
|
||||
|
||||
/// Get user's preferred pager from PAGER env var
|
||||
pub fn getPager() ?[]const u8 {
|
||||
const allocator = std.heap.page_allocator;
|
||||
return std.process.getEnvVarOwned(allocator, "PAGER") catch null;
|
||||
}
|
||||
Loading…
Reference in a new issue