# Allocation Free TUI Tables

**URL:** <https://ziggit.dev/t/allocation-free-tui-tables/16009>\
**Category:** Help\
**Tags:** language, memory-management\
**Created:** [June 7, 2026, 11:05pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009 "2026-06-07T23:05:50Z")\
**Posts on this page:** 14\
**Page:** 1

<div class="post-metadata">

**Author:** ![sbrow](https://ziggit.dev/user_avatar/ziggit.dev/sbrow/32/4970_2.png) [@sbrow](https://ziggit.dev/u/sbrow)\
**Post date:** [June 7, 2026, 11:05pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/1 "2026-06-07T23:05:50Z")

</div>

Sorry for the noob question, I’m sure most people have already done this and figured it out, but I’ve always let libraries do it for me until now. I’m quickly realizing why Zig doesn’t have a native string type 😅

I’m porting a CLI project over from Go, and I need a really simple table:

```bash
┌──────────────────────────────┬──────┐
│ DIRECTORY │ PATH │
├──────────────────────────────┼──────┤
│ /home/username/Desktop/envr/ │ .env │
└──────────────────────────────┴──────┘

```

I was able to get this working, but the code I wrote feels…far from correct. The function is called `structs`, it accepts a list of structs, a writer, and optionally a subset of struct fields to print (by default it prints all of them:

```zig
test "can print a table with limited columns" {
    const gpa = std.testing.allocator;

    var out: std.Io.Writer.Allocating = .init(gpa);
    defer out.deinit();

    const F = struct { foo: []const u8, bar: []const u8 };
    const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }};

    // I realize @constCast is a code smell, I'll fix it later
    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);
}

```

I basically just defer to `writer` for everything, and while it works, code like this feels inelegant, especially when called inside of an `inline for` loop:

```zig
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
    _ = try writer.write("─");
}

```

I would like to try and do this without allocations, but have found that to pretty difficult (if I deviate from my working code). Even if I assign a line buffer, and break every column up into slices perfectly, the characters I’ve used to draw the lines take up more than one byte, making the math awkward.

Any advice on how the zig zen way to do this would be appreciated 🙂

> - Only one obvious way to do things.

### Here is the full code

tabula.zig

```zig
const std = @import("std");

// Could be an enum? or tagged union.
const hor = "─"; // Horizontal
const tl = "┌"; // Top Left
const tm = "┬"; // Top Middle
const tr = "┐"; // Top Right
const sep = "│"; // Separator
const ml = "├"; // Middle Left
const mm = "┼"; // Middle Middle (Center)
const mr = "┤"; // Middle Right
const bl = "└"; // Bottom Left
const bm = "┴"; // Bottom Middle
const br = "┘"; // Bottom Right

/// 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 to 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: Always loops through every field
        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 };

```

I did manage to rewrite `write_aligned` in a form that didn’t require a writer anymore

```zig
fn align_text(data: []const u8, buf: []u8, alignment: Alignment) void {
    const dest = switch (alignment) {
        .left => buf[0..data.len],
        .right => buf[buf.len - data.len .. buf.len],
        .center => blk: {
            const start = (buf.len - data.len) / 2;

            break :blk buf[start .. start + data.len];
        },
    };

    @memcpy(dest, data);
}

```

But this necessitated adding a (fixed) buffer, which while probably fine for my small tables, could potentially run out with long path names.

```zig
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,
    },
) error{WriteFailed}!void {
    const writer = opts.out;
    const max_column_widths = determine_col_widths(T, items);
    var buf: [255]u8 = undefined;

    // Print body
    for (items) |item| {
        _ = try writer.write(sep);
 
        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)) {
                    const col = buf[0..max_column_widths[i]];
                    @memset(col, ' ');

                    align_text(@field(item, field.name), col, .left);

                    try writer.print(" {s} {s}", .{ col, sep });

                    break;
                }
            }
        }

        _ = try writer.write("\n");
    }
    
    // ...
}

```

If there’s a way to loop through just the selected fields at comptime rather than having to loop + check all of them, that would also be great.

```zig
// TODO: Always loops through every field
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;
    }
}

```

---

<div class="post-metadata">

**Author:** ![npc1054657282](https://ziggit.dev/user_avatar/ziggit.dev/npc1054657282/32/5790_2.png) [@npc1054657282](https://ziggit.dev/u/npc1054657282)\
**Post date:** [June 8, 2026, 3:00am UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/2 "2026-06-08T03:00:00Z")

</div>

> [@sbrow](#):
>
> I would like to try and do this without allocations

For strings with lengths unknown at compile time, using an allocator is quite reasonable. Although the strings in your example are known at compile time, in actual use there will inevitably be a large amount of content that is only known at runtime. One should not deliberately avoid allocation for this reason, especially under the abstraction of `writer`, where in fact allocation or not is also user-configurable.

> [@sbrow](#):
>
> ```zig
> pub fn structs(
> comptime T: type,
> items: []T,
> opts: struct {
> out: *std.Io.Writer, // TODO: Default to stdout.
> fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,
> },
> ) !void {
> 
> ```

I don’t think there is a major problem with your original implementation idea. However, regarding the function signature, `items: []T,` here should be `items: []const T,` (the same applies to the function signature of `determine_col_widths`) because there are no modifications to `items` within the entire function.

In addition, I hope the `writer` does not default to `stdout`. I can’t think of any benefit in implicitly defaulting to `stdout`; no one wants the output table to be printed to `stdout` by default.

> [@sbrow](#):
>
> did manage to rewrite `write_aligned` in a form that didn’t require a writer anymore
> 
> ```zig
> fn align_text(data: []const u8, buf: []u8, alignment: Alignment) void {
> const dest = switch (alignment) {
> .left => buf[0..data.len],
> .right => buf[buf.len - data.len .. buf.len],
> .center => blk: {
> const start = (buf.len - data.len) / 2;
> 
> break :blk buf[start .. start + data.len];
> },
> };
> 
> @memcpy(dest, data);
> }
> 
> ```
> 
> But this necessitated adding a (fixed) buffer, which while probably fine for my small tables, could potentially run out with long path names.

In my eyes, this is a significant regression. Although using buffers is not inadvisable, it is obvious that hard-coded buffer sizes are absolutely unacceptable. Even when using buffers, they should be allocated with a runtime-determined size. Clearly, this makes the code logic more complex and fails to reuse the `writer` abstraction.

---

<div class="post-metadata">

**Author:** ![sbrow](https://ziggit.dev/user_avatar/ziggit.dev/sbrow/32/4970_2.png) [@sbrow](https://ziggit.dev/u/sbrow)\
**Post date:** [June 8, 2026, 3:26am UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/3 "2026-06-08T03:26:45Z")

</div>

> [@npc1054657282](#):
>
> In addition, I hope the `writer` does not default to `stdout`. I can’t think of any benefit in implicitly defaulting to `stdout`;

Thank you for your time and input!

Perhaps it is better (and more idiomatic) to inject stdout at the top and trickle it all thr way down. However if its a mandatory parameter, then perhaps it deserves its own slot in the function definition, rather than tucked away in a struct.

And thanks for solving my `@constCast` issue, still getting used to wrangling that.

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [June 8, 2026, 9:41am UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/4 "2026-06-08T09:41:15Z")

</div>

> [@npc1054657282](#):
>
> here should be `items: const []T,`

`items: []const T`

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [June 8, 2026, 10:04am UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/5 "2026-06-08T10:04:30Z")

</div>

> [@sbrow](#):
>
> ```zig
> pub fn structs(
> comptime T: type,
> items: []T,
> opts: struct {
> out: *std.Io.Writer, // TODO: Default to stdout.
> fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,
> },
> ) 
> 
> ```

You could separate this into a generic function producing a type and that type having a format function:

```zig
pub fn Structs(comptime T: type) type {
    return struct {
        items: []const T,
        fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,

        pub fn format(self:@This(), writer:*std.Io.Writer) !void {
            // ...
        }
    };
}

```

> [@sbrow](#):
>
> I did manage to rewrite `write_aligned` in a form that didn’t require a writer anymore

Why? I think using a writer would be better…  
I think using fixed size buffers for things you can calculate and directly write to a writer is bad, because it adds unnecessary intermediary restrictions and potential code bloat (for example when these buffers are poorly used which is likely because we already have the writer which can handle buffering).

> [@sbrow](#):
>
> If there’s a way to loop through just the selected fields at comptime rather than having to loop + check all of them, that would also be great.

You can move the fields to be a comptime parameter:

```zig
pub const Options = struct {
    fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,
};
pub fn Structs(comptime T: type, comptime options:Options) type {
    return struct {
        items: []const T,
        ...

```

---

<div class="post-metadata">

**Author:** ![sbrow](https://ziggit.dev/user_avatar/ziggit.dev/sbrow/32/4970_2.png) [@sbrow](https://ziggit.dev/u/sbrow)\
**Post date:** [June 8, 2026, 12:40pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/6 "2026-06-08T12:40:03Z")

</div>

Thank you both for your insight!

> [@Sze](#):
>
> > [@sbrow](#):
> >
> > I did manage to rewrite `write_aligned` in a form that didn’t require a writer anymore
> 
> Why? I think using a writer would be better…

Because I am new to working at this level (close to the cpu). It felt more intuitive and performant to align the bytes inside the column buffer using `@memcopy`and `@memset`, than to make a bunch of extra function calls with the writer. Probably also influenced by functional programming training scars 😅

It sounds like my initial idea wasn’t terrible, and if I want to use a buffer, I should use an `ArrayList`.

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [June 8, 2026, 1:06pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/7 "2026-06-08T13:06:48Z")

</div>

> [@sbrow](#):
>
> It sounds like my initial idea wasn’t terrible, and if I want to use a buffer, I should use an `ArrayList`.

Just use a format function and pass it a writer like [std.Io.Writer.Allocating](https://ziglang.org/documentation/0.16.0/std/#std.Io.Writer.Allocating), it can be used as a writer (and is similar-ish to an `ArrayList(u8)`) and then you can do one of these things:

- access the part that was accumulated in memory by using `aw.written()`
- convert it to an ArrayList with `aw.toArrayList()`
- use `aw.toOwnedSlice()`
- reset it with `aw.clearRetainingCapacity()` and use it for another task
- pass `&aw.writer` to functions expecting a writer

And alternatively if your code that needs to use the format function already has a fixed buffer available to it: [std.Io.Writer.fixed](https://ziglang.org/documentation/0.16.0/std/#std.Io.Writer.fixed) (but I would avoid adding lots of hardcoded fixed buffers, especially if you don’t have a good way to find the size needed for it)

---

<div class="post-metadata">

**Author:** ![sbrow](https://ziggit.dev/user_avatar/ziggit.dev/sbrow/32/4970_2.png) [@sbrow](https://ziggit.dev/u/sbrow)\
**Post date:** [June 8, 2026, 1:16pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/8 "2026-06-08T13:16:26Z")

</div>

> [@Sze](#):
>
> ```zig
> pub fn Structs(comptime T: type) type {
> return struct {
> items: []const T,
> fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,
> 
> pub fn format(self:@This(), writer:*std.Io.Writer) !void {
> // ...
> }
> };
> }
> 
> ```

I do like separating the type from the writing, I feel that that’s cleaner, however, it seems I’m still SOL with the comptime loop :’(

```zig
/// Prepare a TUI table to be written to a writer.
pub fn Table(comptime T: type) type {
    return struct {
        items: []const T,
        comptime fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,

        pub fn format(self: @This(), writer: *std.Io.Writer) !void {
            const max_column_widths = determine_col_widths(T, self.items);

            try header(T, self.fields, &max_column_widths, writer);

            // Print body
            for (self.items) |item| {
                _ = try writer.write(sep);

                // const all_fields = @typeInfo(T).@"struct".fields;
                // inline for (all_fields) |field| {
                var itr = self.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 write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                    try writer.print(" {s}", .{sep});

                    break;
                    // }
                }
                // }

                _ = try writer.write("\n");
            }

            // Print post-body
            {
                _ = try writer.write(bl);

                var itr = self.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");
            }
        }
    };
}

```

```bash
zig build test
test
└─ run test
   └─ compile test Debug native 5 errors
src/tabula.zig:33:38: error: runtime value contains reference to comptime var
                var itr = self.fields.iterator();
                          ~~~~~~~~~~~^~~~~~~~~
src/tabula.zig:33:38: note: comptime var pointers are not available at runtime
src/tabula.zig:33:38: note: 'runtime_value' points to comptime field
referenced by:
    printValue__anon_51379: deps/zig/lib/std/Io/Writer.zig:1077:39
    print__anon_51306: deps/zig/lib/std/Io/Writer.zig:719:25
    2 reference(s) hidden; use '-freference-trace=4' to see all references
src/tabula.zig:33:38: error: runtime value contains reference to comptime var
                var itr = self.fields.iterator();
                          ~~~~~~~~~~~^~~~~~~~~
src/tabula.zig:33:38: note: comptime var pointers are not available at runtime
src/tabula.zig:33:38: note: 'runtime_value' points to comptime field
src/tabula.zig:33:38: error: runtime value contains reference to comptime var
                var itr = self.fields.iterator();
                          ~~~~~~~~~~~^~~~~~~~~
src/tabula.zig:33:38: note: comptime var pointers are not available at runtime
src/tabula.zig:33:38: note: 'runtime_value' points to comptime field
src/tabula.zig:33:38: error: runtime value contains reference to comptime var
                var itr = self.fields.iterator();
                          ~~~~~~~~~~~^~~~~~~~~
src/tabula.zig:33:38: note: comptime var pointers are not available at runtime
src/tabula.zig:33:38: note: 'runtime_value' points to comptime field
src/tabula.zig:305:27: error: cannot store runtime value in compile time variable
        .fields = .initOne(.foo),
                  ~~~~~~~~^~~~~~

```

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [June 8, 2026, 1:19pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/9 "2026-06-08T13:19:07Z")

</div>

Don’t use comptime fields and especially don’t mix types you want to use for comptime programming with types you want to use at runtime.

Make the fields part of the parameters to `Table` like I showed in my example.

---

<div class="post-metadata">

**Author:** ![sbrow](https://ziggit.dev/user_avatar/ziggit.dev/sbrow/32/4970_2.png) [@sbrow](https://ziggit.dev/u/sbrow)\
**Post date:** [June 8, 2026, 1:28pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/10 "2026-06-08T13:28:58Z")

</div>

It seems I need a way to get `inline for` over fields rather than an iterator.

```zig
/// Prepare a TUI table to be written to a writer.
pub fn Table(
    comptime T: type,
    comptime fields: std.EnumSet(std.meta.FieldEnum(T)),
) type {
    return struct {
        items: []const T,

        pub fn format(self: @This(), writer: *std.Io.Writer) !void {
            const max_column_widths = determine_col_widths(T, self.items);

            try header(T, fields, &max_column_widths, writer);

            // Print body
            for (self.items) |item| {
                _ = try writer.write(sep);

                // const all_fields = @typeInfo(T).@"struct".fields;
                // inline for (all_fields) |field| {
                var itr = 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 write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                    try writer.print(" {s}", .{sep});

                    break;
                    // }
                }
                // }

                _ = try writer.write("\n");
            }

            // Print post-body
            {
                _ = try writer.write(bl);

                var itr = self.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");
            }
        }
    };
}

```

```bash
zig build test
test
└─ run test
   └─ compile test Debug native 5 errors
tabula.zig:41:60: error: unable to evaluate comptime expression
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                           ^ ~~~~~~~~~~
tabula.zig:41:69: note: operation is runtime due to this operand
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                                    ^
tabula.zig:41:60: note: field name must be comptime-known
referenced by:
    printValue__anon_51362: /home/spencer/github.com/envr-zig/deps/zig/lib/std/Io/Writer.zig:1077:39
    print__anon_51291: /home/spencer/github.com/envr-zig/deps/zig/lib/std/Io/Writer.zig:719:25
    2 reference(s) hidden; use '-freference-trace=4' to see all references
tabula.zig:41:60: error: unable to evaluate comptime expression
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                           ^ ~~~~~~~~~~
tabula.zig:41:69: note: operation is runtime due to this operand
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                                    ^
tabula.zig:41:60: note: field name must be comptime-known
tabula.zig:41:60: error: unable to evaluate comptime expression
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                           ^ ~~~~~~~~~~
tabula.zig:41:69: note: operation is runtime due to this operand
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                                    ^
tabula.zig:41:60: note: field name must be comptime-known
tabula.zig:41:60: error: unable to evaluate comptime expression
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                           ^ ~~~~~~~~~~
tabula.zig:41:69: note: operation is runtime due to this operand
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                                    ^
tabula.zig:41:60: note: field name must be comptime-known
tabula.zig:41:60: error: unable to evaluate comptime expression
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                           ^ ~~~~~~~~~~
tabula.zig:41:69: note: operation is runtime due to this operand
                    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
                                                                    ^
tabula.zig:41:60: note: field name must be comptime-known

```

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [June 8, 2026, 1:54pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/11 "2026-06-08T13:54:03Z")

</div>

If you want to iterate over all fields you could do something like this:

```zig
comptime var column_index = 0;
const all_fields = @typeInfo(std.meta.FieldEnum(T)).@"enum".fields;
inline for (all_fields) |field| {
    if(comptime fields.contains(field.value)) {
        _ = try writer.write(" ");
        // try write_aligned(writer, @field(item, field.name), max_column_widths[column_index], .left);
        try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[column_index], .left);
        try writer.print(" {s}", .{sep});
        column_index += 1;
    }
}

```

Alternatively you could use:

```zig
comptime var itr = fields.iterator();
comptime var i: usize = 0;
inline while (itr.next()) |c| : (i += 1) {
    _ = try writer.write(" ");
    // try write_aligned(writer, @field(item, field.name), max_column_widths[i], .left);
    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
    try writer.print(" {s}", .{sep});
}

```

---

<div class="post-metadata">

**Author:** ![sbrow](https://ziggit.dev/user_avatar/ziggit.dev/sbrow/32/4970_2.png) [@sbrow](https://ziggit.dev/u/sbrow)\
**Post date:** [June 8, 2026, 8:10pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/12 "2026-06-08T20:10:50Z")

</div>

> [@Sze](#):
>
> Alternatively you could use:
> 
> ```zig
> comptime var itr = fields.iterator();
> comptime var i: usize = 0;
> inline while (itr.next()) |c| : (i += 1) {
> _ = try writer.write(" ");
> // try write_aligned(writer, @field(item, field.name), max_column_widths[i], .left);
> try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
> try writer.print(" {s}", .{sep});
> }
> 
> ```

Unfortunately, no matter what way I slice it, the compiler rejects it.

```bash
zig build test
test
└─ run test
   └─ compile test Debug native 5 errors
src/tabula.zig:111:26: error: runtime value contains reference to comptime var
        inline while (itr.next()) |_| : (i += 1) {
                      ~~~^~~~~
src/tabula.zig:111:26: note: comptime var pointers are not available at runtime
src/tabula.zig:109:43: note: 'runtime_value' points to comptime var declared here
        comptime var itr = fields.iterator();
                           ~~~~~~~~~~~~~~~ ^~

```

---

<div class="post-metadata">

**Author:** ![Sze](https://ziggit.dev/user_avatar/ziggit.dev/sze/32/496_2.png) [@Sze](https://ziggit.dev/u/Sze)\
**Post date:** [June 8, 2026, 9:14pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/13 "2026-06-08T21:14:37Z")

</div>

> [@sbrow](#):
>
> `inline while (itr.next()) |_| : (i += 1) {`

Why do you use the iterator if you don’t use its value?  
If you just need the index I would use `inline for(0..fields.count())`

Can you show the code?

---

<div class="post-metadata">

**Author:** ![sbrow](https://ziggit.dev/user_avatar/ziggit.dev/sbrow/32/4970_2.png) [@sbrow](https://ziggit.dev/u/sbrow)\
**Post date:** [June 8, 2026, 9:38pm UTC](https://ziggit.dev/t/allocation-free-tui-tables/16009/14 "2026-06-08T21:38:08Z")

</div>

Thank your for your intellegent insight. You are indeed correct in that 2 of the loops in the `header` function don’t need the iterator, they can be simplified with an `inline for` loop. for the others, the code you suggested was missing a `comptime` in front of `itr.next()`. This let me get rid of all unnecessary loop steps! Thank you 🙏

```zig
comptime var itr = fields.iterator();
comptime var i: usize = 0;
inline while (comptime itr.next()) |c| : (i += 1) {
    _ = try writer.write(" ");
    try write_aligned(writer, @field(item, @tagName(c)), max_column_widths[i], .left);
    try writer.print(" {s}", .{sep});
}

```
