summaryrefslogtreecommitdiff
path: root/src/DisplayWidth.zig
diff options
context:
space:
mode:
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);