A pretty printer for zig
zig
1const std = @import("std");
2const Io = std.Io;
3const Type = std.builtin.Type;
4
5pub fn pretty(value: anytype) Pretty(@TypeOf(value)) {
6 return Pretty(@TypeOf(value)).init(value);
7}
8
9pub fn prettyO(value: anytype, comptime opts: Options) PrettyWithOptions(@TypeOf(value), opts) {
10 return PrettyWithOptions(@TypeOf(value), opts).init(value);
11}
12
13const root = @import("root");
14const default_options: Options = if (@hasDecl(root, "pretty_options"))
15 root.pretty_options
16else
17 Options{};
18
19pub const Options = struct {
20 /// Disable pretty printing.
21 ///
22 /// If enabled, will format values using `{any}`
23 disable: bool = false,
24
25 /// Show type names
26 show_type_names: bool = true,
27 /// Don't show type names for the root value
28 skip_root_type_name: bool = false,
29
30 /// Disable color output
31 no_color: bool = false,
32 /// Customize the theme, change colors or separators
33 theme: Theme = .{},
34
35 /// Inline print arrays
36 array_inline: bool = false,
37 /// Always show array indices, even in inline mode
38 array_always_show_index: bool = false,
39
40 /// Treat pointers to `[]u8` / `[]const u8` as strings
41 ptr_array_u8_as_string: bool = true,
42 /// Treat pointers to a u8 slice as strings
43 ptr_slice_u8_as_string: bool = true,
44 /// Inline print slices
45 ptr_slice_inline: bool = false,
46 /// Always show slice indices, even in inline mode
47 ptr_slice_always_show_index: bool = false,
48
49 /// Inline print structs
50 struct_inline: bool = false,
51 /// Highlight tag type of unions
52 union_highlight_tag: bool = true,
53};
54
55pub const Theme = struct {
56 pub const Color = enum {
57 dim,
58 field,
59 value,
60 @"error",
61 type,
62 string,
63 many_ptr,
64 null,
65 true,
66 false,
67 };
68
69 indent_width: comptime_int = 2,
70
71 field_name_type_sep: []const u8 = ": ",
72 type_value_sep: []const u8 = " = ",
73 index_value_sep: []const u8 = " = ",
74
75 index_open: []const u8 = "[",
76 index_close: []const u8 = "]",
77
78 array_open: []const u8 = "{",
79 array_close: []const u8 = "}",
80
81 vec_open: []const u8 = "(",
82 vec_close: []const u8 = ")",
83
84 color_dim: Io.Terminal.Color = .dim,
85 color_field: Io.Terminal.Color = .green,
86 color_value: Io.Terminal.Color = .blue,
87 color_error: Io.Terminal.Color = .red,
88 color_type: Io.Terminal.Color = .bright_blue,
89 color_string: Io.Terminal.Color = .magenta,
90 color_many_ptr: Io.Terminal.Color = .magenta,
91 color_null: Io.Terminal.Color = .cyan,
92 color_true: Io.Terminal.Color = .bright_green,
93 color_false: Io.Terminal.Color = .bright_red,
94
95 pub inline fn getColor(comptime this: Theme, comptime color: Color) Io.Terminal.Color {
96 return @field(this, "color_" ++ @tagName(color));
97 }
98};
99
100pub fn Pretty(comptime T: type) type {
101 return PrettyWithOptions(T, default_options);
102}
103
104pub fn PrettyWithOptions(comptime T: type, comptime options: Options) type {
105 if (default_options.disable) return struct {
106 value: T,
107
108 pub fn init(val: T) @This() {
109 return .{ .value = val };
110 }
111
112 pub fn format(this: *const @This(), w: *std.Io.Writer) error{WriteFailed}!void {
113 try w.print("{any}", .{this.value});
114 }
115 };
116
117 const global = struct {
118 var tty: ?Io.Terminal = null;
119 };
120 const ctx: Context = Context{ .options = options };
121
122 return struct {
123 value: T,
124
125 pub fn init(val: T) @This() {
126 return .{ .value = val };
127 }
128
129 pub fn format(this: *const @This(), w: *std.Io.Writer) error{WriteFailed}!void {
130 if (global.tty == null) {
131 var buffer: [1]u8 = undefined;
132 const stderr = std.debug.lockStderr(&buffer).terminal();
133 defer std.debug.unlockStderr();
134
135 global.tty = stderr;
136 global.tty.?.writer = w;
137 }
138
139 var run = Runtime{
140 .out = w,
141 .tty = global.tty.?,
142 .no_color = options.no_color,
143 };
144
145 return run.pretty(ctx, this.value, .{});
146 }
147 };
148}
149
150pub const Runtime = struct {
151 out: *Io.Writer,
152 tty: Io.Terminal,
153
154 no_color: bool = default_options.no_color,
155
156 pub inline fn print(
157 this: *const Runtime,
158 comptime fmt: []const u8,
159 args: anytype,
160 ) error{WriteFailed}!void {
161 return this.out.print(fmt, args);
162 }
163
164 pub inline fn write(this: *const Runtime, text: []const u8) error{WriteFailed}!void {
165 _ = try this.out.write(text);
166 }
167
168 pub fn pretty(
169 this: *const Runtime,
170 comptime ctx: Context,
171 value: anytype,
172 comptime opts: InnerFmtOptions,
173 ) error{WriteFailed}!void {
174 return innerFmt(@TypeOf(value), ctx, this, value, opts);
175 }
176
177 pub inline fn setColor(
178 this: *const Runtime,
179 comptime ctx: Context,
180 comptime color: Theme.Color,
181 ) void {
182 if (this.no_color or default_options.no_color) return;
183 this.tty.setColor(comptime ctx.options.theme.getColor(color)) catch {};
184 }
185
186 pub inline fn setColorRaw(this: *const Runtime, color: Io.Terminal.Color) void {
187 if (this.no_color or default_options.no_color) return;
188 this.tty.setColor(color) catch {};
189 }
190
191 pub inline fn resetColor(this: *const Runtime) void {
192 if (this.no_color or default_options.no_color) return;
193 this.tty.setColor(.reset) catch {};
194 }
195};
196
197pub const Context = struct {
198 depth: comptime_int = 0,
199
200 options: Options,
201 exited_comptime: bool = false,
202
203 pub fn inComptime(comptime this: Context) bool {
204 if (!this.exited_comptime) return false;
205 return @inComptime();
206 }
207};
208
209pub const InnerFmtOptions = struct {
210 skip_type_name: bool = !default_options.show_type_names,
211};
212
213fn innerFmt(
214 comptime T: type,
215 comptime ctx: Context,
216 run: *const Runtime,
217 value: T,
218 comptime opts: InnerFmtOptions,
219) error{WriteFailed}!void {
220 const info = @typeInfo(T);
221
222 if (!opts.skip_type_name)
223 try printType(T, ctx, run, value);
224
225 if (std.meta.hasMethod(T, "pretty")) {
226 return value.pretty(ctx, run);
227 }
228
229 return switch (info) {
230 .bool => formatBool(ctx, run, value),
231 .null => formatNull(ctx, run),
232 .type => formatType(ctx, run, value),
233
234 // comptime types
235 .comptime_int,
236 .comptime_float,
237 // number types
238 .int,
239 .float,
240 // enum types
241 .@"enum",
242 .enum_literal,
243 => formatValue(ctx, run, value),
244
245 .optional => |opt| formatOptional(opt.child, ctx, run, value),
246 .@"struct" => |st| formatStruct(T, ctx, st, run, value),
247 .@"union" => formatUnion(T, ctx, run, value),
248
249 .error_set => formatErrorSet(ctx, run, value),
250 .error_union => formatErrorUnion(ctx, run, value),
251
252 .array => |arr| formatArray(ctx, arr, run, value),
253 .vector => |vec| formatVector(ctx, vec, run, value),
254
255 .@"fn" => formatFn(T, ctx, run),
256
257 .pointer => |ptr| switch (ptr.size) {
258 .one => formatPtrOne(ctx, ptr, run, value),
259 .slice => formatPtrSlice(ctx, ptr, run, value),
260 .many => formatPtrMany(ctx, ptr, run, value),
261 else => {
262 run.setColor(ctx, .@"error");
263 try run.print("Unimplemented! ({} = {any})", .{ info, value });
264 run.resetColor();
265 },
266 },
267
268 else => {
269 run.setColor(ctx, .@"error");
270 try run.print("Unimplemented! ({} = {any})", .{ info, value });
271 run.resetColor();
272 },
273 };
274}
275
276inline fn printType(
277 comptime T: type,
278 comptime ctx: Context,
279 run: *const Runtime,
280 value: T,
281) error{WriteFailed}!void {
282 const active_type = comptime std.meta.activeTag(@typeInfo(T));
283 const excluded = comptime switch (active_type) {
284 .void, .noreturn, .undefined, .null, .@"fn" => true,
285 else => false,
286 };
287
288 if ((ctx.depth != 0 or !ctx.options.skip_root_type_name) and !excluded) {
289 run.setColor(ctx, .dim);
290
291 if (ctx.options.show_type_names) {
292 try run.write(@typeName(T));
293
294 if (active_type == .@"union") {
295 if (ctx.options.union_highlight_tag) {
296 run.resetColor();
297 run.setColor(ctx, .field);
298 }
299
300 try run.print("{}", .{std.meta.activeTag(value)});
301
302 if (ctx.options.union_highlight_tag) {
303 run.resetColor();
304 run.setColor(ctx, .dim);
305 }
306 }
307 }
308 try run.write(ctx.options.theme.type_value_sep);
309
310 run.resetColor();
311 }
312}
313
314inline fn formatBool(
315 comptime ctx: Context,
316 run: *const Runtime,
317 value: bool,
318) !void {
319 if (value)
320 run.setColor(ctx, .true)
321 else
322 run.setColor(ctx, .false);
323
324 try run.print("{}", .{value});
325 run.resetColor();
326}
327
328inline fn formatNull(comptime ctx: Context, run: *const Runtime) !void {
329 run.setColor(ctx, .null);
330 try run.write("null");
331 run.resetColor();
332}
333
334inline fn formatType(
335 comptime ctx: Context,
336 run: *const Runtime,
337 value: type,
338) !void {
339 run.setColor(ctx, .type);
340 run.setColorRaw(.bold);
341 try run.write(@typeName(value));
342 run.resetColor();
343}
344
345inline fn formatValue(
346 comptime ctx: Context,
347 run: *const Runtime,
348 value: anytype,
349) !void {
350 run.setColor(ctx, .value);
351 try run.print("{}", .{value});
352 run.resetColor();
353}
354
355inline fn formatOptional(
356 comptime T: type,
357 comptime ctx: Context,
358 run: *const Runtime,
359 value: ?T,
360) !void {
361 return if (value) |val|
362 innerFmt(T, ctx, run, val, .{ .skip_type_name = true })
363 else
364 formatNull(ctx, run);
365}
366
367inline fn formatStruct(
368 comptime T: type,
369 comptime ctx: Context,
370 comptime st: Type.Struct,
371 run: *const Runtime,
372 value: T,
373) !void {
374 const next_ctx = Context{
375 .depth = ctx.depth + 1,
376 .exited_comptime = ctx.exited_comptime,
377 .options = ctx.options,
378 };
379
380 if (ctx.options.struct_inline) {
381 run.setColor(ctx, .dim);
382 try run.write(".{ ");
383 run.resetColor();
384 }
385
386 comptime var index = 0;
387 inline for (st.fields) |field| {
388 indent(next_ctx, next_ctx.options.struct_inline, run);
389 if (index != 0 and ctx.options.struct_inline) {
390 run.setColor(ctx, .dim);
391 try run.write(", ");
392 run.resetColor();
393 }
394
395 run.setColor(ctx, .field);
396 try run.write("." ++ field.name ++
397 ctx.options.theme.field_name_type_sep);
398 run.resetColor();
399
400 try innerFmt(field.type, next_ctx, run, @field(value, field.name), .{});
401
402 index += 1;
403 }
404
405 if (ctx.options.struct_inline) {
406 run.setColor(ctx, .dim);
407 try run.write(" }");
408 run.resetColor();
409 }
410}
411
412inline fn formatUnion(
413 comptime T: type,
414 comptime ctx: Context,
415 run: *const Runtime,
416 value: T,
417) !void {
418 switch (value) {
419 inline else => |val| try innerFmt(@TypeOf(val), ctx, run, val, .{ .skip_type_name = true }),
420 }
421}
422
423inline fn formatArray(
424 comptime ctx: Context,
425 comptime arr: Type.Array,
426 run: *const Runtime,
427 value: anytype,
428) !void {
429 if (arr.len == 0) {
430 try run.write(ctx.options.theme.array_open);
431 try run.write(ctx.options.theme.array_close);
432 return;
433 }
434
435 const next_ctx = Context{
436 .depth = ctx.depth + 1,
437 .exited_comptime = ctx.exited_comptime,
438 .options = ctx.options,
439 };
440
441 if (ctx.options.array_inline) try arrayOpen(ctx, run);
442
443 comptime var index = 0;
444 inline for (value) |val| {
445 indent(
446 next_ctx,
447 next_ctx.options.array_inline,
448 run,
449 );
450
451 run.setColor(ctx, .dim);
452 if (index != 0 and ctx.options.array_inline)
453 try run.write(", ");
454
455 if (!ctx.options.array_inline or ctx.options.array_always_show_index) {
456 try run.write(ctx.options.theme.index_open);
457 try run.print("{}", .{index});
458 try run.write(ctx.options.theme.index_close ++
459 ctx.options.theme.index_value_sep);
460 }
461
462 run.resetColor();
463
464 try innerFmt(arr.child, next_ctx, run, val, .{ .skip_type_name = true });
465
466 index += 1;
467 }
468
469 if (ctx.options.array_inline) try arrayClose(ctx, run);
470}
471
472inline fn formatVector(
473 comptime ctx: Context,
474 comptime vec: Type.Vector,
475 run: *const Runtime,
476 value: @Vector(vec.len, vec.child),
477) !void {
478 run.setColor(ctx, .dim);
479 try run.write(ctx.options.theme.vec_open ++ " ");
480 run.resetColor();
481
482 inline for (0..vec.len) |idx| {
483 if (idx != 0) {
484 run.setColor(ctx, .dim);
485 try run.write(", ");
486 run.resetColor();
487 }
488
489 try innerFmt(vec.child, ctx, run, value[idx], .{ .skip_type_name = true });
490 }
491
492 run.setColor(ctx, .dim);
493 try run.write(" " ++ ctx.options.theme.vec_close);
494 run.resetColor();
495}
496
497inline fn formatPtrOne(
498 comptime ctx: Context,
499 comptime ptr: Type.Pointer,
500 run: *const Runtime,
501 value: anytype,
502) !void {
503 const is_string = switch (@typeInfo(ptr.child)) {
504 .array => |arr| arr.child == u8 and ctx.options.ptr_array_u8_as_string,
505 else => false,
506 };
507 if (is_string) return formatString(ctx, run, value);
508
509 try innerFmt(ptr.child, ctx, run, value.*, .{ .skip_type_name = true });
510}
511
512inline fn formatPtrSlice(
513 comptime ctx: Context,
514 comptime ptr: Type.Pointer,
515 run: *const Runtime,
516 value: anytype,
517) !void {
518 if (ptr.child == u8 and ctx.options.ptr_slice_u8_as_string)
519 return formatString(ctx, run, value);
520
521 const next_ctx = Context{
522 .depth = if (!ctx.options.ptr_slice_inline) ctx.depth + 1 else ctx.depth,
523 .exited_comptime = ctx.exited_comptime,
524 .options = ctx.options,
525 };
526
527 if (ctx.options.ptr_slice_inline) try arrayOpen(ctx, run);
528
529 var count: usize = 0;
530 for (value, 0..) |val, idx| {
531 count += 1;
532
533 indent(
534 next_ctx,
535 next_ctx.options.ptr_slice_inline,
536 run,
537 );
538
539 run.setColor(ctx, .dim);
540 if (idx != 0 and ctx.options.ptr_slice_inline)
541 try run.write(", ");
542
543 if (!ctx.options.ptr_slice_inline or ctx.options.ptr_slice_always_show_index) {
544 try run.write(ctx.options.theme.index_open);
545 try run.print("{}", .{idx});
546 try run.write(ctx.options.theme.index_close ++
547 ctx.options.theme.index_value_sep);
548 }
549
550 run.resetColor();
551
552 try innerFmt(ptr.child, next_ctx, run, val, .{ .skip_type_name = true });
553 }
554
555 if (count == 0) {
556 run.setColor(ctx, .dim);
557 try run.write(ctx.options.theme.array_open);
558 try run.write(ctx.options.theme.array_close);
559 run.resetColor();
560 }
561
562 if (ctx.options.ptr_slice_inline) try arrayClose(ctx, run);
563}
564
565inline fn formatPtrMany(
566 comptime ctx: Context,
567 comptime ptr: Type.Pointer,
568 run: *const Runtime,
569 value: anytype,
570) !void {
571 _ = ptr;
572 run.setColor(ctx, .dim);
573 run.setColor(ctx, .many_ptr);
574 try run.print("{*}", .{value});
575 run.resetColor();
576}
577
578inline fn formatString(
579 comptime ctx: Context,
580 run: *const Runtime,
581 value: anytype,
582) !void {
583 run.setColor(ctx, .string);
584 try run.write("\"");
585 try run.write(value);
586 try run.write("\"");
587 run.resetColor();
588}
589
590inline fn formatErrorSet(
591 comptime ctx: Context,
592 run: *const Runtime,
593 value: anyerror,
594) !void {
595 run.setColor(ctx, .@"error");
596 try run.write("error.");
597 try run.write(@errorName(value));
598 run.resetColor();
599}
600
601inline fn formatErrorUnion(
602 comptime ctx: Context,
603 run: *const Runtime,
604 value: anytype,
605) !void {
606 const val = value catch |err| return formatErrorSet(ctx, run, err);
607 return innerFmt(
608 @TypeOf(val),
609 ctx,
610 run,
611 val,
612 .{ .skip_type_name = true },
613 );
614}
615
616inline fn formatFn(
617 comptime T: type,
618 comptime ctx: Context,
619 run: *const Runtime,
620) !void {
621 run.setColor(ctx, .dim);
622 run.setColor(ctx, .type);
623 run.setColorRaw(.bold);
624
625 try run.write(@typeName(T));
626
627 run.resetColor();
628}
629
630inline fn indent(
631 comptime ctx: Context,
632 comptime inline_option: bool,
633 run: *const Runtime,
634) void {
635 if (inline_option) return;
636
637 const text: [ctx.depth * ctx.options.theme.indent_width]u8 = @splat(' ');
638 run.write("\n" ++ text) catch {};
639}
640
641fn arrayOpen(comptime ctx: Context, run: *const Runtime) !void {
642 run.setColor(ctx, .dim);
643 try run.write(ctx.options.theme.array_open ++ " ");
644 run.resetColor();
645}
646
647fn arrayClose(comptime ctx: Context, run: *const Runtime) !void {
648 run.setColor(ctx, .dim);
649 try run.write(" " ++ ctx.options.theme.array_close);
650 run.resetColor();
651}