const std = @import("std"); const Writer = std.Io.Writer; const Cmd = @import("Cmd.zig"); const blush = @import("blush"); const cmd_style = blush.new().fg(.cyan).attr(.bold, true); const param_style = blush.new().fg(.cyan).attr(.bold, true); const title_style = blush.new().fg(.magenta).attr(.bold, true); const dim_style = blush.new().fg(.bright_black); const err_style = blush.new().fg(.red).attr(.bold, true); const label_style = blush.new().fg(.yellow).attr(.bold, true); const cmd_ul_style = cmd_style.ul(.single); const param_ul_style = param_style.ul(.single); const dim_ul_style = dim_style.ul(.single); pub fn printGroup( writer: *Writer, root_description: []const u8, group: ?Cmd, cmds: []const Cmd, ) !void { if (group) |g| { if (g.description.len > 0) { try title_style.render(writer, g.key); try writer.writeAll(": "); try writer.print("{s}\n\n", .{g.description}); } } else if (root_description.len > 0) { try writer.print("{s}\n\n", .{root_description}); } if (cmds.len == 0) return; var max_key: usize = 0; for (cmds) |cmd| max_key = @max(max_key, cmd.key.len); try writer.writeAll(" "); try label_style.render(writer, "Commands"); try writer.writeByte('\n'); for (cmds) |cmd| { const desc = switch (cmd.value) { .cmd => firstLine(cmd.description), .list => firstLine(cmd.description), }; try writer.writeAll(" "); try renderMinPrefix(cmd_ul_style, cmd_style, writer, cmd.key, cmds, "key"); try pad(writer, cmd.key.len, max_key + 4); try writer.print("{s}\n", .{desc}); } } pub fn printCommand( writer: *Writer, name: []const u8, description: []const u8, params: []const Cmd.Command.Param, ) !void { if (description.len > 0) { try title_style.render(writer, name); try writer.writeAll(": "); try writer.print("{s}\n\n", .{description}); } if (params.len == 0) return; var max_name: usize = 0; var max_type: usize = 0; for (params) |p| { max_name = @max(max_name, p.name.len); const display = if (p.values.len > 0) p.values else p.type_name; max_type = @max(max_type, display.len); } try writer.writeAll(" "); try label_style.render(writer, "Arguments"); try writer.writeByte('\n'); for (params) |p| { const display = if (p.values.len > 0) p.values else p.type_name; try writer.writeAll(" "); try renderMinPrefix(param_ul_style, param_style, writer, p.name, params, "name"); try pad(writer, p.name.len, max_name + 4); if (p.values.len > 0) { try renderValues(writer, p.values); } else { try dim_style.render(writer, display); } try pad(writer, display.len, max_type + 4); try writer.print("{s}", .{firstLine(p.description)}); if (!p.required) { try writer.writeAll(" "); try dim_style.render(writer, "[optional]"); } try writer.writeByte('\n'); } } pub fn printError(writer: *Writer, msg: []const u8) !void { try err_style.render(writer, "error:"); try writer.writeByte(' '); try writer.writeAll(msg); } fn firstLine(s: []const u8) []const u8 { return if (std.mem.findScalar(u8, s, '\n')) |n| s[0..n] else s; } /// Renders `name` with the shortest prefix that uniquely identifies it among /// `items` (by field `field_name`) underlined. fn renderMinPrefix( ul_style: blush.Style, base_style: blush.Style, writer: *Writer, name: []const u8, items: anytype, comptime field_name: []const u8, ) !void { const n = minPrefixLen(name, items, field_name); try ul_style.render(writer, name[0..n]); if (n < name.len) try base_style.render(writer, name[n..]); } fn minPrefixLen(name: []const u8, items: anytype, comptime field_name: []const u8) usize { var best: usize = 1; for (items) |item| { const other = @field(item, field_name); if (std.mem.eql(u8, other, name)) continue; const n = @min(name.len, other.len); var lcp: usize = 0; while (lcp < n and name[lcp] == other[lcp]) : (lcp += 1) {} best = @max(best, @min(lcp + 1, name.len)); } return best; } /// Renders a values string like "[installed, all, updates]" with the minimum /// unique prefix of each value underlined. fn renderValues(writer: *Writer, values: []const u8) !void { if (values.len < 2 or values[0] != '[' or values[values.len - 1] != ']') { try dim_style.render(writer, values); return; } const inner = values[1 .. values.len - 1]; try dim_style.render(writer, "["); var first = true; var it = std.mem.splitScalar(u8, inner, ','); while (it.next()) |raw| { const seg = std.mem.trim(u8, raw, " "); if (seg.len == 0) continue; if (!first) try dim_style.render(writer, ", "); first = false; const n = valueMinPrefixLen(seg, inner); try dim_ul_style.render(writer, seg[0..n]); if (n < seg.len) try dim_style.render(writer, seg[n..]); } try dim_style.render(writer, "]"); } fn valueMinPrefixLen(seg: []const u8, inner: []const u8) usize { var best: usize = 1; var it = std.mem.splitScalar(u8, inner, ','); while (it.next()) |raw| { const other = std.mem.trim(u8, raw, " "); if (std.mem.eql(u8, other, seg)) continue; const n = @min(seg.len, other.len); var lcp: usize = 0; while (lcp < n and seg[lcp] == other[lcp]) : (lcp += 1) {} best = @max(best, @min(lcp + 1, seg.len)); } return best; } fn pad(writer: *Writer, current: usize, target: usize) !void { if (current >= target) return; try writer.splatByteAll(' ', target - current); }