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);
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 = @min(self.y_off * self.width, self.screen.buf.len);
221 const end = @min(start + (self.height * self.width), self.screen.buf.len);
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 = @min(self.x_off + (row * self.screen.width), self.screen.buf.len);
229 var end = @min(start + self.width, start + (self.screen.width - self.x_off));
230 end = @min(end, self.screen.buf.len);
231 @memset(self.screen.buf[start..end], cell);
232 }
233 }
234}
235
236/// hide the cursor
237pub fn hideCursor(self: Window) void {
238 self.screen.cursor_vis = false;
239}
240
241/// show the cursor at the given coordinates, 0 indexed
242pub fn showCursor(self: Window, col: usize, row: usize) void {
243 if (self.height == 0 or self.width == 0) return;
244 if (self.height <= row or self.width <= col) return;
245 self.screen.cursor_vis = true;
246 self.screen.cursor_row = row + self.y_off;
247 self.screen.cursor_col = col + self.x_off;
248}
249
250pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
251 self.screen.cursor_shape = shape;
252}
253
254/// Options to use when printing Segments to a window
255pub const PrintOptions = struct {
256 /// vertical offset to start printing at
257 row_offset: usize = 0,
258 /// horizontal offset to start printing at
259 col_offset: usize = 0,
260
261 /// wrap behavior for printing
262 wrap: enum {
263 /// wrap at grapheme boundaries
264 grapheme,
265 /// wrap at word boundaries
266 word,
267 /// stop printing after one line
268 none,
269 } = .grapheme,
270
271 /// when true, print will write to the screen for rendering. When false,
272 /// nothing is written. The return value describes the size of the wrapped
273 /// text
274 commit: bool = true,
275};
276
277pub const PrintResult = struct {
278 col: usize,
279 row: usize,
280 overflow: bool,
281};
282
283/// prints segments to the window. returns true if the text overflowed with the
284/// given wrap strategy and size.
285pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !PrintResult {
286 var row = opts.row_offset;
287 switch (opts.wrap) {
288 .grapheme => {
289 var col: usize = opts.col_offset;
290 const overflow: bool = blk: for (segments) |segment| {
291 var iter = self.screen.unicode.graphemeIterator(segment.text);
292 while (iter.next()) |grapheme| {
293 if (col >= self.width) {
294 row += 1;
295 col = 0;
296 }
297 if (row >= self.height) break :blk true;
298 const s = grapheme.bytes(segment.text);
299 if (std.mem.eql(u8, s, "\n")) {
300 row +|= 1;
301 col = 0;
302 continue;
303 }
304 const w = self.gwidth(s);
305 if (w == 0) continue;
306 if (opts.commit) self.writeCell(col, row, .{
307 .char = .{
308 .grapheme = s,
309 .width = w,
310 },
311 .style = segment.style,
312 .link = segment.link,
313 .wrapped = col + w >= self.width,
314 });
315 col += w;
316 }
317 } else false;
318 if (col >= self.width) {
319 row += 1;
320 col = 0;
321 }
322 return .{
323 .row = row,
324 .col = col,
325 .overflow = overflow,
326 };
327 },
328 .word => {
329 var col: usize = opts.col_offset;
330 var overflow: bool = false;
331 var soft_wrapped: bool = false;
332 outer: for (segments) |segment| {
333 var line_iter: LineIterator = .{ .buf = segment.text };
334 while (line_iter.next()) |line| {
335 defer {
336 // We only set soft_wrapped to false if a segment actually contains a linebreak
337 if (line_iter.has_break) {
338 soft_wrapped = false;
339 row += 1;
340 col = 0;
341 }
342 }
343 var iter: WhitespaceTokenizer = .{ .buf = line };
344 while (iter.next()) |token| {
345 switch (token) {
346 .whitespace => |len| {
347 if (soft_wrapped) continue;
348 for (0..len) |_| {
349 if (col >= self.width) {
350 col = 0;
351 row += 1;
352 break;
353 }
354 if (opts.commit) {
355 self.writeCell(col, row, .{
356 .char = .{
357 .grapheme = " ",
358 .width = 1,
359 },
360 .style = segment.style,
361 .link = segment.link,
362 });
363 }
364 col += 1;
365 }
366 },
367 .word => |word| {
368 const width = self.gwidth(word);
369 if (width + col > self.width and width < self.width) {
370 row += 1;
371 col = 0;
372 }
373
374 var grapheme_iterator = self.screen.unicode.graphemeIterator(word);
375 while (grapheme_iterator.next()) |grapheme| {
376 soft_wrapped = false;
377 if (row >= self.height) {
378 overflow = true;
379 break :outer;
380 }
381 const s = grapheme.bytes(word);
382 const w = self.gwidth(s);
383 if (opts.commit) self.writeCell(col, row, .{
384 .char = .{
385 .grapheme = s,
386 .width = w,
387 },
388 .style = segment.style,
389 .link = segment.link,
390 });
391 col += w;
392 if (col >= self.width) {
393 row += 1;
394 col = 0;
395 soft_wrapped = true;
396 }
397 }
398 },
399 }
400 }
401 }
402 }
403 return .{
404 // remove last row counter
405 .row = row,
406 .col = col,
407 .overflow = overflow,
408 };
409 },
410 .none => {
411 var col: usize = opts.col_offset;
412 const overflow: bool = blk: for (segments) |segment| {
413 var iter = self.screen.unicode.graphemeIterator(segment.text);
414 while (iter.next()) |grapheme| {
415 if (col >= self.width) break :blk true;
416 const s = grapheme.bytes(segment.text);
417 if (std.mem.eql(u8, s, "\n")) break :blk true;
418 const w = self.gwidth(s);
419 if (w == 0) continue;
420 if (opts.commit) self.writeCell(col, row, .{
421 .char = .{
422 .grapheme = s,
423 .width = w,
424 },
425 .style = segment.style,
426 .link = segment.link,
427 });
428 col +|= w;
429 }
430 } else false;
431 return .{
432 .row = row,
433 .col = col,
434 .overflow = overflow,
435 };
436 },
437 }
438 return false;
439}
440
441/// print a single segment. This is just a shortcut for print(&.{segment}, opts)
442pub fn printSegment(self: Window, segment: Segment, opts: PrintOptions) !PrintResult {
443 return self.print(&.{segment}, opts);
444}
445
446/// scrolls the window down one row (IE inserts a blank row at the bottom of the
447/// screen and shifts all rows up one)
448pub fn scroll(self: Window, n: usize) void {
449 if (n > self.height) return;
450 var row = self.y_off;
451 while (row < self.height - n) : (row += 1) {
452 const dst_start = (row * self.width) + self.x_off;
453 const dst_end = dst_start + self.width;
454
455 const src_start = ((row + n) * self.width) + self.x_off;
456 const src_end = src_start + self.width;
457 @memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
458 }
459 const last_row = self.child(.{
460 .y_off = self.height - n,
461 });
462 last_row.clear();
463}
464
465/// returns the mouse event if the mouse event occurred within the window. If
466/// the mouse event occurred outside the window, null is returned
467pub fn hasMouse(win: Window, mouse: ?Mouse) ?Mouse {
468 const event = mouse orelse return null;
469 if (event.col >= win.x_off and
470 event.col < (win.x_off + win.width) and
471 event.row >= win.y_off and
472 event.row < (win.y_off + win.height)) return event else return null;
473}
474
475test "Window size set" {
476 var parent = Window{
477 .x_off = 0,
478 .y_off = 0,
479 .width = 20,
480 .height = 20,
481 .screen = undefined,
482 };
483
484 const ch = parent.initChild(1, 1, .expand, .expand);
485 try std.testing.expectEqual(19, ch.width);
486 try std.testing.expectEqual(19, ch.height);
487}
488
489test "Window size set too big" {
490 var parent = Window{
491 .x_off = 0,
492 .y_off = 0,
493 .width = 20,
494 .height = 20,
495 .screen = undefined,
496 };
497
498 const ch = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 });
499 try std.testing.expectEqual(20, ch.width);
500 try std.testing.expectEqual(20, ch.height);
501}
502
503test "Window size set too big with offset" {
504 var parent = Window{
505 .x_off = 0,
506 .y_off = 0,
507 .width = 20,
508 .height = 20,
509 .screen = undefined,
510 };
511
512 const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
513 try std.testing.expectEqual(10, ch.width);
514 try std.testing.expectEqual(10, ch.height);
515}
516
517test "Window size nested offsets" {
518 var parent = Window{
519 .x_off = 1,
520 .y_off = 1,
521 .width = 20,
522 .height = 20,
523 .screen = undefined,
524 };
525
526 const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
527 try std.testing.expectEqual(11, ch.x_off);
528 try std.testing.expectEqual(11, ch.y_off);
529}
530
531test "print: grapheme" {
532 const alloc = std.testing.allocator_instance.allocator();
533 const unicode = try Unicode.init(alloc);
534 defer unicode.deinit();
535 var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode };
536 const win: Window = .{
537 .x_off = 0,
538 .y_off = 0,
539 .width = 4,
540 .height = 2,
541 .screen = &screen,
542 };
543 const opts: PrintOptions = .{
544 .commit = false,
545 .wrap = .grapheme,
546 };
547
548 {
549 var segments = [_]Segment{
550 .{ .text = "a" },
551 };
552 const result = try win.print(&segments, opts);
553 try std.testing.expectEqual(1, result.col);
554 try std.testing.expectEqual(0, result.row);
555 try std.testing.expectEqual(false, result.overflow);
556 }
557 {
558 var segments = [_]Segment{
559 .{ .text = "abcd" },
560 };
561 const result = try win.print(&segments, opts);
562 try std.testing.expectEqual(0, result.col);
563 try std.testing.expectEqual(1, result.row);
564 try std.testing.expectEqual(false, result.overflow);
565 }
566 {
567 var segments = [_]Segment{
568 .{ .text = "abcde" },
569 };
570 const result = try win.print(&segments, opts);
571 try std.testing.expectEqual(1, result.col);
572 try std.testing.expectEqual(1, result.row);
573 try std.testing.expectEqual(false, result.overflow);
574 }
575 {
576 var segments = [_]Segment{
577 .{ .text = "abcdefgh" },
578 };
579 const result = try win.print(&segments, opts);
580 try std.testing.expectEqual(0, result.col);
581 try std.testing.expectEqual(2, result.row);
582 try std.testing.expectEqual(false, result.overflow);
583 }
584 {
585 var segments = [_]Segment{
586 .{ .text = "abcdefghi" },
587 };
588 const result = try win.print(&segments, opts);
589 try std.testing.expectEqual(0, result.col);
590 try std.testing.expectEqual(2, result.row);
591 try std.testing.expectEqual(true, result.overflow);
592 }
593}
594
595test "print: word" {
596 const alloc = std.testing.allocator_instance.allocator();
597 const unicode = try Unicode.init(alloc);
598 defer unicode.deinit();
599 var screen: Screen = .{
600 .width_method = .unicode,
601 .unicode = &unicode,
602 };
603 const win: Window = .{
604 .x_off = 0,
605 .y_off = 0,
606 .width = 4,
607 .height = 2,
608 .screen = &screen,
609 };
610 const opts: PrintOptions = .{
611 .commit = false,
612 .wrap = .word,
613 };
614
615 {
616 var segments = [_]Segment{
617 .{ .text = "a" },
618 };
619 const result = try win.print(&segments, opts);
620 try std.testing.expectEqual(1, result.col);
621 try std.testing.expectEqual(0, result.row);
622 try std.testing.expectEqual(false, result.overflow);
623 }
624 {
625 var segments = [_]Segment{
626 .{ .text = " " },
627 };
628 const result = try win.print(&segments, opts);
629 try std.testing.expectEqual(1, result.col);
630 try std.testing.expectEqual(0, result.row);
631 try std.testing.expectEqual(false, result.overflow);
632 }
633 {
634 var segments = [_]Segment{
635 .{ .text = " a" },
636 };
637 const result = try win.print(&segments, opts);
638 try std.testing.expectEqual(2, result.col);
639 try std.testing.expectEqual(0, result.row);
640 try std.testing.expectEqual(false, result.overflow);
641 }
642 {
643 var segments = [_]Segment{
644 .{ .text = "a b" },
645 };
646 const result = try win.print(&segments, opts);
647 try std.testing.expectEqual(3, result.col);
648 try std.testing.expectEqual(0, result.row);
649 try std.testing.expectEqual(false, result.overflow);
650 }
651 {
652 var segments = [_]Segment{
653 .{ .text = "a b c" },
654 };
655 const result = try win.print(&segments, opts);
656 try std.testing.expectEqual(1, result.col);
657 try std.testing.expectEqual(1, result.row);
658 try std.testing.expectEqual(false, result.overflow);
659 }
660 {
661 var segments = [_]Segment{
662 .{ .text = "hello" },
663 };
664 const result = try win.print(&segments, opts);
665 try std.testing.expectEqual(1, result.col);
666 try std.testing.expectEqual(1, result.row);
667 try std.testing.expectEqual(false, result.overflow);
668 }
669 {
670 var segments = [_]Segment{
671 .{ .text = "hi tim" },
672 };
673 const result = try win.print(&segments, opts);
674 try std.testing.expectEqual(3, result.col);
675 try std.testing.expectEqual(1, result.row);
676 try std.testing.expectEqual(false, result.overflow);
677 }
678 {
679 var segments = [_]Segment{
680 .{ .text = "hello tim" },
681 };
682 const result = try win.print(&segments, opts);
683 try std.testing.expectEqual(0, result.col);
684 try std.testing.expectEqual(2, result.row);
685 try std.testing.expectEqual(true, result.overflow);
686 }
687 {
688 var segments = [_]Segment{
689 .{ .text = "hello ti" },
690 };
691 const result = try win.print(&segments, opts);
692 try std.testing.expectEqual(0, result.col);
693 try std.testing.expectEqual(2, result.row);
694 try std.testing.expectEqual(false, result.overflow);
695 }
696 {
697 var segments = [_]Segment{
698 .{ .text = "h" },
699 .{ .text = "e" },
700 };
701 const result = try win.print(&segments, opts);
702 try std.testing.expectEqual(2, result.col);
703 try std.testing.expectEqual(0, result.row);
704 try std.testing.expectEqual(false, result.overflow);
705 }
706 {
707 var segments = [_]Segment{
708 .{ .text = "h" },
709 .{ .text = "e" },
710 .{ .text = "l" },
711 .{ .text = "l" },
712 .{ .text = "o" },
713 };
714 const result = try win.print(&segments, opts);
715 try std.testing.expectEqual(1, result.col);
716 try std.testing.expectEqual(1, result.row);
717 try std.testing.expectEqual(false, result.overflow);
718 }
719 {
720 var segments = [_]Segment{
721 .{ .text = "he\n" },
722 };
723 const result = try win.print(&segments, opts);
724 try std.testing.expectEqual(0, result.col);
725 try std.testing.expectEqual(1, result.row);
726 try std.testing.expectEqual(false, result.overflow);
727 }
728 {
729 var segments = [_]Segment{
730 .{ .text = "he\n\n" },
731 };
732 const result = try win.print(&segments, opts);
733 try std.testing.expectEqual(0, result.col);
734 try std.testing.expectEqual(2, result.row);
735 try std.testing.expectEqual(false, result.overflow);
736 }
737 {
738 var segments = [_]Segment{
739 .{ .text = "not now" },
740 };
741 const result = try win.print(&segments, opts);
742 try std.testing.expectEqual(3, result.col);
743 try std.testing.expectEqual(1, result.row);
744 try std.testing.expectEqual(false, result.overflow);
745 }
746 {
747 var segments = [_]Segment{
748 .{ .text = "note now" },
749 };
750 const result = try win.print(&segments, opts);
751 try std.testing.expectEqual(3, result.col);
752 try std.testing.expectEqual(1, result.row);
753 try std.testing.expectEqual(false, result.overflow);
754 }
755 {
756 var segments = [_]Segment{
757 .{ .text = "note" },
758 .{ .text = " now" },
759 };
760 const result = try win.print(&segments, opts);
761 try std.testing.expectEqual(3, 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 = "note " },
768 .{ .text = "now" },
769 };
770 const result = try win.print(&segments, opts);
771 try std.testing.expectEqual(3, result.col);
772 try std.testing.expectEqual(1, result.row);
773 try std.testing.expectEqual(false, result.overflow);
774 }
775}
776
777/// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n'
778const LineIterator = struct {
779 buf: []const u8,
780 index: usize = 0,
781 has_break: bool = true,
782
783 fn next(self: *LineIterator) ?[]const u8 {
784 if (self.index >= self.buf.len) return null;
785
786 const start = self.index;
787 const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse {
788 if (start == 0) self.has_break = false;
789 self.index = self.buf.len;
790 return self.buf[start..];
791 };
792
793 self.index = end;
794 self.consumeCR();
795 self.consumeLF();
796 return self.buf[start..end];
797 }
798
799 // consumes a \n byte
800 fn consumeLF(self: *LineIterator) void {
801 if (self.index >= self.buf.len) return;
802 if (self.buf[self.index] == '\n') self.index += 1;
803 }
804
805 // consumes a \r byte
806 fn consumeCR(self: *LineIterator) void {
807 if (self.index >= self.buf.len) return;
808 if (self.buf[self.index] == '\r') self.index += 1;
809 }
810};
811
812/// Returns tokens of text and whitespace
813const WhitespaceTokenizer = struct {
814 buf: []const u8,
815 index: usize = 0,
816
817 const Token = union(enum) {
818 // the length of whitespace. Tab = 8
819 whitespace: usize,
820 word: []const u8,
821 };
822
823 fn next(self: *WhitespaceTokenizer) ?Token {
824 if (self.index >= self.buf.len) return null;
825 const Mode = enum {
826 whitespace,
827 word,
828 };
829 const first = self.buf[self.index];
830 const mode: Mode = if (first == ' ' or first == '\t') .whitespace else .word;
831 switch (mode) {
832 .whitespace => {
833 var len: usize = 0;
834 while (self.index < self.buf.len) : (self.index += 1) {
835 switch (self.buf[self.index]) {
836 ' ' => len += 1,
837 '\t' => len += 8,
838 else => break,
839 }
840 }
841 return .{ .whitespace = len };
842 },
843 .word => {
844 const start = self.index;
845 while (self.index < self.buf.len) : (self.index += 1) {
846 switch (self.buf[self.index]) {
847 ' ', '\t' => break,
848 else => {},
849 }
850 }
851 return .{ .word = self.buf[start..self.index] };
852 },
853 }
854 }
855};