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