lib/pretty/core/src/stream.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const render = @import("render.zig");
3 const types = @import("types.zig");
4
5 /// Adapts incremental text production to the Pretty renderer without allocating.
6 /// Flushes propagate to the wrapped writer so process-boundary ordering is preserved.
7 pub const TextWriter = struct {
8 writer: std.Io.Writer,
9 output: *std.Io.Writer,
10 options: types.LayoutOptions,
11
12 pub fn init(
13 output: *std.Io.Writer,
14 buffer: []u8,
15 options: types.LayoutOptions,
16 ) TextWriter {
17 return .{
18 .writer = .{
19 .vtable = &.{
20 .drain = drain,
21 .flush = flush,
22 },
23 .buffer = buffer,
24 },
25 .output = output,
26 .options = options,
27 };
28 }
29
30 fn drain(
31 writer: *std.Io.Writer,
32 data: []const []const u8,
33 splat: usize,
34 ) std.Io.Writer.Error!usize {
35 const self: *TextWriter = @alignCast(@fieldParentPtr("writer", writer));
36 std.debug.assert(data.len > 0);
37 try self.write(writer.buffered());
38 writer.end = 0;
39
40 var consumed: usize = 0;
41 for (data[0 .. data.len - 1]) |bytes| {
42 try self.write(bytes);
43 consumed += bytes.len;
44 }
45 const pattern = data[data.len - 1];
46 for (0..splat) |_| {
47 try self.write(pattern);
48 consumed += pattern.len;
49 }
50 return consumed;
51 }
52
53 fn flush(writer: *std.Io.Writer) std.Io.Writer.Error!void {
54 const self: *TextWriter = @alignCast(@fieldParentPtr("writer", writer));
55 try self.write(writer.buffered());
56 writer.end = 0;
57 try self.output.flush();
58 }
59
60 fn write(self: *TextWriter, bytes: []const u8) std.Io.Writer.Error!void {
61 render.write(
62 self.output,
63 .{ .text = bytes },
64 self.options,
65 ) catch return error.WriteFailed;
66 }
67 };
68
69 test "text writer routes incremental output through the renderer" {
70 var output: std.Io.Writer.Allocating = .init(std.testing.allocator);
71 defer output.deinit();
72 var buffer: [4]u8 = undefined;
73 var text = TextWriter.init(&output.writer, &buffer, .{ .width = 20 });
74 try text.writer.print("alpha {d}\n", .{7});
75 try text.writer.writeAll("omega");
76 try text.writer.flush();
77 try std.testing.expectEqualStrings("alpha 7\nomega", output.writer.buffered());
78 }
79
80 test "text writer preserves fragmented UTF-8 and terminal bytes" {
81 const expected = "{\"value\":\"λ\"}\n\x1b[31mred\x1b[0m\n";
82 var output: std.Io.Writer.Allocating = .init(std.testing.allocator);
83 defer output.deinit();
84 var buffer: [3]u8 = undefined;
85 var text = TextWriter.init(&output.writer, &buffer, .{ .width = 20 });
86 try text.writer.writeAll(expected);
87 try text.writer.flush();
88 try std.testing.expectEqualStrings(expected, output.writer.buffered());
89 }
90
91 test "text writer flush reaches its output" {
92 const Probe = struct {
93 writer: std.Io.Writer,
94 flushed: *bool,
95
96 fn init(buffer: []u8, flushed: *bool) @This() {
97 return .{
98 .writer = .{
99 .vtable = &.{
100 .drain = drain,
101 .flush = flush,
102 },
103 .buffer = buffer,
104 },
105 .flushed = flushed,
106 };
107 }
108
109 fn drain(
110 _: *std.Io.Writer,
111 _: []const []const u8,
112 _: usize,
113 ) std.Io.Writer.Error!usize {
114 return error.WriteFailed;
115 }
116
117 fn flush(writer: *std.Io.Writer) std.Io.Writer.Error!void {
118 const self: *@This() = @alignCast(@fieldParentPtr("writer", writer));
119 writer.end = 0;
120 self.flushed.* = true;
121 }
122 };
123
124 var flushed = false;
125 var output_buffer: [32]u8 = undefined;
126 var output = Probe.init(&output_buffer, &flushed);
127 var text_buffer: [8]u8 = undefined;
128 var text = TextWriter.init(&output.writer, &text_buffer, .{});
129 try text.writer.writeAll("alpha");
130 try text.writer.flush();
131 try std.testing.expect(flushed);
132 }