Something went wrong. Try again.
Completely reimplimented in zig (eventually)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145//! Terminal styling built on blush. Color output is only enabled when stdout//! is a TTY and `NO_COLOR` is unset, so piped/redirected output stays clean.
const std = @import("std");const Io = std.Io;const mem = std.mem;const blush = @import("blush");
const Writer = std.Io.Writer;const Allocator = std.mem.Allocator;
const TIOCGWINSZ: usize = 0x5413;const default_width: u16 = 80;
pub const Ui = struct { color: bool, width: u16,
/// Probe the environment once per command invocation. pub fn detect(io: Io, env: ?*const std.process.Environ.Map) Ui { const tty = Io.File.stdout().isTty(io) catch false; var ui = Ui{ .color = tty and (env == null or env.?.get("NO_COLOR") == null), .width = default_width, }; if (tty) ui.width = detectWidth(); return ui; }
fn detectWidth() u16 { var ws: std.posix.winsize = .{ .row = 0, .col = 0, .xpixel = 0, .ypixel = 0 }; const rc = std.os.linux.ioctl(Io.File.stdout().handle, TIOCGWINSZ, @intFromPtr(&ws)); if (std.posix.errno(rc) == .SUCCESS and ws.col > 10) return @intCast(ws.col); return default_width; }
fn emit(self: Ui, w: *Writer, style: blush.Style, text: []const u8) Writer.Error!void { if (self.color) { try style.render(w, text); } else { try w.writeAll(text); } }
/// Bold emphasis (pack names, key values). pub fn strong(self: Ui, w: *Writer, text: []const u8) Writer.Error!void { try self.emit(w, .{ .bold = true }, text); }
/// De-emphasised annotations (labels, hints). pub fn dim(self: Ui, w: *Writer, text: []const u8) Writer.Error!void { try self.emit(w, .{ .faint = true }, text); }
/// Success values / up-to-date versions. pub fn good(self: Ui, w: *Writer, text: []const u8) Writer.Error!void { try self.emit(w, .{ .foreground = .green }, text); }
/// Attention values (outdated versions). pub fn warn(self: Ui, w: *Writer, text: []const u8) Writer.Error!void { try self.emit(w, .{ .foreground = .yellow }, text); }
/// Section headers within a command's output. pub fn header(self: Ui, w: *Writer, text: []const u8) Writer.Error!void { try self.emit(w, .{ .bold = true, .underline = .single }, text); }};
/// Styled "error" prefix for diagnostics on stderr. In plain mode the prefix/// stays greppable as `error: `.pub fn errPrefix(io: Io, w: *Writer) Writer.Error!void { const ui = Ui.detect(io, null); if (!ui.color) { try w.writeAll("error: "); return; } try ui.emit(w, .{ .bold = true, .foreground = .red }, "error"); try w.writeAll(": ");}
/// Greedily flow `items` into comma-separated rows of at most `width`/// display columns. Rows are plain text — apply styling afterwards.pub fn flowColumns( allocator: Allocator, items: []const []const u8, width: u16,) ![][]u8 { var rows: std.ArrayList([]u8) = .empty; errdefer { for (rows.items) |r| allocator.free(r); rows.deinit(allocator); }
var row: std.ArrayList(u8) = .empty; errdefer row.deinit(allocator);
var idx: usize = 0; while (idx < items.len) { row.clearRetainingCapacity(); var row_len: usize = 0; while (idx < items.len) : (idx += 1) { const new_len = if (row_len == 0) items[idx].len else row_len + 2 + items[idx].len; if (row_len != 0 and new_len > width) break; if (row_len != 0) { try row.appendSlice(allocator, ", "); row_len += 2; } try row.appendSlice(allocator, items[idx]); row_len += items[idx].len; } try rows.append(allocator, try allocator.dupe(u8, row.items)); }
return rows.toOwnedSlice(allocator);}
/// Free the rows returned by `flowColumns`.pub fn freeRows(allocator: Allocator, rows: [][]u8) void { for (rows) |r| allocator.free(r); allocator.free(rows);}
test "flowColumns wraps at width" { const gpa = std.testing.allocator; const items = [_][]const u8{ "alpha", "beta", "gamma", "delta", "epsilon", "zeta" }; const rows = try flowColumns(gpa, &items, 12); defer freeRows(gpa, rows); // "alpha, beta," is 11 chars; "gamma," would push past 12 → second row. try std.testing.expectEqualStrings("alpha, beta", rows[0]); // Every item survives the flow. var count: usize = 0; for (rows) |r| count += mem.count(u8, r, ", ") + 1; try std.testing.expectEqual(items.len, count);}
test "flowColumns keeps oversized item on its own row" { const gpa = std.testing.allocator; const items = [_][]const u8{ "a", "verylongdevicename exceeding width", "b" }; const rows = try flowColumns(gpa, &items, 10); defer freeRows(gpa, rows); try std.testing.expectEqual(@as(usize, 3), rows.len);}