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