this repo has no description
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, &self.screen.unicode.width_data);
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 = self.screen.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 = self.screen.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 = self.screen.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 const alloc = std.testing.allocator_instance.allocator();
566 const unicode = try Unicode.init(alloc);
567 defer unicode.deinit(alloc);
568 var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode };
569 const win: Window = .{
570 .x_off = 0,
571 .y_off = 0,
572 .parent_x_off = 0,
573 .parent_y_off = 0,
574 .width = 4,
575 .height = 2,
576 .screen = &screen,
577 };
578 const opts: PrintOptions = .{
579 .commit = false,
580 .wrap = .grapheme,
581 };
582
583 {
584 var segments = [_]Segment{
585 .{ .text = "a" },
586 };
587 const result = win.print(&segments, opts);
588 try std.testing.expectEqual(1, result.col);
589 try std.testing.expectEqual(0, result.row);
590 try std.testing.expectEqual(false, result.overflow);
591 }
592 {
593 var segments = [_]Segment{
594 .{ .text = "abcd" },
595 };
596 const result = win.print(&segments, opts);
597 try std.testing.expectEqual(0, result.col);
598 try std.testing.expectEqual(1, result.row);
599 try std.testing.expectEqual(false, result.overflow);
600 }
601 {
602 var segments = [_]Segment{
603 .{ .text = "abcde" },
604 };
605 const result = win.print(&segments, opts);
606 try std.testing.expectEqual(1, result.col);
607 try std.testing.expectEqual(1, result.row);
608 try std.testing.expectEqual(false, result.overflow);
609 }
610 {
611 var segments = [_]Segment{
612 .{ .text = "abcdefgh" },
613 };
614 const result = win.print(&segments, opts);
615 try std.testing.expectEqual(0, result.col);
616 try std.testing.expectEqual(2, result.row);
617 try std.testing.expectEqual(false, result.overflow);
618 }
619 {
620 var segments = [_]Segment{
621 .{ .text = "abcdefghi" },
622 };
623 const result = win.print(&segments, opts);
624 try std.testing.expectEqual(0, result.col);
625 try std.testing.expectEqual(2, result.row);
626 try std.testing.expectEqual(true, result.overflow);
627 }
628}
629
630test "print: word" {
631 const alloc = std.testing.allocator_instance.allocator();
632 const unicode = try Unicode.init(alloc);
633 defer unicode.deinit(alloc);
634 var screen: Screen = .{
635 .width_method = .unicode,
636 .unicode = &unicode,
637 };
638 const win: Window = .{
639 .x_off = 0,
640 .y_off = 0,
641 .parent_x_off = 0,
642 .parent_y_off = 0,
643 .width = 4,
644 .height = 2,
645 .screen = &screen,
646 };
647 const opts: PrintOptions = .{
648 .commit = false,
649 .wrap = .word,
650 };
651
652 {
653 var segments = [_]Segment{
654 .{ .text = "a" },
655 };
656 const result = win.print(&segments, opts);
657 try std.testing.expectEqual(1, result.col);
658 try std.testing.expectEqual(0, result.row);
659 try std.testing.expectEqual(false, result.overflow);
660 }
661 {
662 var segments = [_]Segment{
663 .{ .text = " " },
664 };
665 const result = win.print(&segments, opts);
666 try std.testing.expectEqual(1, result.col);
667 try std.testing.expectEqual(0, result.row);
668 try std.testing.expectEqual(false, result.overflow);
669 }
670 {
671 var segments = [_]Segment{
672 .{ .text = " a" },
673 };
674 const result = win.print(&segments, opts);
675 try std.testing.expectEqual(2, result.col);
676 try std.testing.expectEqual(0, result.row);
677 try std.testing.expectEqual(false, result.overflow);
678 }
679 {
680 var segments = [_]Segment{
681 .{ .text = "a b" },
682 };
683 const result = win.print(&segments, opts);
684 try std.testing.expectEqual(3, result.col);
685 try std.testing.expectEqual(0, result.row);
686 try std.testing.expectEqual(false, result.overflow);
687 }
688 {
689 var segments = [_]Segment{
690 .{ .text = "a b c" },
691 };
692 const result = win.print(&segments, opts);
693 try std.testing.expectEqual(1, result.col);
694 try std.testing.expectEqual(1, result.row);
695 try std.testing.expectEqual(false, result.overflow);
696 }
697 {
698 var segments = [_]Segment{
699 .{ .text = "hello" },
700 };
701 const result = win.print(&segments, opts);
702 try std.testing.expectEqual(1, result.col);
703 try std.testing.expectEqual(1, result.row);
704 try std.testing.expectEqual(false, result.overflow);
705 }
706 {
707 var segments = [_]Segment{
708 .{ .text = "hi tim" },
709 };
710 const result = win.print(&segments, opts);
711 try std.testing.expectEqual(3, result.col);
712 try std.testing.expectEqual(1, result.row);
713 try std.testing.expectEqual(false, result.overflow);
714 }
715 {
716 var segments = [_]Segment{
717 .{ .text = "hello tim" },
718 };
719 const result = win.print(&segments, opts);
720 try std.testing.expectEqual(0, result.col);
721 try std.testing.expectEqual(2, result.row);
722 try std.testing.expectEqual(true, result.overflow);
723 }
724 {
725 var segments = [_]Segment{
726 .{ .text = "hello ti" },
727 };
728 const result = win.print(&segments, opts);
729 try std.testing.expectEqual(0, result.col);
730 try std.testing.expectEqual(2, result.row);
731 try std.testing.expectEqual(false, result.overflow);
732 }
733 {
734 var segments = [_]Segment{
735 .{ .text = "h" },
736 .{ .text = "e" },
737 };
738 const result = win.print(&segments, opts);
739 try std.testing.expectEqual(2, result.col);
740 try std.testing.expectEqual(0, result.row);
741 try std.testing.expectEqual(false, result.overflow);
742 }
743 {
744 var segments = [_]Segment{
745 .{ .text = "h" },
746 .{ .text = "e" },
747 .{ .text = "l" },
748 .{ .text = "l" },
749 .{ .text = "o" },
750 };
751 const result = win.print(&segments, opts);
752 try std.testing.expectEqual(1, result.col);
753 try std.testing.expectEqual(1, result.row);
754 try std.testing.expectEqual(false, result.overflow);
755 }
756 {
757 var segments = [_]Segment{
758 .{ .text = "he\n" },
759 };
760 const result = win.print(&segments, opts);
761 try std.testing.expectEqual(0, result.col);
762 try std.testing.expectEqual(1, result.row);
763 try std.testing.expectEqual(false, result.overflow);
764 }
765 {
766 var segments = [_]Segment{
767 .{ .text = "he\n\n" },
768 };
769 const result = win.print(&segments, opts);
770 try std.testing.expectEqual(0, result.col);
771 try std.testing.expectEqual(2, result.row);
772 try std.testing.expectEqual(false, result.overflow);
773 }
774 {
775 var segments = [_]Segment{
776 .{ .text = "not now" },
777 };
778 const result = win.print(&segments, opts);
779 try std.testing.expectEqual(3, result.col);
780 try std.testing.expectEqual(1, result.row);
781 try std.testing.expectEqual(false, result.overflow);
782 }
783 {
784 var segments = [_]Segment{
785 .{ .text = "note now" },
786 };
787 const result = win.print(&segments, opts);
788 try std.testing.expectEqual(3, result.col);
789 try std.testing.expectEqual(1, result.row);
790 try std.testing.expectEqual(false, result.overflow);
791 }
792 {
793 var segments = [_]Segment{
794 .{ .text = "note" },
795 .{ .text = " now" },
796 };
797 const result = win.print(&segments, opts);
798 try std.testing.expectEqual(3, result.col);
799 try std.testing.expectEqual(1, result.row);
800 try std.testing.expectEqual(false, result.overflow);
801 }
802 {
803 var segments = [_]Segment{
804 .{ .text = "note " },
805 .{ .text = "now" },
806 };
807 const result = win.print(&segments, opts);
808 try std.testing.expectEqual(3, result.col);
809 try std.testing.expectEqual(1, result.row);
810 try std.testing.expectEqual(false, result.overflow);
811 }
812}
813
814/// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n'
815const LineIterator = struct {
816 buf: []const u8,
817 index: usize = 0,
818 has_break: bool = true,
819
820 fn next(self: *LineIterator) ?[]const u8 {
821 if (self.index >= self.buf.len) return null;
822
823 const start = self.index;
824 const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse {
825 if (start == 0) self.has_break = false;
826 self.index = self.buf.len;
827 return self.buf[start..];
828 };
829
830 self.index = end;
831 self.consumeCR();
832 self.consumeLF();
833 return self.buf[start..end];
834 }
835
836 // consumes a \n byte
837 fn consumeLF(self: *LineIterator) void {
838 if (self.index >= self.buf.len) return;
839 if (self.buf[self.index] == '\n') self.index += 1;
840 }
841
842 // consumes a \r byte
843 fn consumeCR(self: *LineIterator) void {
844 if (self.index >= self.buf.len) return;
845 if (self.buf[self.index] == '\r') self.index += 1;
846 }
847};
848
849/// Returns tokens of text and whitespace
850const WhitespaceTokenizer = struct {
851 buf: []const u8,
852 index: usize = 0,
853
854 const Token = union(enum) {
855 // the length of whitespace. Tab = 8
856 whitespace: usize,
857 word: []const u8,
858 };
859
860 fn next(self: *WhitespaceTokenizer) ?Token {
861 if (self.index >= self.buf.len) return null;
862 const Mode = enum {
863 whitespace,
864 word,
865 };
866 const first = self.buf[self.index];
867 const mode: Mode = if (first == ' ' or first == '\t') .whitespace else .word;
868 switch (mode) {
869 .whitespace => {
870 var len: usize = 0;
871 while (self.index < self.buf.len) : (self.index += 1) {
872 switch (self.buf[self.index]) {
873 ' ' => len += 1,
874 '\t' => len += 8,
875 else => break,
876 }
877 }
878 return .{ .whitespace = len };
879 },
880 .word => {
881 const start = self.index;
882 while (self.index < self.buf.len) : (self.index += 1) {
883 switch (self.buf[self.index]) {
884 ' ', '\t' => break,
885 else => {},
886 }
887 }
888 return .{ .word = self.buf[start..self.index] };
889 },
890 }
891 }
892};
893
894test "refAllDecls" {
895 std.testing.refAllDecls(@This());
896}