a modern tui library written in zig
at v0.5.1 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); 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};