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