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 force_inline: bool = false,
212};
213
214fn innerFmt(
215 comptime T: type,
216 comptime ctx: Context,
217 run: *const Runtime,
218 value: T,
219 comptime opts: InnerFmtOptions,
220) error{WriteFailed}!void {
221 const info = @typeInfo(T);
222
223 if (!opts.skip_type_name)
224 try printType(T, ctx, run, value);
225
226 if (ctx.options.struct_inline or opts.force_inline) {
227 if (std.meta.hasMethod(T, "prettyInline")) {
228 return value.prettyInline(ctx, run);
229 }
230 } else {
231 if (std.meta.hasMethod(T, "pretty")) {
232 return value.pretty(ctx, run);
233 }
234 }
235
236 return switch (info) {
237 .bool => formatBool(ctx, run, value),
238 .null => formatNull(ctx, run),
239 .type => formatType(ctx, run, value),
240
241 // comptime types
242 .comptime_int,
243 .comptime_float,
244 // number types
245 .int,
246 .float,
247 // enum types
248 .@"enum",
249 .enum_literal,
250 => formatValue(ctx, run, value),
251
252 .optional => |opt| formatOptional(opt.child, ctx, run, value),
253 .@"struct" => |st| formatStruct(T, ctx, st, run, value),
254 .@"union" => formatUnion(T, ctx, run, value),
255
256 .error_set => formatErrorSet(ctx, run, value),
257 .error_union => formatErrorUnion(ctx, run, value),
258
259 .array => |arr| formatArray(ctx, arr, run, value),
260 .vector => |vec| formatVector(ctx, vec, run, value),
261
262 .@"fn" => formatFn(T, ctx, run),
263
264 .pointer => |ptr| switch (ptr.size) {
265 .one => formatPtrOne(ctx, ptr, run, value),
266 .slice => formatPtrSlice(ctx, ptr, run, value),
267 .many => formatPtrMany(ctx, ptr, run, value),
268 else => {
269 run.setColor(ctx, .@"error");
270 try run.print("Unimplemented! ({} = {any})", .{ info, value });
271 run.resetColor();
272 },
273 },
274
275 else => {
276 run.setColor(ctx, .@"error");
277 try run.print("Unimplemented! ({} = {any})", .{ info, value });
278 run.resetColor();
279 },
280 };
281}
282
283inline fn printType(
284 comptime T: type,
285 comptime ctx: Context,
286 run: *const Runtime,
287 value: T,
288) error{WriteFailed}!void {
289 const active_type = comptime std.meta.activeTag(@typeInfo(T));
290 const excluded = comptime switch (active_type) {
291 .void, .noreturn, .undefined, .null, .@"fn" => true,
292 else => false,
293 };
294
295 if ((ctx.depth != 0 or !ctx.options.skip_root_type_name) and !excluded) {
296 run.setColor(ctx, .dim);
297
298 if (ctx.options.show_type_names) {
299 try run.write(@typeName(T));
300
301 if (active_type == .@"union") {
302 if (ctx.options.union_highlight_tag) {
303 run.resetColor();
304 run.setColor(ctx, .field);
305 }
306
307 try run.print("{}", .{std.meta.activeTag(value)});
308
309 if (ctx.options.union_highlight_tag) {
310 run.resetColor();
311 run.setColor(ctx, .dim);
312 }
313 }
314 }
315 try run.write(ctx.options.theme.type_value_sep);
316
317 run.resetColor();
318 }
319}
320
321inline fn formatBool(
322 comptime ctx: Context,
323 run: *const Runtime,
324 value: bool,
325) !void {
326 if (value)
327 run.setColor(ctx, .true)
328 else
329 run.setColor(ctx, .false);
330
331 try run.print("{}", .{value});
332 run.resetColor();
333}
334
335inline fn formatNull(comptime ctx: Context, run: *const Runtime) !void {
336 run.setColor(ctx, .null);
337 try run.write("null");
338 run.resetColor();
339}
340
341inline fn formatType(
342 comptime ctx: Context,
343 run: *const Runtime,
344 value: type,
345) !void {
346 run.setColor(ctx, .type);
347 run.setColorRaw(.bold);
348 try run.write(@typeName(value));
349 run.resetColor();
350}
351
352inline fn formatValue(
353 comptime ctx: Context,
354 run: *const Runtime,
355 value: anytype,
356) !void {
357 run.setColor(ctx, .value);
358 try run.print("{}", .{value});
359 run.resetColor();
360}
361
362inline fn formatOptional(
363 comptime T: type,
364 comptime ctx: Context,
365 run: *const Runtime,
366 value: ?T,
367) !void {
368 return if (value) |val|
369 innerFmt(T, ctx, run, val, .{ .skip_type_name = true })
370 else
371 formatNull(ctx, run);
372}
373
374inline fn formatStruct(
375 comptime T: type,
376 comptime ctx: Context,
377 comptime st: Type.Struct,
378 run: *const Runtime,
379 value: T,
380) !void {
381 const next_ctx = Context{
382 .depth = ctx.depth + 1,
383 .exited_comptime = ctx.exited_comptime,
384 .options = ctx.options,
385 };
386
387 if (ctx.options.struct_inline) {
388 run.setColor(ctx, .dim);
389 try run.write(".{ ");
390 run.resetColor();
391 }
392
393 comptime var index = 0;
394 inline for (st.fields) |field| {
395 indent(next_ctx, next_ctx.options.struct_inline, run);
396 if (index != 0 and ctx.options.struct_inline) {
397 run.setColor(ctx, .dim);
398 try run.write(", ");
399 run.resetColor();
400 }
401
402 run.setColor(ctx, .field);
403 try run.write("." ++ field.name ++
404 ctx.options.theme.field_name_type_sep);
405 run.resetColor();
406
407 try innerFmt(field.type, next_ctx, run, @field(value, field.name), .{});
408
409 index += 1;
410 }
411
412 if (ctx.options.struct_inline) {
413 run.setColor(ctx, .dim);
414 try run.write(" }");
415 run.resetColor();
416 }
417}
418
419inline fn formatUnion(
420 comptime T: type,
421 comptime ctx: Context,
422 run: *const Runtime,
423 value: T,
424) !void {
425 switch (value) {
426 inline else => |val| try innerFmt(@TypeOf(val), ctx, run, val, .{ .skip_type_name = true }),
427 }
428}
429
430inline fn formatArray(
431 comptime ctx: Context,
432 comptime arr: Type.Array,
433 run: *const Runtime,
434 value: anytype,
435) !void {
436 if (arr.len == 0) {
437 try run.write(ctx.options.theme.array_open);
438 try run.write(ctx.options.theme.array_close);
439 return;
440 }
441
442 const next_ctx = Context{
443 .depth = ctx.depth + 1,
444 .exited_comptime = ctx.exited_comptime,
445 .options = ctx.options,
446 };
447
448 if (ctx.options.array_inline) try arrayOpen(ctx, run);
449
450 comptime var index = 0;
451 inline for (value) |val| {
452 indent(
453 next_ctx,
454 next_ctx.options.array_inline,
455 run,
456 );
457
458 run.setColor(ctx, .dim);
459 if (index != 0 and ctx.options.array_inline)
460 try run.write(", ");
461
462 if (!ctx.options.array_inline or ctx.options.array_always_show_index) {
463 try run.write(ctx.options.theme.index_open);
464 try run.print("{}", .{index});
465 try run.write(ctx.options.theme.index_close ++
466 ctx.options.theme.index_value_sep);
467 }
468
469 run.resetColor();
470
471 try innerFmt(arr.child, next_ctx, run, val, .{ .skip_type_name = true });
472
473 index += 1;
474 }
475
476 if (ctx.options.array_inline) try arrayClose(ctx, run);
477}
478
479inline fn formatVector(
480 comptime ctx: Context,
481 comptime vec: Type.Vector,
482 run: *const Runtime,
483 value: @Vector(vec.len, vec.child),
484) !void {
485 run.setColor(ctx, .dim);
486 try run.write(ctx.options.theme.vec_open ++ " ");
487 run.resetColor();
488
489 inline for (0..vec.len) |idx| {
490 if (idx != 0) {
491 run.setColor(ctx, .dim);
492 try run.write(", ");
493 run.resetColor();
494 }
495
496 try innerFmt(vec.child, ctx, run, value[idx], .{ .skip_type_name = true });
497 }
498
499 run.setColor(ctx, .dim);
500 try run.write(" " ++ ctx.options.theme.vec_close);
501 run.resetColor();
502}
503
504inline fn formatPtrOne(
505 comptime ctx: Context,
506 comptime ptr: Type.Pointer,
507 run: *const Runtime,
508 value: anytype,
509) !void {
510 const is_string = switch (@typeInfo(ptr.child)) {
511 .array => |arr| arr.child == u8 and ctx.options.ptr_array_u8_as_string,
512 else => false,
513 };
514 if (is_string) return formatString(ctx, run, value);
515
516 try innerFmt(ptr.child, ctx, run, value.*, .{ .skip_type_name = true });
517}
518
519inline fn formatPtrSlice(
520 comptime ctx: Context,
521 comptime ptr: Type.Pointer,
522 run: *const Runtime,
523 value: anytype,
524) !void {
525 if (ptr.child == u8 and ctx.options.ptr_slice_u8_as_string)
526 return formatString(ctx, run, value);
527
528 const next_ctx = Context{
529 .depth = if (!ctx.options.ptr_slice_inline) ctx.depth + 1 else ctx.depth,
530 .exited_comptime = ctx.exited_comptime,
531 .options = ctx.options,
532 };
533
534 if (ctx.options.ptr_slice_inline) try arrayOpen(ctx, run);
535
536 var count: usize = 0;
537 for (value, 0..) |val, idx| {
538 count += 1;
539
540 indent(
541 next_ctx,
542 next_ctx.options.ptr_slice_inline,
543 run,
544 );
545
546 run.setColor(ctx, .dim);
547 if (idx != 0 and ctx.options.ptr_slice_inline)
548 try run.write(", ");
549
550 if (!ctx.options.ptr_slice_inline or ctx.options.ptr_slice_always_show_index) {
551 try run.write(ctx.options.theme.index_open);
552 try run.print("{}", .{idx});
553 try run.write(ctx.options.theme.index_close ++
554 ctx.options.theme.index_value_sep);
555 }
556
557 run.resetColor();
558
559 try innerFmt(ptr.child, next_ctx, run, val, .{ .skip_type_name = true });
560 }
561
562 if (count == 0) {
563 run.setColor(ctx, .dim);
564 try run.write(ctx.options.theme.array_open);
565 try run.write(ctx.options.theme.array_close);
566 run.resetColor();
567 }
568
569 if (ctx.options.ptr_slice_inline) try arrayClose(ctx, run);
570}
571
572inline fn formatPtrMany(
573 comptime ctx: Context,
574 comptime ptr: Type.Pointer,
575 run: *const Runtime,
576 value: anytype,
577) !void {
578 _ = ptr;
579 run.setColor(ctx, .dim);
580 run.setColor(ctx, .many_ptr);
581 try run.print("{*}", .{value});
582 run.resetColor();
583}
584
585inline fn formatString(
586 comptime ctx: Context,
587 run: *const Runtime,
588 value: anytype,
589) !void {
590 run.setColor(ctx, .string);
591 try run.write("\"");
592 try run.write(value);
593 try run.write("\"");
594 run.resetColor();
595}
596
597inline fn formatErrorSet(
598 comptime ctx: Context,
599 run: *const Runtime,
600 value: anyerror,
601) !void {
602 run.setColor(ctx, .@"error");
603 try run.write("error.");
604 try run.write(@errorName(value));
605 run.resetColor();
606}
607
608inline fn formatErrorUnion(
609 comptime ctx: Context,
610 run: *const Runtime,
611 value: anytype,
612) !void {
613 const val = value catch |err| return formatErrorSet(ctx, run, err);
614 return innerFmt(
615 @TypeOf(val),
616 ctx,
617 run,
618 val,
619 .{ .skip_type_name = true },
620 );
621}
622
623inline fn formatFn(
624 comptime T: type,
625 comptime ctx: Context,
626 run: *const Runtime,
627) !void {
628 run.setColor(ctx, .dim);
629 run.setColor(ctx, .type);
630 run.setColorRaw(.bold);
631
632 try run.write(@typeName(T));
633
634 run.resetColor();
635}
636
637inline fn indent(
638 comptime ctx: Context,
639 comptime inline_option: bool,
640 run: *const Runtime,
641) void {
642 if (inline_option) return;
643
644 const text: [ctx.depth * ctx.options.theme.indent_width]u8 = @splat(' ');
645 run.write("\n" ++ text) catch {};
646}
647
648fn arrayOpen(comptime ctx: Context, run: *const Runtime) !void {
649 run.setColor(ctx, .dim);
650 try run.write(ctx.options.theme.array_open ++ " ");
651 run.resetColor();
652}
653
654fn arrayClose(comptime ctx: Context, run: *const Runtime) !void {
655 run.setColor(ctx, .dim);
656 try run.write(" " ++ ctx.options.theme.array_close);
657 run.resetColor();
658}