this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 4320ec29d03415eba80a14b2eaaff8cefa6822e8 889 lines 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}