lib/pretty/core/src/compact/format.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const types = @import("types.zig");
3
4 const Column = types.Column;
5 pub const gutter = " ";
6 const ellipsis = "\xE2\x80\xA6";
7 const footer_dash = "\xE2\x80\x94";
8 pub const footer_tail = " " ++ footer_dash ++
9 " use --limit 0 for all, --json for exact records)\n";
10
11 pub fn maximumLineBytes(
12 columns: []const Column,
13 rows: []const []const []const u8,
14 ) error{CapacityOverflow}!usize {
15 var content_bytes: usize = 0;
16 for (columns, 0..) |column, column_index| {
17 var width = rawTruncatedLength(column.header, column.max_width);
18 for (rows) |row| {
19 const value = if (column_index < row.len) row[column_index] else "";
20 width = @max(width, normalizedTruncatedLength(value, column.max_width));
21 }
22 content_bytes = std.math.add(usize, content_bytes, width) catch
23 return error.CapacityOverflow;
24 }
25 const gutters = if (columns.len == 0) 0 else std.math.mul(
26 usize,
27 columns.len - 1,
28 gutter.len,
29 ) catch return error.CapacityOverflow;
30 const with_gutters = std.math.add(usize, content_bytes, gutters) catch
31 return error.CapacityOverflow;
32 return std.math.add(usize, with_gutters, 1) catch error.CapacityOverflow;
33 }
34
35 pub fn footerLength(displayed: usize, total: ?usize) error{CapacityOverflow}!usize {
36 const count = total orelse return 0;
37 if (count <= displayed) return 0;
38 var bytes = "(showing ".len;
39 bytes = std.math.add(usize, bytes, decimalLength(displayed)) catch
40 return error.CapacityOverflow;
41 bytes = std.math.add(usize, bytes, " of ".len) catch
42 return error.CapacityOverflow;
43 bytes = std.math.add(usize, bytes, decimalLength(count)) catch
44 return error.CapacityOverflow;
45 return std.math.add(usize, bytes, footer_tail.len) catch error.CapacityOverflow;
46 }
47
48 pub fn measureWidths(
49 widths: []usize,
50 columns: []const Column,
51 rows: []const []const []const u8,
52 ) void {
53 std.debug.assert(widths.len == columns.len);
54 for (columns, widths, 0..) |column, *width, column_index| {
55 width.* = rawTruncatedLength(column.header, column.max_width);
56 for (rows) |row| {
57 const value = if (column_index < row.len) row[column_index] else "";
58 width.* = @max(
59 width.*,
60 normalizedTruncatedLength(value, column.max_width),
61 );
62 }
63 }
64 }
65
66 pub fn renderInto(
67 writer: *std.Io.Writer,
68 widths: []const usize,
69 columns: []const Column,
70 rows: []const []const []const u8,
71 total: ?usize,
72 ) std.Io.Writer.Error!void {
73 try writeHeaderRow(writer, widths, columns);
74 try writeSeparator(writer, widths);
75 for (rows) |row| try writeDataRow(writer, widths, columns, row);
76 if (total) |count| {
77 if (count > rows.len) {
78 try writer.print(
79 "(showing {d} of {d}{s}",
80 .{ rows.len, count, footer_tail },
81 );
82 }
83 }
84 }
85
86 fn writeHeaderRow(
87 writer: *std.Io.Writer,
88 widths: []const usize,
89 columns: []const Column,
90 ) std.Io.Writer.Error!void {
91 std.debug.assert(widths.len == columns.len);
92 for (columns, 0..) |column, index| {
93 if (index != 0) try writer.writeAll(gutter);
94 const length = rawTruncatedLength(column.header, column.max_width);
95 if (column.alignment == .right) try writeSpaces(writer, widths[index] - length);
96 try writeLowerTruncated(writer, column.header, column.max_width);
97 if (column.alignment == .left and index + 1 != columns.len) {
98 try writeSpaces(writer, widths[index] - length);
99 }
100 }
101 try writer.writeByte('\n');
102 }
103
104 fn writeDataRow(
105 writer: *std.Io.Writer,
106 widths: []const usize,
107 columns: []const Column,
108 row: []const []const u8,
109 ) std.Io.Writer.Error!void {
110 std.debug.assert(widths.len == columns.len);
111 for (columns, 0..) |column, index| {
112 if (index != 0) try writer.writeAll(gutter);
113 const value = if (index < row.len) row[index] else "";
114 const length = normalizedTruncatedLength(value, column.max_width);
115 if (column.alignment == .right) try writeSpaces(writer, widths[index] - length);
116 try writeNormalizedTruncated(writer, value, column.max_width);
117 if (column.alignment == .left and index + 1 != columns.len) {
118 try writeSpaces(writer, widths[index] - length);
119 }
120 }
121 try writer.writeByte('\n');
122 }
123
124 fn writeSeparator(
125 writer: *std.Io.Writer,
126 widths: []const usize,
127 ) std.Io.Writer.Error!void {
128 for (widths, 0..) |width, index| {
129 if (index != 0) try writer.writeAll(gutter);
130 for (0..width) |_| try writer.writeByte('-');
131 }
132 try writer.writeByte('\n');
133 }
134
135 fn writeSpaces(writer: *std.Io.Writer, count: usize) std.Io.Writer.Error!void {
136 for (0..count) |_| try writer.writeByte(' ');
137 }
138
139 fn rawTruncatedLength(value: []const u8, maximum: usize) usize {
140 if (value.len <= maximum) return value.len;
141 if (maximum <= ellipsis.len) return @min(maximum, ellipsis.len);
142 var cut = maximum - ellipsis.len;
143 while (cut > 0 and isContinuation(value[cut])) cut -= 1;
144 return cut + ellipsis.len;
145 }
146
147 fn normalizedTruncatedLength(value: []const u8, maximum: usize) usize {
148 const length = normalizedLength(value);
149 if (length <= maximum) return length;
150 if (maximum <= ellipsis.len) return @min(maximum, ellipsis.len);
151 var cut = maximum - ellipsis.len;
152 while (cut > 0 and isContinuation(normalizedByteAt(value, cut))) cut -= 1;
153 return cut + ellipsis.len;
154 }
155
156 fn normalizedLength(value: []const u8) usize {
157 var length: usize = 0;
158 for (value) |byte| {
159 if (normalizedByte(byte) != null) length += 1;
160 }
161 return length;
162 }
163
164 fn normalizedByteAt(value: []const u8, target: usize) u8 {
165 var index: usize = 0;
166 for (value) |byte| {
167 const normalized = normalizedByte(byte) orelse continue;
168 if (index == target) return normalized;
169 index += 1;
170 }
171 unreachable;
172 }
173
174 fn normalizedByte(byte: u8) ?u8 {
175 return switch (byte) {
176 '\t', '\r', '\n' => ' ',
177 0...8, 11...12, 14...31, 127 => null,
178 else => byte,
179 };
180 }
181
182 fn writeLowerTruncated(
183 writer: *std.Io.Writer,
184 value: []const u8,
185 maximum: usize,
186 ) std.Io.Writer.Error!void {
187 const length = rawTruncatedLength(value, maximum);
188 if (value.len <= maximum) {
189 for (value) |byte| try writer.writeByte(std.ascii.toLower(byte));
190 return;
191 }
192 if (maximum <= ellipsis.len) return writer.writeAll(ellipsis[0..length]);
193 const prefix = length - ellipsis.len;
194 for (value[0..prefix]) |byte| try writer.writeByte(std.ascii.toLower(byte));
195 try writer.writeAll(ellipsis);
196 }
197
198 fn writeNormalizedTruncated(
199 writer: *std.Io.Writer,
200 value: []const u8,
201 maximum: usize,
202 ) std.Io.Writer.Error!void {
203 const length = normalizedTruncatedLength(value, maximum);
204 const normalized_length = normalizedLength(value);
205 if (normalized_length <= maximum) return writeNormalizedPrefix(writer, value, length);
206 if (maximum <= ellipsis.len) return writer.writeAll(ellipsis[0..length]);
207 try writeNormalizedPrefix(writer, value, length - ellipsis.len);
208 try writer.writeAll(ellipsis);
209 }
210
211 fn writeNormalizedPrefix(
212 writer: *std.Io.Writer,
213 value: []const u8,
214 count: usize,
215 ) std.Io.Writer.Error!void {
216 var written: usize = 0;
217 for (value) |byte| {
218 if (written == count) break;
219 const normalized = normalizedByte(byte) orelse continue;
220 try writer.writeByte(normalized);
221 written += 1;
222 }
223 std.debug.assert(written == count);
224 }
225
226 fn isContinuation(byte: u8) bool {
227 return byte & 0xc0 == 0x80;
228 }
229
230 fn decimalLength(value: usize) usize {
231 var remaining = value;
232 var digits: usize = 1;
233 while (remaining >= 10) : (remaining /= 10) digits += 1;
234 return digits;
235 }