summaryrefslogtreecommitdiff
path: root/src/DisplayWidth.zig
diff options
context:
space:
mode:
authorGravatar Sam Atman2025-04-30 11:58:19 -0400
committerGravatar Sam Atman2025-04-30 11:58:19 -0400
commit1be5e46490e061761b4b97dff5c6acb2181d6fe9 (patch)
tree77a1edcdedd7afae7428e92feba37d2bb1035b22 /src/DisplayWidth.zig
parentAdd general tests step (diff)
downloadzg-1be5e46490e061761b4b97dff5c6acb2181d6fe9.tar.gz
zg-1be5e46490e061761b4b97dff5c6acb2181d6fe9.tar.xz
zg-1be5e46490e061761b4b97dff5c6acb2181d6fe9.zip
Factor out 'Data' for grapheme and DisplayWidth
In the process of refactoring the whole library, so that it doesn't expose anything called "Data" separately from user functionality.
Diffstat (limited to 'src/DisplayWidth.zig')
-rw-r--r--src/DisplayWidth.zig240
1 files changed, 164 insertions, 76 deletions
diff --git a/src/DisplayWidth.zig b/src/DisplayWidth.zig
index 8631bd4..11ec59e 100644
--- a/src/DisplayWidth.zig
+++ b/src/DisplayWidth.zig
@@ -2,38 +2,131 @@ const std = @import("std");
2const builtin = @import("builtin"); 2const builtin = @import("builtin");
3const options = @import("options"); 3const options = @import("options");
4const ArrayList = std.ArrayList; 4const ArrayList = std.ArrayList;
5const compress = std.compress;
5const mem = std.mem; 6const mem = std.mem;
6const simd = std.simd; 7const simd = std.simd;
7const testing = std.testing; 8const testing = std.testing;
8 9
9const ascii = @import("ascii"); 10const ascii = @import("ascii");
10const CodePointIterator = @import("code_point").Iterator; 11const CodePointIterator = @import("code_point").Iterator;
11const GraphemeIterator = @import("grapheme").Iterator;
12pub const DisplayWidthData = @import("DisplayWidthData"); 12pub const DisplayWidthData = @import("DisplayWidthData");
13 13
14data: *const DisplayWidthData, 14const Graphemes = @import("Graphemes");
15 15
16const Self = @This(); 16g_data: Graphemes,
17s1: []u16 = undefined,
18s2: []i4 = undefined,
19owns_gdata: bool,
20
21const DisplayWidth = @This();
22
23pub fn init(allocator: mem.Allocator) mem.Allocator.Error!DisplayWidth {
24 var dw: DisplayWidth = try DisplayWidth.setup(allocator);
25 errdefer {
26 allocator.free(dw.s1);
27 allocator.free(dw.s2);
28 }
29 dw.owns_gdata = true;
30 dw.g_data = try Graphemes.init(allocator);
31 errdefer dw.g_data.deinit(allocator);
32 return dw;
33}
34
35pub fn initWithGraphemeData(allocator: mem.Allocator, g_data: Graphemes) mem.Allocator.Error!DisplayWidth {
36 var dw = try DisplayWidth.setup(allocator);
37 dw.g_data = g_data;
38 dw.owns_gdata = false;
39 return dw;
40}
41
42// Sets up the DisplayWidthData, leaving the GraphemeData undefined.
43fn setup(allocator: mem.Allocator) mem.Allocator.Error!DisplayWidth {
44 const decompressor = compress.flate.inflate.decompressor;
45 const in_bytes = @embedFile("dwp");
46 var in_fbs = std.io.fixedBufferStream(in_bytes);
47 var in_decomp = decompressor(.raw, in_fbs.reader());
48 var reader = in_decomp.reader();
49
50 const endian = builtin.cpu.arch.endian();
51
52 var dw: DisplayWidth = undefined;
53
54 const stage_1_len: u16 = reader.readInt(u16, endian) catch unreachable;
55 dw.s1 = try allocator.alloc(u16, stage_1_len);
56 errdefer allocator.free(dw.s1);
57 for (0..stage_1_len) |i| dw.s1[i] = reader.readInt(u16, endian) catch unreachable;
58
59 const stage_2_len: u16 = reader.readInt(u16, endian) catch unreachable;
60 dw.s2 = try allocator.alloc(i4, stage_2_len);
61 errdefer allocator.free(dw.s2);
62 for (0..stage_2_len) |i| dw.s2[i] = @intCast(reader.readInt(i8, endian) catch unreachable);
63
64 return dw;
65}
66
67pub fn deinit(dw: *const DisplayWidth, allocator: mem.Allocator) void {
68 allocator.free(dw.s1);
69 allocator.free(dw.s2);
70 if (dw.owns_gdata) dw.g_data.deinit(allocator);
71}
72
73/// codePointWidth returns the number of cells `cp` requires when rendered
74/// in a fixed-pitch font (i.e. a terminal screen). This can range from -1 to
75/// 3, where BACKSPACE and DELETE return -1 and 3-em-dash returns 3. C0/C1
76/// control codes return 0. If `cjk` is true, ambiguous code points return 2,
77/// otherwise they return 1.
78pub fn codePointWidth(dw: DisplayWidth, cp: u21) i4 {
79 return dw.s2[dw.s1[cp >> 8] + (cp & 0xff)];
80}
81
82test "codePointWidth" {
83 const dw = try DisplayWidth.init(std.testing.allocator);
84 defer dw.deinit(std.testing.allocator);
85 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0000)); // null
86 try testing.expectEqual(@as(i4, -1), dw.codePointWidth(0x8)); // \b
87 try testing.expectEqual(@as(i4, -1), dw.codePointWidth(0x7f)); // DEL
88 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0005)); // Cf
89 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x0007)); // \a BEL
90 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000A)); // \n LF
91 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000B)); // \v VT
92 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000C)); // \f FF
93 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000D)); // \r CR
94 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000E)); // SQ
95 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x000F)); // SI
96
97 try testing.expectEqual(@as(i4, 0), dw.codePointWidth(0x070F)); // Cf
98 try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x0603)); // Cf Arabic
99
100 try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x00AD)); // soft-hyphen
101 try testing.expectEqual(@as(i4, 2), dw.codePointWidth(0x2E3A)); // two-em dash
102 try testing.expectEqual(@as(i4, 3), dw.codePointWidth(0x2E3B)); // three-em dash
103
104 try testing.expectEqual(@as(i4, 1), dw.codePointWidth(0x00BD)); // ambiguous halfwidth
105
106 try testing.expectEqual(@as(i4, 1), dw.codePointWidth('é'));
107 try testing.expectEqual(@as(i4, 2), dw.codePointWidth('😊'));
108 try testing.expectEqual(@as(i4, 2), dw.codePointWidth('统'));
109}
17 110
18/// strWidth returns the total display width of `str` as the number of cells 111/// strWidth returns the total display width of `str` as the number of cells
19/// required in a fixed-pitch font (i.e. a terminal screen). 112/// required in a fixed-pitch font (i.e. a terminal screen).
20pub fn strWidth(self: Self, str: []const u8) usize { 113pub fn strWidth(dw: DisplayWidth, str: []const u8) usize {
21 var total: isize = 0; 114 var total: isize = 0;
22 115
23 // ASCII fast path 116 // ASCII fast path
24 if (ascii.isAsciiOnly(str)) { 117 if (ascii.isAsciiOnly(str)) {
25 for (str) |b| total += self.data.codePointWidth(b); 118 for (str) |b| total += dw.codePointWidth(b);
26 return @intCast(@max(0, total)); 119 return @intCast(@max(0, total));
27 } 120 }
28 121
29 var giter = GraphemeIterator.init(str, &self.data.g_data); 122 var giter = dw.g_data.iterator(str);
30 123
31 while (giter.next()) |gc| { 124 while (giter.next()) |gc| {
32 var cp_iter = CodePointIterator{ .bytes = gc.bytes(str) }; 125 var cp_iter = CodePointIterator{ .bytes = gc.bytes(str) };
33 var gc_total: isize = 0; 126 var gc_total: isize = 0;
34 127
35 while (cp_iter.next()) |cp| { 128 while (cp_iter.next()) |cp| {
36 var w = self.data.codePointWidth(cp.code); 129 var w = dw.codePointWidth(cp.code);
37 130
38 if (w != 0) { 131 if (w != 0) {
39 // Handle text emoji sequence. 132 // Handle text emoji sequence.
@@ -58,41 +151,40 @@ pub fn strWidth(self: Self, str: []const u8) usize {
58} 151}
59 152
60test "strWidth" { 153test "strWidth" {
61 const data = try DisplayWidthData.init(testing.allocator); 154 const dw = try DisplayWidth.init(testing.allocator);
62 defer data.deinit(testing.allocator); 155 defer dw.deinit(testing.allocator);
63 const self = Self{ .data = &data };
64 const c0 = options.c0_width orelse 0; 156 const c0 = options.c0_width orelse 0;
65 157
66 try testing.expectEqual(@as(usize, 5), self.strWidth("Hello\r\n")); 158 try testing.expectEqual(@as(usize, 5), dw.strWidth("Hello\r\n"));
67 try testing.expectEqual(@as(usize, 1), self.strWidth("\u{0065}\u{0301}")); 159 try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{0065}\u{0301}"));
68 try testing.expectEqual(@as(usize, 2), self.strWidth("\u{1F476}\u{1F3FF}\u{0308}\u{200D}\u{1F476}\u{1F3FF}")); 160 try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{1F476}\u{1F3FF}\u{0308}\u{200D}\u{1F476}\u{1F3FF}"));
69 try testing.expectEqual(@as(usize, 8), self.strWidth("Hello 😊")); 161 try testing.expectEqual(@as(usize, 8), dw.strWidth("Hello 😊"));
70 try testing.expectEqual(@as(usize, 8), self.strWidth("Héllo 😊")); 162 try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo 😊"));
71 try testing.expectEqual(@as(usize, 8), self.strWidth("Héllo :)")); 163 try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo :)"));
72 try testing.expectEqual(@as(usize, 8), self.strWidth("Héllo 🇪🇸")); 164 try testing.expectEqual(@as(usize, 8), dw.strWidth("Héllo 🇪🇸"));
73 try testing.expectEqual(@as(usize, 2), self.strWidth("\u{26A1}")); // Lone emoji 165 try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{26A1}")); // Lone emoji
74 try testing.expectEqual(@as(usize, 1), self.strWidth("\u{26A1}\u{FE0E}")); // Text sequence 166 try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{26A1}\u{FE0E}")); // Text sequence
75 try testing.expectEqual(@as(usize, 2), self.strWidth("\u{26A1}\u{FE0F}")); // Presentation sequence 167 try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{26A1}\u{FE0F}")); // Presentation sequence
76 try testing.expectEqual(@as(usize, 1), self.strWidth("\u{2764}")); // Default text presentation 168 try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{2764}")); // Default text presentation
77 try testing.expectEqual(@as(usize, 1), self.strWidth("\u{2764}\u{FE0E}")); // Default text presentation with VS15 selector 169 try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{2764}\u{FE0E}")); // Default text presentation with VS15 selector
78 try testing.expectEqual(@as(usize, 2), self.strWidth("\u{2764}\u{FE0F}")); // Default text presentation with VS16 selector 170 try testing.expectEqual(@as(usize, 2), dw.strWidth("\u{2764}\u{FE0F}")); // Default text presentation with VS16 selector
79 const expect_bs: usize = if (c0 == 0) 0 else 1 + c0; 171 const expect_bs: usize = if (c0 == 0) 0 else 1 + c0;
80 try testing.expectEqual(expect_bs, self.strWidth("A\x08")); // Backspace 172 try testing.expectEqual(expect_bs, dw.strWidth("A\x08")); // Backspace
81 try testing.expectEqual(expect_bs, self.strWidth("\x7FA")); // DEL 173 try testing.expectEqual(expect_bs, dw.strWidth("\x7FA")); // DEL
82 const expect_long_del: usize = if (c0 == 0) 0 else 1 + (c0 * 3); 174 const expect_long_del: usize = if (c0 == 0) 0 else 1 + (c0 * 3);
83 try testing.expectEqual(expect_long_del, self.strWidth("\x7FA\x08\x08")); // never less than 0 175 try testing.expectEqual(expect_long_del, dw.strWidth("\x7FA\x08\x08")); // never less than 0
84 176
85 // wcwidth Python lib tests. See: https://github.com/jquast/wcwidth/blob/master/tests/test_core.py 177 // wcwidth Python lib tests. See: https://github.com/jquast/wcwidth/blob/master/tests/test_core.py
86 const empty = ""; 178 const empty = "";
87 try testing.expectEqual(@as(usize, 0), self.strWidth(empty)); 179 try testing.expectEqual(@as(usize, 0), dw.strWidth(empty));
88 const with_null = "hello\x00world"; 180 const with_null = "hello\x00world";
89 try testing.expectEqual(@as(usize, 10 + c0), self.strWidth(with_null)); 181 try testing.expectEqual(@as(usize, 10 + c0), dw.strWidth(with_null));
90 const hello_jp = "コンニチハ, セカイ!"; 182 const hello_jp = "コンニチハ, セカイ!";
91 try testing.expectEqual(@as(usize, 19), self.strWidth(hello_jp)); 183 try testing.expectEqual(@as(usize, 19), dw.strWidth(hello_jp));
92 const control = "\x1b[0m"; 184 const control = "\x1b[0m";
93 try testing.expectEqual(@as(usize, 3 + c0), self.strWidth(control)); 185 try testing.expectEqual(@as(usize, 3 + c0), dw.strWidth(control));
94 const balinese = "\u{1B13}\u{1B28}\u{1B2E}\u{1B44}"; 186 const balinese = "\u{1B13}\u{1B28}\u{1B2E}\u{1B44}";
95 try testing.expectEqual(@as(usize, 3), self.strWidth(balinese)); 187 try testing.expectEqual(@as(usize, 3), dw.strWidth(balinese));
96 188
97 // These commented out tests require a new specification for complex scripts. 189 // These commented out tests require a new specification for complex scripts.
98 // See: https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdf 190 // See: https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdf
@@ -106,17 +198,17 @@ test "strWidth" {
106 // try testing.expectEqual(@as(usize, 3), strWidth(kannada_1)); 198 // try testing.expectEqual(@as(usize, 3), strWidth(kannada_1));
107 // The following passes but as a mere coincidence. 199 // The following passes but as a mere coincidence.
108 const kannada_2 = "\u{0cb0}\u{0cbc}\u{0ccd}\u{0c9a}"; 200 const kannada_2 = "\u{0cb0}\u{0cbc}\u{0ccd}\u{0c9a}";
109 try testing.expectEqual(@as(usize, 2), self.strWidth(kannada_2)); 201 try testing.expectEqual(@as(usize, 2), dw.strWidth(kannada_2));
110 202
111 // From Rust https://github.com/jameslanska/unicode-display-width 203 // From Rust https://github.com/jameslanska/unicode-display-width
112 try testing.expectEqual(@as(usize, 15), self.strWidth("🔥🗡🍩👩🏻‍🚀⏰💃🏼🔦👍🏻")); 204 try testing.expectEqual(@as(usize, 15), dw.strWidth("🔥🗡🍩👩🏻‍🚀⏰💃🏼🔦👍🏻"));
113 try testing.expectEqual(@as(usize, 2), self.strWidth("🦀")); 205 try testing.expectEqual(@as(usize, 2), dw.strWidth("🦀"));
114 try testing.expectEqual(@as(usize, 2), self.strWidth("👨‍👩‍👧‍👧")); 206 try testing.expectEqual(@as(usize, 2), dw.strWidth("👨‍👩‍👧‍👧"));
115 try testing.expectEqual(@as(usize, 2), self.strWidth("👩‍🔬")); 207 try testing.expectEqual(@as(usize, 2), dw.strWidth("👩‍🔬"));
116 try testing.expectEqual(@as(usize, 9), self.strWidth("sane text")); 208 try testing.expectEqual(@as(usize, 9), dw.strWidth("sane text"));
117 try testing.expectEqual(@as(usize, 9), self.strWidth("Ẓ̌á̲l͔̝̞̄̑͌g̖̘̘̔̔͢͞͝o̪̔T̢̙̫̈̍͞e̬͈͕͌̏͑x̺̍ṭ̓̓ͅ")); 209 try testing.expectEqual(@as(usize, 9), dw.strWidth("Ẓ̌á̲l͔̝̞̄̑͌g̖̘̘̔̔͢͞͝o̪̔T̢̙̫̈̍͞e̬͈͕͌̏͑x̺̍ṭ̓̓ͅ"));
118 try testing.expectEqual(@as(usize, 17), self.strWidth("슬라바 우크라이나")); 210 try testing.expectEqual(@as(usize, 17), dw.strWidth("슬라바 우크라이나"));
119 try testing.expectEqual(@as(usize, 1), self.strWidth("\u{378}")); 211 try testing.expectEqual(@as(usize, 1), dw.strWidth("\u{378}"));
120} 212}
121 213
122/// centers `str` in a new string of width `total_width` (in display cells) using `pad` as padding. 214/// centers `str` in a new string of width `total_width` (in display cells) using `pad` as padding.
@@ -124,17 +216,17 @@ test "strWidth" {
124/// receive one additional pad. This makes sure the returned string fills the requested width. 216/// receive one additional pad. This makes sure the returned string fills the requested width.
125/// Caller must free returned bytes with `allocator`. 217/// Caller must free returned bytes with `allocator`.
126pub fn center( 218pub fn center(
127 self: Self, 219 dw: DisplayWidth,
128 allocator: mem.Allocator, 220 allocator: mem.Allocator,
129 str: []const u8, 221 str: []const u8,
130 total_width: usize, 222 total_width: usize,
131 pad: []const u8, 223 pad: []const u8,
132) ![]u8 { 224) ![]u8 {
133 const str_width = self.strWidth(str); 225 const str_width = dw.strWidth(str);
134 if (str_width > total_width) return error.StrTooLong; 226 if (str_width > total_width) return error.StrTooLong;
135 if (str_width == total_width) return try allocator.dupe(u8, str); 227 if (str_width == total_width) return try allocator.dupe(u8, str);
136 228
137 const pad_width = self.strWidth(pad); 229 const pad_width = dw.strWidth(pad);
138 if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; 230 if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong;
139 231
140 const margin_width = @divFloor((total_width - str_width), 2); 232 const margin_width = @divFloor((total_width - str_width), 2);
@@ -165,62 +257,61 @@ pub fn center(
165 257
166test "center" { 258test "center" {
167 const allocator = testing.allocator; 259 const allocator = testing.allocator;
168 const data = try DisplayWidthData.init(allocator); 260 const dw = try DisplayWidth.init(allocator);
169 defer data.deinit(allocator); 261 defer dw.deinit(allocator);
170 const self = Self{ .data = &data };
171 262
172 // Input and width both have odd length 263 // Input and width both have odd length
173 var centered = try self.center(allocator, "abc", 9, "*"); 264 var centered = try dw.center(allocator, "abc", 9, "*");
174 try testing.expectEqualSlices(u8, "***abc***", centered); 265 try testing.expectEqualSlices(u8, "***abc***", centered);
175 266
176 // Input and width both have even length 267 // Input and width both have even length
177 testing.allocator.free(centered); 268 testing.allocator.free(centered);
178 centered = try self.center(allocator, "w😊w", 10, "-"); 269 centered = try dw.center(allocator, "w😊w", 10, "-");
179 try testing.expectEqualSlices(u8, "---w😊w---", centered); 270 try testing.expectEqualSlices(u8, "---w😊w---", centered);
180 271
181 // Input has even length, width has odd length 272 // Input has even length, width has odd length
182 testing.allocator.free(centered); 273 testing.allocator.free(centered);
183 centered = try self.center(allocator, "1234", 9, "-"); 274 centered = try dw.center(allocator, "1234", 9, "-");
184 try testing.expectEqualSlices(u8, "--1234---", centered); 275 try testing.expectEqualSlices(u8, "--1234---", centered);
185 276
186 // Input has odd length, width has even length 277 // Input has odd length, width has even length
187 testing.allocator.free(centered); 278 testing.allocator.free(centered);
188 centered = try self.center(allocator, "123", 8, "-"); 279 centered = try dw.center(allocator, "123", 8, "-");
189 try testing.expectEqualSlices(u8, "--123---", centered); 280 try testing.expectEqualSlices(u8, "--123---", centered);
190 281
191 // Input is the same length as the width 282 // Input is the same length as the width
192 testing.allocator.free(centered); 283 testing.allocator.free(centered);
193 centered = try self.center(allocator, "123", 3, "-"); 284 centered = try dw.center(allocator, "123", 3, "-");
194 try testing.expectEqualSlices(u8, "123", centered); 285 try testing.expectEqualSlices(u8, "123", centered);
195 286
196 // Input is empty 287 // Input is empty
197 testing.allocator.free(centered); 288 testing.allocator.free(centered);
198 centered = try self.center(allocator, "", 3, "-"); 289 centered = try dw.center(allocator, "", 3, "-");
199 try testing.expectEqualSlices(u8, "---", centered); 290 try testing.expectEqualSlices(u8, "---", centered);
200 291
201 // Input is empty and width is zero 292 // Input is empty and width is zero
202 testing.allocator.free(centered); 293 testing.allocator.free(centered);
203 centered = try self.center(allocator, "", 0, "-"); 294 centered = try dw.center(allocator, "", 0, "-");
204 try testing.expectEqualSlices(u8, "", centered); 295 try testing.expectEqualSlices(u8, "", centered);
205 296
206 // Input is longer than the width, which is an error 297 // Input is longer than the width, which is an error
207 testing.allocator.free(centered); 298 testing.allocator.free(centered);
208 try testing.expectError(error.StrTooLong, self.center(allocator, "123", 2, "-")); 299 try testing.expectError(error.StrTooLong, dw.center(allocator, "123", 2, "-"));
209} 300}
210 301
211/// padLeft returns a new string of width `total_width` (in display cells) using `pad` as padding 302/// padLeft returns a new string of width `total_width` (in display cells) using `pad` as padding
212/// on the left side. Caller must free returned bytes with `allocator`. 303/// on the left side. Caller must free returned bytes with `allocator`.
213pub fn padLeft( 304pub fn padLeft(
214 self: Self, 305 dw: DisplayWidth,
215 allocator: mem.Allocator, 306 allocator: mem.Allocator,
216 str: []const u8, 307 str: []const u8,
217 total_width: usize, 308 total_width: usize,
218 pad: []const u8, 309 pad: []const u8,
219) ![]u8 { 310) ![]u8 {
220 const str_width = self.strWidth(str); 311 const str_width = dw.strWidth(str);
221 if (str_width > total_width) return error.StrTooLong; 312 if (str_width > total_width) return error.StrTooLong;
222 313
223 const pad_width = self.strWidth(pad); 314 const pad_width = dw.strWidth(pad);
224 if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; 315 if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong;
225 316
226 const margin_width = total_width - str_width; 317 const margin_width = total_width - str_width;
@@ -244,32 +335,31 @@ pub fn padLeft(
244 335
245test "padLeft" { 336test "padLeft" {
246 const allocator = testing.allocator; 337 const allocator = testing.allocator;
247 const data = try DisplayWidthData.init(allocator); 338 const dw = try DisplayWidth.init(allocator);
248 defer data.deinit(allocator); 339 defer dw.deinit(allocator);
249 const self = Self{ .data = &data };
250 340
251 var right_aligned = try self.padLeft(allocator, "abc", 9, "*"); 341 var right_aligned = try dw.padLeft(allocator, "abc", 9, "*");
252 defer testing.allocator.free(right_aligned); 342 defer testing.allocator.free(right_aligned);
253 try testing.expectEqualSlices(u8, "******abc", right_aligned); 343 try testing.expectEqualSlices(u8, "******abc", right_aligned);
254 344
255 testing.allocator.free(right_aligned); 345 testing.allocator.free(right_aligned);
256 right_aligned = try self.padLeft(allocator, "w😊w", 10, "-"); 346 right_aligned = try dw.padLeft(allocator, "w😊w", 10, "-");
257 try testing.expectEqualSlices(u8, "------w😊w", right_aligned); 347 try testing.expectEqualSlices(u8, "------w😊w", right_aligned);
258} 348}
259 349
260/// padRight returns a new string of width `total_width` (in display cells) using `pad` as padding 350/// padRight returns a new string of width `total_width` (in display cells) using `pad` as padding
261/// on the right side. Caller must free returned bytes with `allocator`. 351/// on the right side. Caller must free returned bytes with `allocator`.
262pub fn padRight( 352pub fn padRight(
263 self: Self, 353 dw: DisplayWidth,
264 allocator: mem.Allocator, 354 allocator: mem.Allocator,
265 str: []const u8, 355 str: []const u8,
266 total_width: usize, 356 total_width: usize,
267 pad: []const u8, 357 pad: []const u8,
268) ![]u8 { 358) ![]u8 {
269 const str_width = self.strWidth(str); 359 const str_width = dw.strWidth(str);
270 if (str_width > total_width) return error.StrTooLong; 360 if (str_width > total_width) return error.StrTooLong;
271 361
272 const pad_width = self.strWidth(pad); 362 const pad_width = dw.strWidth(pad);
273 if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong; 363 if (pad_width > total_width or str_width + pad_width > total_width) return error.PadTooLong;
274 364
275 const margin_width = total_width - str_width; 365 const margin_width = total_width - str_width;
@@ -294,16 +384,15 @@ pub fn padRight(
294 384
295test "padRight" { 385test "padRight" {
296 const allocator = testing.allocator; 386 const allocator = testing.allocator;
297 const data = try DisplayWidthData.init(allocator); 387 const dw = try DisplayWidth.init(allocator);
298 defer data.deinit(allocator); 388 defer dw.deinit(allocator);
299 const self = Self{ .data = &data };
300 389
301 var left_aligned = try self.padRight(allocator, "abc", 9, "*"); 390 var left_aligned = try dw.padRight(allocator, "abc", 9, "*");
302 defer testing.allocator.free(left_aligned); 391 defer testing.allocator.free(left_aligned);
303 try testing.expectEqualSlices(u8, "abc******", left_aligned); 392 try testing.expectEqualSlices(u8, "abc******", left_aligned);
304 393
305 testing.allocator.free(left_aligned); 394 testing.allocator.free(left_aligned);
306 left_aligned = try self.padRight(allocator, "w😊w", 10, "-"); 395 left_aligned = try dw.padRight(allocator, "w😊w", 10, "-");
307 try testing.expectEqualSlices(u8, "w😊w------", left_aligned); 396 try testing.expectEqualSlices(u8, "w😊w------", left_aligned);
308} 397}
309 398
@@ -311,7 +400,7 @@ test "padRight" {
311/// `threshold` defines how far the last column of the last word can be 400/// `threshold` defines how far the last column of the last word can be
312/// from the edge. Caller must free returned bytes with `allocator`. 401/// from the edge. Caller must free returned bytes with `allocator`.
313pub fn wrap( 402pub fn wrap(
314 self: Self, 403 dw: DisplayWidth,
315 allocator: mem.Allocator, 404 allocator: mem.Allocator,
316 str: []const u8, 405 str: []const u8,
317 columns: usize, 406 columns: usize,
@@ -329,7 +418,7 @@ pub fn wrap(
329 while (word_iter.next()) |word| { 418 while (word_iter.next()) |word| {
330 try result.appendSlice(word); 419 try result.appendSlice(word);
331 try result.append(' '); 420 try result.append(' ');
332 line_width += self.strWidth(word) + 1; 421 line_width += dw.strWidth(word) + 1;
333 422
334 if (line_width > columns or columns - line_width <= threshold) { 423 if (line_width > columns or columns - line_width <= threshold) {
335 try result.append('\n'); 424 try result.append('\n');
@@ -347,12 +436,11 @@ pub fn wrap(
347 436
348test "wrap" { 437test "wrap" {
349 const allocator = testing.allocator; 438 const allocator = testing.allocator;
350 const data = try DisplayWidthData.init(allocator); 439 const dw = try DisplayWidth.init(allocator);
351 defer data.deinit(allocator); 440 defer dw.deinit(allocator);
352 const self = Self{ .data = &data };
353 441
354 const input = "The quick brown fox\r\njumped over the lazy dog!"; 442 const input = "The quick brown fox\r\njumped over the lazy dog!";
355 const got = try self.wrap(allocator, input, 10, 3); 443 const got = try dw.wrap(allocator, input, 10, 3);
356 defer testing.allocator.free(got); 444 defer testing.allocator.free(got);
357 const want = "The quick \nbrown fox \njumped \nover the \nlazy dog!"; 445 const want = "The quick \nbrown fox \njumped \nover the \nlazy dog!";
358 try testing.expectEqualStrings(want, got); 446 try testing.expectEqualStrings(want, got);