Files
envr/src/tabula.zig
2026-06-01 14:55:21 -04:00

315 lines
8.3 KiB
Zig

const std = @import("std");
const hor = "";
const tl = "";
const tm = "";
const tr = "";
const sep = "";
const ml = "";
const mm = "";
const mr = "";
const bl = "";
const bm = "";
const br = "";
// Print a list of structs as a table to opts.out.
pub fn structs(
comptime T: type,
items: []T,
opts: struct {
out: *std.Io.Writer, // TODO: Default stdout.
fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,
},
) !void {
const writer = opts.out;
const max_column_widths = determine_col_widths(T, items);
try header(T, opts.fields, &max_column_widths, opts.out);
// Print body
for (items) |item| {
_ = try writer.write(sep);
// TODO: Not the most efficient
const all_fields = @typeInfo(T).@"struct".fields;
inline for (all_fields) |field| {
var itr = opts.fields.iterator();
var i: usize = 0;
while (itr.next()) |c| : (i += 1) {
if (std.mem.eql(u8, @tagName(c), field.name)) {
_ = try writer.write(" ");
try write_aligned(writer, @field(item, field.name), max_column_widths[i], .left);
try writer.print(" {s}", .{sep});
break;
}
}
}
_ = try writer.write("\n");
}
// Print post-body
{
_ = try writer.write(bl);
var itr = opts.fields.iterator();
var i: usize = 0;
while (itr.next()) |_| : (i += 1) {
if (i > 0) {
_ = try writer.write(bm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
_ = try writer.write(hor);
}
}
_ = try writer.write(br);
_ = try writer.write("\n");
}
}
fn determine_col_widths(
T: type,
items: []T,
) [@typeInfo(T).@"struct".fields.len]usize {
const all_fields = @typeInfo(T).@"struct".fields;
var max_column_widths: [all_fields.len]usize = @splat(0);
for (items) |item| {
inline for (all_fields, 0..) |field, i| {
// TODO: Get str len of item
const value_len = @field(item, field.name).len;
max_column_widths[i] = @max(
max_column_widths[i],
field.name.len,
value_len,
);
}
}
return max_column_widths;
}
// Print the header of a table
fn header(
T: type,
fields: std.EnumSet(std.meta.FieldEnum(T)),
max_column_widths: []const usize,
writer: *std.Io.Writer,
) !void {
// Print Pre-Header
{
_ = try writer.write(tl);
var itr = fields.iterator();
var i: usize = 0;
while (itr.next()) |_| : (i += 1) {
if (i > 0) {
_ = try writer.write(tm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
_ = try writer.write(hor);
}
}
_ = try writer.write(tr ++ "\n");
}
// Main Header
{
_ = try writer.write(sep);
var itr = fields.iterator();
var i: usize = 0;
while (itr.next()) |field| : (i += 1) {
_ = try writer.write(" ");
try write_aligned(
writer,
@tagName(field),
max_column_widths[i],
.center,
);
try writer.print(" {s}", .{sep});
}
try writer.print("\n", .{});
}
// Print post-header
{
_ = try writer.write(ml);
var itr = fields.iterator();
var i: usize = 0;
while (itr.next()) |_| : (i += 1) {
if (i > 0) {
_ = try writer.write(mm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
_ = try writer.write(hor);
}
}
_ = try writer.write(mr ++ "\n");
}
}
fn write_aligned(
writer: *std.Io.Writer,
data: []const u8,
max_width: usize,
alignment: Alignment,
) !void {
const padding: [2]usize = switch (alignment) {
.left => .{ 0, max_width - data.len },
.right => .{ max_width - data.len, 0 },
.center => blk: {
// Faster to inline the divFloor?
const half = @divFloor(max_width - data.len, 2);
break :blk .{ half, max_width - data.len - half };
},
};
for (0..padding[0]) |_| {
_ = try writer.write(" ");
}
_ = try writer.write(data);
for (0..padding[1]) |_| {
_ = try writer.write(" ");
}
}
const Alignment = enum { left, center, right };
test "can print a simple table" {
const gpa = std.testing.allocator;
const F = struct { foo: []const u8, bar: []const u8 };
// const table: Table(F) = .{ .rows = &.{.{ .foo = "bat", .bar = "baz" }} };
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }};
try structs(F, @constCast(&rows), .{ .out = &out.writer });
// try out.writer.print("{f}", .{table});
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬─────┐
\\│ foo │ bar │
\\├─────┼─────┤
\\│ bat │ baz │
\\└─────┴─────┘
\\
, got);
}
test "can print a table with varying header widths" {
const gpa = std.testing.allocator;
const F = struct { foo: []const u8, abart: []const u8 };
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const rows: [1]F = .{.{ .foo = "bat", .abart = "baz" }};
try structs(F, @constCast(&rows), .{ .out = &out.writer });
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬───────┐
\\│ foo │ abart │
\\├─────┼───────┤
\\│ bat │ baz │
\\└─────┴───────┘
\\
, got);
}
test "can print a table with varying column widths" {
const gpa = std.testing.allocator;
const F = struct { foo: []const u8, bar: []const u8 };
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const rows: [1]F = .{.{ .foo = "bat", .bar = "bazzar" }};
try structs(F, @constCast(&rows), .{ .out = &out.writer });
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬────────┐
\\│ foo │ bar │
\\├─────┼────────┤
\\│ bat │ bazzar │
\\└─────┴────────┘
\\
, got);
}
test "can print a multi row table with varying column widths" {
const gpa = std.testing.allocator;
const F = struct { foo: []const u8, bar: []const u8 };
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const rows: []const F = &.{
.{ .foo = "baz", .bar = "quz" },
.{ .foo = "bat", .bar = "bazzar" },
};
try structs(F, @constCast(rows), .{ .out = &out.writer });
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┬────────┐
\\│ foo │ bar │
\\├─────┼────────┤
\\│ baz │ quz │
\\│ bat │ bazzar │
\\└─────┴────────┘
\\
, got);
}
test "can print a table with limited columns" {
const gpa = std.testing.allocator;
const F = struct { foo: []const u8, bar: []const u8 };
const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }};
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
try structs(F, @constCast(&rows), .{ .out = &out.writer, .fields = .initOne(.foo) });
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┐
\\│ foo │
\\├─────┤
\\│ bat │
\\└─────┘
\\
, got);
}
// TODO: test "returns a table" {}