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" {}