a modern tui library written in zig
at v0.4.0 29 kB view raw
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};