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