a modern tui library written in zig
1const std = @import("std");
2
3const Screen = @import("Screen.zig");
4const Cell = @import("Cell.zig");
5const Mouse = @import("Mouse.zig");
6const Segment = @import("Cell.zig").Segment;
7const unicode = @import("unicode.zig");
8const gw = @import("gwidth.zig");
9
10const Window = @This();
11
12/// absolute horizontal offset from the screen
13x_off: i17,
14/// absolute vertical offset from the screen
15y_off: i17,
16/// relative horizontal offset, from parent window. This only accumulates if it is negative so that
17/// we can clip the window correctly
18parent_x_off: i17,
19/// relative vertical offset, from parent window. This only accumulates if it is negative so that
20/// we can clip the window correctly
21parent_y_off: i17,
22/// width of the window. This can't be larger than the terminal screen
23width: u16,
24/// height of the window. This can't be larger than the terminal screen
25height: u16,
26
27screen: *Screen,
28
29/// Creates a new window with offset relative to parent and size clamped to the
30/// parent's size. Windows do not retain a reference to their parent and are
31/// unaware of resizes.
32fn initChild(
33 self: Window,
34 x_off: i17,
35 y_off: i17,
36 maybe_width: ?u16,
37 maybe_height: ?u16,
38) Window {
39 const max_height = @max(self.height - y_off, 0);
40 const max_width = @max(self.width - x_off, 0);
41 const width: u16 = maybe_width orelse max_width;
42 const height: u16 = maybe_height orelse max_height;
43
44 return Window{
45 .x_off = x_off + self.x_off,
46 .y_off = y_off + self.y_off,
47 .parent_x_off = @min(self.parent_x_off + x_off, 0),
48 .parent_y_off = @min(self.parent_y_off + y_off, 0),
49 .width = @min(width, max_width),
50 .height = @min(height, max_height),
51 .screen = self.screen,
52 };
53}
54
55pub const ChildOptions = struct {
56 x_off: i17 = 0,
57 y_off: i17 = 0,
58 /// the width of the resulting child, including any borders
59 width: ?u16 = null,
60 /// the height of the resulting child, including any borders
61 height: ?u16 = null,
62 border: BorderOptions = .{},
63};
64
65pub const BorderOptions = struct {
66 style: Cell.Style = .{},
67 where: union(enum) {
68 none,
69 all,
70 top,
71 right,
72 bottom,
73 left,
74 other: Locations,
75 } = .none,
76 glyphs: Glyphs = .single_rounded,
77
78 pub const Locations = packed struct {
79 top: bool = false,
80 right: bool = false,
81 bottom: bool = false,
82 left: bool = false,
83 };
84
85 pub const Glyphs = union(enum) {
86 single_rounded,
87 single_square,
88 /// custom border glyphs. each glyph should be one cell wide and the
89 /// following indices apply:
90 /// [0] = top left
91 /// [1] = horizontal
92 /// [2] = top right
93 /// [3] = vertical
94 /// [4] = bottom right
95 /// [5] = bottom left
96 custom: [6][]const u8,
97 };
98
99 const single_rounded: [6][]const u8 = .{ "╭", "─", "╮", "│", "╯", "╰" };
100 const single_square: [6][]const u8 = .{ "┌", "─", "┐", "│", "┘", "└" };
101};
102
103/// create a child window
104pub fn child(self: Window, opts: ChildOptions) Window {
105 var result = self.initChild(opts.x_off, opts.y_off, opts.width, opts.height);
106
107 const glyphs = switch (opts.border.glyphs) {
108 .single_rounded => BorderOptions.single_rounded,
109 .single_square => BorderOptions.single_square,
110 .custom => |custom| custom,
111 };
112
113 const top_left: Cell.Character = .{ .grapheme = glyphs[0], .width = 1 };
114 const horizontal: Cell.Character = .{ .grapheme = glyphs[1], .width = 1 };
115 const top_right: Cell.Character = .{ .grapheme = glyphs[2], .width = 1 };
116 const vertical: Cell.Character = .{ .grapheme = glyphs[3], .width = 1 };
117 const bottom_right: Cell.Character = .{ .grapheme = glyphs[4], .width = 1 };
118 const bottom_left: Cell.Character = .{ .grapheme = glyphs[5], .width = 1 };
119 const style = opts.border.style;
120
121 const h = result.height;
122 const w = result.width;
123
124 const loc: BorderOptions.Locations = switch (opts.border.where) {
125 .none => return result,
126 .all => .{ .top = true, .bottom = true, .right = true, .left = true },
127 .bottom => .{ .bottom = true },
128 .right => .{ .right = true },
129 .left => .{ .left = true },
130 .top => .{ .top = true },
131 .other => |loc| loc,
132 };
133 if (loc.top) {
134 var i: u16 = 0;
135 while (i < w) : (i += 1) {
136 result.writeCell(i, 0, .{ .char = horizontal, .style = style });
137 }
138 }
139 if (loc.bottom) {
140 var i: u16 = 0;
141 while (i < w) : (i += 1) {
142 result.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
143 }
144 }
145 if (loc.left) {
146 var i: u16 = 0;
147 while (i < h) : (i += 1) {
148 result.writeCell(0, i, .{ .char = vertical, .style = style });
149 }
150 }
151 if (loc.right) {
152 var i: u16 = 0;
153 while (i < h) : (i += 1) {
154 result.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
155 }
156 }
157 // draw corners
158 if (loc.top and loc.left)
159 result.writeCell(0, 0, .{ .char = top_left, .style = style });
160 if (loc.top and loc.right)
161 result.writeCell(w -| 1, 0, .{ .char = top_right, .style = style });
162 if (loc.bottom and loc.left)
163 result.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
164 if (loc.bottom and loc.right)
165 result.writeCell(w -| 1, h -| 1, .{ .char = bottom_right, .style = style });
166
167 const x_off: u16 = if (loc.left) 1 else 0;
168 const y_off: u16 = if (loc.top) 1 else 0;
169 const h_delt: u16 = if (loc.bottom) 1 else 0;
170 const w_delt: u16 = if (loc.right) 1 else 0;
171 const h_ch: u16 = h -| y_off -| h_delt;
172 const w_ch: u16 = w -| x_off -| w_delt;
173 return result.initChild(x_off, y_off, w_ch, h_ch);
174}
175
176/// writes a cell to the location in the window
177pub fn writeCell(self: Window, col: u16, row: u16, cell: Cell) void {
178 if (self.height <= row or
179 self.width <= col or
180 self.x_off + col < 0 or
181 self.y_off + row < 0 or
182 self.parent_x_off + col < 0 or
183 self.parent_y_off + row < 0)
184 return;
185
186 self.screen.writeCell(@intCast(col + self.x_off), @intCast(row + self.y_off), cell);
187}
188
189/// reads a cell at the location in the window
190pub fn readCell(self: Window, col: u16, row: u16) ?Cell {
191 if (self.height <= row or
192 self.width <= col or
193 self.x_off + col < 0 or
194 self.y_off + row < 0 or
195 self.parent_x_off + col < 0 or
196 self.parent_y_off + row < 0)
197 return null;
198 return self.screen.readCell(@intCast(col + self.x_off), @intCast(row + self.y_off));
199}
200
201/// fills the window with the default cell
202pub fn clear(self: Window) void {
203 self.fill(.{ .default = true });
204}
205
206/// returns the width of the grapheme. This depends on the terminal capabilities
207pub fn gwidth(self: Window, str: []const u8) u16 {
208 return gw.gwidth(str, self.screen.width_method);
209}
210
211/// fills the window with the provided cell
212pub fn fill(self: Window, cell: Cell) void {
213 if (self.x_off + self.width < 0 or
214 self.y_off + self.height < 0 or
215 self.screen.width < self.x_off or
216 self.screen.height < self.y_off)
217 return;
218 const first_row: usize = @intCast(@max(self.y_off, 0));
219 if (self.x_off == 0 and self.width == self.screen.width) {
220 // we have a full width window, therefore contiguous memory.
221 const start = @min(first_row * self.width, self.screen.buf.len);
222 const end = @min(start + (@as(usize, @intCast(self.height)) * self.width), self.screen.buf.len);
223 @memset(self.screen.buf[start..end], cell);
224 } else {
225 // Non-contiguous. Iterate over rows an memset
226 var row: usize = first_row;
227 const first_col: usize = @max(self.x_off, 0);
228 const last_row = @min(self.height + self.y_off, self.screen.height);
229 while (row < last_row) : (row += 1) {
230 const start = @min(first_col + (row * self.screen.width), self.screen.buf.len);
231 var end = @min(start + self.width, start + (self.screen.width - first_col));
232 end = @min(end, self.screen.buf.len);
233 @memset(self.screen.buf[start..end], cell);
234 }
235 }
236}
237
238/// hide the cursor
239pub fn hideCursor(self: Window) void {
240 self.screen.cursor_vis = false;
241}
242
243/// show the cursor at the given coordinates, 0 indexed
244pub fn showCursor(self: Window, col: u16, row: u16) void {
245 if (self.x_off + col < 0 or
246 self.y_off + row < 0 or
247 row >= self.height or
248 col >= self.width)
249 return;
250 self.screen.cursor_vis = true;
251 self.screen.cursor_row = @intCast(row + self.y_off);
252 self.screen.cursor_col = @intCast(col + self.x_off);
253}
254
255pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
256 self.screen.cursor_shape = shape;
257}
258
259/// Options to use when printing Segments to a window
260pub const PrintOptions = struct {
261 /// vertical offset to start printing at
262 row_offset: u16 = 0,
263 /// horizontal offset to start printing at
264 col_offset: u16 = 0,
265
266 /// wrap behavior for printing
267 wrap: enum {
268 /// wrap at grapheme boundaries
269 grapheme,
270 /// wrap at word boundaries
271 word,
272 /// stop printing after one line
273 none,
274 } = .grapheme,
275
276 /// when true, print will write to the screen for rendering. When false,
277 /// nothing is written. The return value describes the size of the wrapped
278 /// text
279 commit: bool = true,
280};
281
282pub const PrintResult = struct {
283 col: u16,
284 row: u16,
285 overflow: bool,
286};
287
288/// prints segments to the window. returns true if the text overflowed with the
289/// given wrap strategy and size.
290pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) PrintResult {
291 var row = opts.row_offset;
292 switch (opts.wrap) {
293 .grapheme => {
294 var col: u16 = opts.col_offset;
295 const overflow: bool = blk: for (segments) |segment| {
296 var iter = unicode.graphemeIterator(segment.text);
297 while (iter.next()) |grapheme| {
298 if (col >= self.width) {
299 row += 1;
300 col = 0;
301 }
302 if (row >= self.height) break :blk true;
303 const s = grapheme.bytes(segment.text);
304 if (std.mem.eql(u8, s, "\n")) {
305 row +|= 1;
306 col = 0;
307 continue;
308 }
309 const w = self.gwidth(s);
310 if (w == 0) continue;
311 if (opts.commit) self.writeCell(col, row, .{
312 .char = .{
313 .grapheme = s,
314 .width = @intCast(w),
315 },
316 .style = segment.style,
317 .link = segment.link,
318 .wrapped = col + w >= self.width,
319 });
320 col += w;
321 }
322 } else false;
323 if (col >= self.width) {
324 row += 1;
325 col = 0;
326 }
327 return .{
328 .row = row,
329 .col = col,
330 .overflow = overflow,
331 };
332 },
333 .word => {
334 var col: u16 = opts.col_offset;
335 var overflow: bool = false;
336 var soft_wrapped: bool = false;
337 outer: for (segments) |segment| {
338 var line_iter: LineIterator = .{ .buf = segment.text };
339 while (line_iter.next()) |line| {
340 defer {
341 // We only set soft_wrapped to false if a segment actually contains a linebreak
342 if (line_iter.has_break) {
343 soft_wrapped = false;
344 row += 1;
345 col = 0;
346 }
347 }
348 var iter: WhitespaceTokenizer = .{ .buf = line };
349 while (iter.next()) |token| {
350 switch (token) {
351 .whitespace => |len| {
352 if (soft_wrapped) continue;
353 for (0..len) |_| {
354 if (col >= self.width) {
355 col = 0;
356 row += 1;
357 break;
358 }
359 if (opts.commit) {
360 self.writeCell(col, row, .{
361 .char = .{
362 .grapheme = " ",
363 .width = 1,
364 },
365 .style = segment.style,
366 .link = segment.link,
367 });
368 }
369 col += 1;
370 }
371 },
372 .word => |word| {
373 const width = self.gwidth(word);
374 if (width + col > self.width and width < self.width) {
375 row += 1;
376 col = 0;
377 }
378
379 var grapheme_iterator = unicode.graphemeIterator(word);
380 while (grapheme_iterator.next()) |grapheme| {
381 soft_wrapped = false;
382 if (row >= self.height) {
383 overflow = true;
384 break :outer;
385 }
386 const s = grapheme.bytes(word);
387 const w = self.gwidth(s);
388 if (opts.commit) self.writeCell(col, row, .{
389 .char = .{
390 .grapheme = s,
391 .width = @intCast(w),
392 },
393 .style = segment.style,
394 .link = segment.link,
395 });
396 col += w;
397 if (col >= self.width) {
398 row += 1;
399 col = 0;
400 soft_wrapped = true;
401 }
402 }
403 },
404 }
405 }
406 }
407 }
408 return .{
409 // remove last row counter
410 .row = row,
411 .col = col,
412 .overflow = overflow,
413 };
414 },
415 .none => {
416 var col: u16 = opts.col_offset;
417 const overflow: bool = blk: for (segments) |segment| {
418 var iter = unicode.graphemeIterator(segment.text);
419 while (iter.next()) |grapheme| {
420 if (col >= self.width) break :blk true;
421 const s = grapheme.bytes(segment.text);
422 if (std.mem.eql(u8, s, "\n")) break :blk true;
423 const w = self.gwidth(s);
424 if (w == 0) continue;
425 if (opts.commit) self.writeCell(col, row, .{
426 .char = .{
427 .grapheme = s,
428 .width = @intCast(w),
429 },
430 .style = segment.style,
431 .link = segment.link,
432 });
433 col +|= w;
434 }
435 } else false;
436 return .{
437 .row = row,
438 .col = col,
439 .overflow = overflow,
440 };
441 },
442 }
443 return false;
444}
445
446/// print a single segment. This is just a shortcut for print(&.{segment}, opts)
447pub fn printSegment(self: Window, segment: Segment, opts: PrintOptions) PrintResult {
448 return self.print(&.{segment}, opts);
449}
450
451/// scrolls the window down one row (IE inserts a blank row at the bottom of the
452/// screen and shifts all rows up one)
453pub fn scroll(self: Window, n: u16) void {
454 if (n > self.height) return;
455 var row: u16 = @max(self.y_off, 0);
456 const first_col: u16 = @max(self.x_off, 0);
457 while (row < self.height - n) : (row += 1) {
458 const dst_start = (row * self.screen.width) + first_col;
459 const dst_end = dst_start + self.width;
460
461 const src_start = ((row + n) * self.screen.width) + first_col;
462 const src_end = src_start + self.width;
463 @memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
464 }
465 const last_row = self.child(.{
466 .y_off = self.height - n,
467 });
468 last_row.clear();
469}
470
471/// returns the mouse event if the mouse event occurred within the window. If
472/// the mouse event occurred outside the window, null is returned
473pub fn hasMouse(win: Window, mouse: ?Mouse) ?Mouse {
474 const event = mouse orelse return null;
475 if (event.col >= win.x_off and
476 event.col < (win.x_off + win.width) and
477 event.row >= win.y_off and
478 event.row < (win.y_off + win.height)) return event else return null;
479}
480
481test "Window size set" {
482 var parent = Window{
483 .x_off = 0,
484 .y_off = 0,
485 .parent_x_off = 0,
486 .parent_y_off = 0,
487 .width = 20,
488 .height = 20,
489 .screen = undefined,
490 };
491
492 const ch = parent.initChild(1, 1, null, null);
493 try std.testing.expectEqual(19, ch.width);
494 try std.testing.expectEqual(19, ch.height);
495}
496
497test "Window size set too big" {
498 var parent = Window{
499 .x_off = 0,
500 .y_off = 0,
501 .parent_x_off = 0,
502 .parent_y_off = 0,
503 .width = 20,
504 .height = 20,
505 .screen = undefined,
506 };
507
508 const ch = parent.initChild(0, 0, 21, 21);
509 try std.testing.expectEqual(20, ch.width);
510 try std.testing.expectEqual(20, ch.height);
511}
512
513test "Window size set too big with offset" {
514 var parent = Window{
515 .x_off = 0,
516 .y_off = 0,
517 .parent_x_off = 0,
518 .parent_y_off = 0,
519 .width = 20,
520 .height = 20,
521 .screen = undefined,
522 };
523
524 const ch = parent.initChild(10, 10, 21, 21);
525 try std.testing.expectEqual(10, ch.width);
526 try std.testing.expectEqual(10, ch.height);
527}
528
529test "Window size nested offsets" {
530 var parent = Window{
531 .x_off = 1,
532 .y_off = 1,
533 .parent_x_off = 0,
534 .parent_y_off = 0,
535 .width = 20,
536 .height = 20,
537 .screen = undefined,
538 };
539
540 const ch = parent.initChild(10, 10, 21, 21);
541 try std.testing.expectEqual(11, ch.x_off);
542 try std.testing.expectEqual(11, ch.y_off);
543}
544
545test "Window offsets" {
546 var parent = Window{
547 .x_off = 0,
548 .y_off = 0,
549 .parent_x_off = 0,
550 .parent_y_off = 0,
551 .width = 20,
552 .height = 20,
553 .screen = undefined,
554 };
555
556 const ch = parent.initChild(10, 10, 21, 21);
557 const ch2 = ch.initChild(-4, -4, null, null);
558 // Reading ch2 at row 0 should be null
559 try std.testing.expect(ch2.readCell(0, 0) == null);
560 // Should not panic us
561 ch2.writeCell(0, 0, undefined);
562}
563
564test "print: grapheme" {
565 var screen: Screen = .{ .width_method = .unicode };
566 const win: Window = .{
567 .x_off = 0,
568 .y_off = 0,
569 .parent_x_off = 0,
570 .parent_y_off = 0,
571 .width = 4,
572 .height = 2,
573 .screen = &screen,
574 };
575 const opts: PrintOptions = .{
576 .commit = false,
577 .wrap = .grapheme,
578 };
579
580 {
581 var segments = [_]Segment{
582 .{ .text = "a" },
583 };
584 const result = win.print(&segments, opts);
585 try std.testing.expectEqual(1, result.col);
586 try std.testing.expectEqual(0, result.row);
587 try std.testing.expectEqual(false, result.overflow);
588 }
589 {
590 var segments = [_]Segment{
591 .{ .text = "abcd" },
592 };
593 const result = win.print(&segments, opts);
594 try std.testing.expectEqual(0, result.col);
595 try std.testing.expectEqual(1, result.row);
596 try std.testing.expectEqual(false, result.overflow);
597 }
598 {
599 var segments = [_]Segment{
600 .{ .text = "abcde" },
601 };
602 const result = win.print(&segments, opts);
603 try std.testing.expectEqual(1, result.col);
604 try std.testing.expectEqual(1, result.row);
605 try std.testing.expectEqual(false, result.overflow);
606 }
607 {
608 var segments = [_]Segment{
609 .{ .text = "abcdefgh" },
610 };
611 const result = win.print(&segments, opts);
612 try std.testing.expectEqual(0, result.col);
613 try std.testing.expectEqual(2, result.row);
614 try std.testing.expectEqual(false, result.overflow);
615 }
616 {
617 var segments = [_]Segment{
618 .{ .text = "abcdefghi" },
619 };
620 const result = win.print(&segments, opts);
621 try std.testing.expectEqual(0, result.col);
622 try std.testing.expectEqual(2, result.row);
623 try std.testing.expectEqual(true, result.overflow);
624 }
625}
626
627test "print: word" {
628 var screen: Screen = .{
629 .width_method = .unicode,
630 };
631 const win: Window = .{
632 .x_off = 0,
633 .y_off = 0,
634 .parent_x_off = 0,
635 .parent_y_off = 0,
636 .width = 4,
637 .height = 2,
638 .screen = &screen,
639 };
640 const opts: PrintOptions = .{
641 .commit = false,
642 .wrap = .word,
643 };
644
645 {
646 var segments = [_]Segment{
647 .{ .text = "a" },
648 };
649 const result = win.print(&segments, opts);
650 try std.testing.expectEqual(1, result.col);
651 try std.testing.expectEqual(0, result.row);
652 try std.testing.expectEqual(false, result.overflow);
653 }
654 {
655 var segments = [_]Segment{
656 .{ .text = " " },
657 };
658 const result = win.print(&segments, opts);
659 try std.testing.expectEqual(1, result.col);
660 try std.testing.expectEqual(0, result.row);
661 try std.testing.expectEqual(false, result.overflow);
662 }
663 {
664 var segments = [_]Segment{
665 .{ .text = " a" },
666 };
667 const result = win.print(&segments, opts);
668 try std.testing.expectEqual(2, result.col);
669 try std.testing.expectEqual(0, result.row);
670 try std.testing.expectEqual(false, result.overflow);
671 }
672 {
673 var segments = [_]Segment{
674 .{ .text = "a b" },
675 };
676 const result = win.print(&segments, opts);
677 try std.testing.expectEqual(3, result.col);
678 try std.testing.expectEqual(0, result.row);
679 try std.testing.expectEqual(false, result.overflow);
680 }
681 {
682 var segments = [_]Segment{
683 .{ .text = "a b c" },
684 };
685 const result = win.print(&segments, opts);
686 try std.testing.expectEqual(1, result.col);
687 try std.testing.expectEqual(1, result.row);
688 try std.testing.expectEqual(false, result.overflow);
689 }
690 {
691 var segments = [_]Segment{
692 .{ .text = "hello" },
693 };
694 const result = win.print(&segments, opts);
695 try std.testing.expectEqual(1, result.col);
696 try std.testing.expectEqual(1, result.row);
697 try std.testing.expectEqual(false, result.overflow);
698 }
699 {
700 var segments = [_]Segment{
701 .{ .text = "hi tim" },
702 };
703 const result = win.print(&segments, opts);
704 try std.testing.expectEqual(3, result.col);
705 try std.testing.expectEqual(1, result.row);
706 try std.testing.expectEqual(false, result.overflow);
707 }
708 {
709 var segments = [_]Segment{
710 .{ .text = "hello tim" },
711 };
712 const result = win.print(&segments, opts);
713 try std.testing.expectEqual(0, result.col);
714 try std.testing.expectEqual(2, result.row);
715 try std.testing.expectEqual(true, result.overflow);
716 }
717 {
718 var segments = [_]Segment{
719 .{ .text = "hello ti" },
720 };
721 const result = win.print(&segments, opts);
722 try std.testing.expectEqual(0, result.col);
723 try std.testing.expectEqual(2, result.row);
724 try std.testing.expectEqual(false, result.overflow);
725 }
726 {
727 var segments = [_]Segment{
728 .{ .text = "h" },
729 .{ .text = "e" },
730 };
731 const result = win.print(&segments, opts);
732 try std.testing.expectEqual(2, result.col);
733 try std.testing.expectEqual(0, result.row);
734 try std.testing.expectEqual(false, result.overflow);
735 }
736 {
737 var segments = [_]Segment{
738 .{ .text = "h" },
739 .{ .text = "e" },
740 .{ .text = "l" },
741 .{ .text = "l" },
742 .{ .text = "o" },
743 };
744 const result = win.print(&segments, opts);
745 try std.testing.expectEqual(1, result.col);
746 try std.testing.expectEqual(1, result.row);
747 try std.testing.expectEqual(false, result.overflow);
748 }
749 {
750 var segments = [_]Segment{
751 .{ .text = "he\n" },
752 };
753 const result = win.print(&segments, opts);
754 try std.testing.expectEqual(0, result.col);
755 try std.testing.expectEqual(1, result.row);
756 try std.testing.expectEqual(false, result.overflow);
757 }
758 {
759 var segments = [_]Segment{
760 .{ .text = "he\n\n" },
761 };
762 const result = win.print(&segments, opts);
763 try std.testing.expectEqual(0, result.col);
764 try std.testing.expectEqual(2, result.row);
765 try std.testing.expectEqual(false, result.overflow);
766 }
767 {
768 var segments = [_]Segment{
769 .{ .text = "not now" },
770 };
771 const result = win.print(&segments, opts);
772 try std.testing.expectEqual(3, result.col);
773 try std.testing.expectEqual(1, result.row);
774 try std.testing.expectEqual(false, result.overflow);
775 }
776 {
777 var segments = [_]Segment{
778 .{ .text = "note now" },
779 };
780 const result = win.print(&segments, opts);
781 try std.testing.expectEqual(3, result.col);
782 try std.testing.expectEqual(1, result.row);
783 try std.testing.expectEqual(false, result.overflow);
784 }
785 {
786 var segments = [_]Segment{
787 .{ .text = "note" },
788 .{ .text = " now" },
789 };
790 const result = win.print(&segments, opts);
791 try std.testing.expectEqual(3, result.col);
792 try std.testing.expectEqual(1, result.row);
793 try std.testing.expectEqual(false, result.overflow);
794 }
795 {
796 var segments = [_]Segment{
797 .{ .text = "note " },
798 .{ .text = "now" },
799 };
800 const result = win.print(&segments, opts);
801 try std.testing.expectEqual(3, result.col);
802 try std.testing.expectEqual(1, result.row);
803 try std.testing.expectEqual(false, result.overflow);
804 }
805}
806
807/// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n'
808const LineIterator = struct {
809 buf: []const u8,
810 index: usize = 0,
811 has_break: bool = true,
812
813 fn next(self: *LineIterator) ?[]const u8 {
814 if (self.index >= self.buf.len) return null;
815
816 const start = self.index;
817 const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse {
818 if (start == 0) self.has_break = false;
819 self.index = self.buf.len;
820 return self.buf[start..];
821 };
822
823 self.index = end;
824 self.consumeCR();
825 self.consumeLF();
826 return self.buf[start..end];
827 }
828
829 // consumes a \n byte
830 fn consumeLF(self: *LineIterator) void {
831 if (self.index >= self.buf.len) return;
832 if (self.buf[self.index] == '\n') self.index += 1;
833 }
834
835 // consumes a \r byte
836 fn consumeCR(self: *LineIterator) void {
837 if (self.index >= self.buf.len) return;
838 if (self.buf[self.index] == '\r') self.index += 1;
839 }
840};
841
842/// Returns tokens of text and whitespace
843const WhitespaceTokenizer = struct {
844 buf: []const u8,
845 index: usize = 0,
846
847 const Token = union(enum) {
848 // the length of whitespace. Tab = 8
849 whitespace: usize,
850 word: []const u8,
851 };
852
853 fn next(self: *WhitespaceTokenizer) ?Token {
854 if (self.index >= self.buf.len) return null;
855 const Mode = enum {
856 whitespace,
857 word,
858 };
859 const first = self.buf[self.index];
860 const mode: Mode = if (first == ' ' or first == '\t') .whitespace else .word;
861 switch (mode) {
862 .whitespace => {
863 var len: usize = 0;
864 while (self.index < self.buf.len) : (self.index += 1) {
865 switch (self.buf[self.index]) {
866 ' ' => len += 1,
867 '\t' => len += 8,
868 else => break,
869 }
870 }
871 return .{ .whitespace = len };
872 },
873 .word => {
874 const start = self.index;
875 while (self.index < self.buf.len) : (self.index += 1) {
876 switch (self.buf[self.index]) {
877 ' ', '\t' => break,
878 else => {},
879 }
880 }
881 return .{ .word = self.buf[start..self.index] };
882 },
883 }
884 }
885};
886
887test "refAllDecls" {
888 std.testing.refAllDecls(@This());
889}