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 }