1 /// Contains various utilities for displaying and formatting text. 2 module jaster.cli.text; 3 4 import std.typecons : Flag; 5 import jaster.cli.ansi : AnsiChar; 6 7 /// Contains options for the `lineWrap` function. 8 struct LineWrapOptions 9 { 10 /++ 11 + How many characters per line, in total, are allowed. 12 + 13 + Do note that the `linePrefix`, `lineSuffix`, as well as leading new line characters are subtracted from this limit, 14 + to find the acutal total amount of characters that can be shown on each line. 15 + ++/ 16 size_t lineCharLimit; 17 18 /++ 19 + A string to prefix each line with, helpful for automatic tabulation of each newly made line. 20 + ++/ 21 string linePrefix; 22 23 /++ 24 + Same as `linePrefix`, except it's a suffix. 25 + ++/ 26 string lineSuffix; 27 28 /++ 29 + Calculates the amount of characters per line that can be used for the user's provided text. 30 + 31 + In other words, how many characters are left after the `linePrefix`, `lineSuffix`, and any `additionalChars` are considered for. 32 + 33 + Params: 34 + additionalChars = The amount of additional chars that are outputted with every line, e.g. if you want to add new lines or tabs or whatever. 35 + 36 + Returns: 37 + The amount of characters per line, not including "static" characters such as the `linePrefix` and so on. 38 + 39 + 0 is returned on underflow. 40 + ++/ 41 @safe @nogc 42 size_t charsPerLine(size_t additionalChars = 0) nothrow pure const 43 { 44 const value = this.lineCharLimit - (this.linePrefix.length + this.lineSuffix.length + additionalChars); 45 46 return (value > this.lineCharLimit) ? 0 : value; // Check for underflow. 47 } 48 /// 49 unittest 50 { 51 assert(LineWrapOptions(120).charsPerLine == 120); 52 assert(LineWrapOptions(120).charsPerLine(20) == 100); 53 assert(LineWrapOptions(120, "ABC", "123").charsPerLine == 114); 54 assert(LineWrapOptions(120).charsPerLine(200) == 0); // Underflow 55 } 56 } 57 58 /++ 59 + Same thing as `asLineWrapped`, except it is eagerly evaluated. 60 + 61 + Throws: 62 + `Exception` if the char limit is too small to show any text. 63 + 64 + Params: 65 + text = The text to line wrap. 66 + options = The options to line wrap with. 67 + 68 + Performance: 69 + With character-based wrapping, this function can calculate the entire amount of space needed for the resulting string, resulting in only 70 + a single GC allocation. 71 + 72 + Returns: 73 + A newly allocated `string` containing the eagerly-evaluated results of `asLineWrapped(text, options)`. 74 + 75 + See_Also: 76 + `LineWrapRange` for full documentation. 77 + 78 + `asLineWrapped` for a lazily evaluated version. 79 + ++/ 80 @trusted // Can't be @safe due to assumeUnique 81 string lineWrap(const(char)[] text, const LineWrapOptions options = LineWrapOptions(120)) pure 82 { 83 import std.exception : assumeUnique, enforce; 84 85 const charsPerLine = options.charsPerLine(LineWrapRange!string.ADDITIONAL_CHARACTERS_PER_LINE); 86 if(charsPerLine == 0) 87 LineWrapRange!string("", options); // Causes the ctor to throw an exception with a proper error message. 88 89 const estimatedLines = (text.length / charsPerLine); 90 91 char[] actualText; 92 actualText.reserve( 93 text.length 94 + (options.linePrefix.length * estimatedLines) 95 + (options.lineSuffix.length * estimatedLines) 96 + estimatedLines // For new line characters. 97 ); // This can overallocate, because we can strip off leading space characters. 98 99 foreach(segment; text.asLineWrapped(options)) 100 actualText ~= segment; 101 102 return actualText.assumeUnique; 103 } 104 /// 105 @safe 106 unittest 107 { 108 const options = LineWrapOptions(8, "\t", "-"); 109 const text = "Hello world".lineWrap(options); 110 assert(text == "\tHello-\n\tworld-", text); 111 } 112 113 @("issue #2") 114 @safe 115 unittest 116 { 117 const options = LineWrapOptions(4, ""); 118 const text = lineWrap("abcdefgh", options); 119 120 assert(text[$-1] != '\n', "lineWrap is inserting a new line at the end again."); 121 assert(text == "abc\ndef\ngh", text); 122 } 123 124 /++ 125 + An InputRange that wraps a piece of text into seperate lines, based on the given options. 126 + 127 + Throws: 128 + `Exception` if the char limit is too small to show any text. 129 + 130 + Notes: 131 + Other than the constructor, this range is entirely `@nogc nothrow`. 132 + 133 + Currently, this performs character-wrapping instead of word-wrapping, so words 134 + can be split between multiple lines. There is no technical reason for this outside of I'm lazy. 135 + The option between character and word wrapping will become a toggle inside of `LineWrapOptions`, so don't fear about 136 + this range magically breaking in the future. 137 + 138 + For every line created from the given `text`, the starting and ending spaces (not all whitespace, just spaces) 139 + are stripped off. This is so the user doesn't have to worry about random leading/trailling spaces, making it 140 + easier to format for the general case (though specific cases might find this undesirable, I'm sorry). 141 + $(B This does not apply to prefixes and suffixes). 142 + 143 + I may expand `LineWrapOptions` so that the user can specify an array of characters to be stripped off, instead of it being hard coded to spaces. 144 + 145 + Output: 146 + For every line that needs to be wrapped by this range, it will return values in the following pattern. 147 + 148 + Prefix (`LineWrapOptions.prefix`) -> Text (from input) -> Suffix (`LineWrapOptions.suffix`) -> New line character (if this isn't the last line). 149 + 150 + $(B Prefixes and suffixes are only outputted if they are not) `null`. 151 + 152 + Please refer to the example unittest for this struct, as it will show you this pattern more clearly. 153 + 154 + Peformance: 155 + This range performs no allocations other than if the ctor throws an exception. 156 + 157 + For character-based wrapping, the part of the code that handles getting the next range of characters from the user-provided input, totals 158 + to `O(l+s)`, where "l" is the amount of lines that this range will produce (`input.length / lineWrapOptions.charsPerLine(1)`), and "s" is the amount 159 + of leading spaces that the range needs to skip over. In general, "l" will be the main speed factor. 160 + 161 + For character-based wrapping, once leading spaces have been skipped over, it is able to calculate the start and end for the range of user-provided 162 + characters to return. In other words, it doesn't need to iterate over every single character (unless every single character is a space >:D), making it very fast. 163 + ++/ 164 @safe 165 struct LineWrapRange(StringT) 166 { 167 // So basically, we want to return the same type we get, instead of going midway with `const(char)[]`. 168 // 169 // This is because it's pretty annoying when you pass a `string` in, with plans to store things as `string`s, but then 170 // find out that you're only getting a `const(char)[]`. 171 // 172 // Also we're working directly with arrays instead of ranges, so we don't have to allocate. 173 static assert( 174 is(StringT : const(char)[]), 175 "StringT must either be a `string` or a `char[]` of some sort." 176 ); 177 178 private 179 { 180 enum Next 181 { 182 prefix, 183 text, 184 suffix, 185 newline 186 } 187 188 enum ADDITIONAL_CHARACTERS_PER_LINE = 1; // New line 189 190 StringT _input; 191 StringT _front; 192 size_t _cursor; 193 Next _nextFront; 194 LineWrapOptions _options; 195 } 196 197 this(StringT input, LineWrapOptions options = LineWrapOptions(120)) pure 198 { 199 import std.exception : enforce; 200 201 this._input = input; 202 this._options = options; 203 204 enforce( 205 options.charsPerLine(ADDITIONAL_CHARACTERS_PER_LINE) > 0, 206 "The lineCharLimit is too low. There's not enough space for any text (after factoring the prefix, suffix, and ending new line characters)." 207 ); 208 209 this.popFront(); 210 } 211 212 @nogc nothrow pure: 213 214 StringT front() 215 { 216 return this._front; 217 } 218 219 bool empty() 220 { 221 return this._front is null; 222 } 223 224 void popFront() 225 { 226 switch(this._nextFront) with(Next) 227 { 228 case prefix: 229 if(this._options.linePrefix is null) 230 { 231 this._nextFront = Next.text; 232 this.popFront(); 233 return; 234 } 235 236 this._front = this._options.linePrefix; 237 this._nextFront = Next.text; 238 return; 239 240 case suffix: 241 if(this._options.lineSuffix is null) 242 { 243 this._nextFront = Next.newline; 244 this.popFront(); 245 return; 246 } 247 248 this._front = this._options.lineSuffix; 249 this._nextFront = Next.newline; 250 return; 251 252 case text: break; // The rest of this function is the logic for .text, so just break. 253 default: break; // We want to hide .newline behind the End of text check, otherwise we'll end up with a stray newline at the end that we don't want. 254 } 255 256 // end of text check 257 if(this._cursor >= this._input.length) 258 { 259 this._front = null; 260 return; 261 } 262 263 // Only add the new lines if we've not hit end of text. 264 if(this._nextFront == Next.newline) 265 { 266 this._front = "\n"; 267 this._nextFront = Next.prefix; 268 return; 269 } 270 271 // This is the logic for Next.text 272 // BUG: "end" can very technically wrap around, causing a range error. 273 // If you're line wrapping a 4 billion+/whatever ulong.max is, sized string, you have other issues I imagine. 274 275 // Find the range for the next piece of text. 276 const charsPerLine = this._options.charsPerLine(ADDITIONAL_CHARACTERS_PER_LINE); 277 size_t end = (this._cursor + charsPerLine); 278 279 // Strip off whitespace, so things format properly. 280 while(this._cursor < this._input.length && this._input[this._cursor] == ' ') 281 { 282 this._cursor++; 283 end++; 284 } 285 286 this._front = this._input[this._cursor..(end >= this._input.length) ? this._input.length : end]; 287 this._cursor += charsPerLine; 288 this._nextFront = Next.suffix; 289 } 290 } 291 /// 292 @safe 293 unittest 294 { 295 import std.algorithm : equal; 296 import std.format : format; 297 298 auto options = LineWrapOptions(8, "\t", "-"); 299 assert(options.charsPerLine(1) == 5); 300 301 // This is the only line that's *not* @nogc nothrow, as it can throw an exception. 302 auto range = "Hello world".asLineWrapped(options); 303 304 assert(range.equal([ 305 "\t", "Hello", "-", "\n", 306 "\t", "world", "-" // Leading spaces were trimmed. No ending newline. 307 ]), "%s".format(range)); 308 309 // If the suffix/prefix are null, then they don't get outputted 310 options.linePrefix = null; 311 options.lineCharLimit--; 312 range = "Hello world".asLineWrapped(options); 313 314 assert(range.equal([ 315 "Hello", "-", "\n", 316 "world", "-" 317 ])); 318 } 319 320 @("Test that a LineWrapRange that only creates a single line, works fine.") 321 unittest 322 { 323 import std.algorithm : equal; 324 325 const options = LineWrapOptions(6); 326 auto range = "Hello".asLineWrapped(options); 327 assert(!range.empty, "Range created no values"); 328 assert(range.equal(["Hello"])); 329 } 330 331 @("LineWrapRange.init must be empty") 332 unittest 333 { 334 assert(LineWrapRange!string.init.empty); 335 } 336 337 // Two overloads to make it clear there's a behaviour difference. 338 339 /++ 340 + Returns an InputRange (`LineWrapRange`) that will wrap the given `text` onto seperate lines. 341 + 342 + Params: 343 + text = The text to line wrap. 344 + options = The options to line wrap with, such as whether to add a prefix and suffix. 345 + 346 + Returns: 347 + A `LineWrapRange` that will wrap the given `text` onto seperate lines. 348 + 349 + See_Also: 350 + `LineWrapRange` for full documentation. 351 + 352 + `lineWrap` for an eagerly evaluated version. 353 + ++/ 354 @safe 355 LineWrapRange!string asLineWrapped(string text, LineWrapOptions options = LineWrapOptions(120)) pure 356 { 357 return typeof(return)(text, options); 358 } 359 360 /// ditto 361 @safe 362 LineWrapRange!(const(char)[]) asLineWrapped(CharArrayT)(CharArrayT text, LineWrapOptions options = LineWrapOptions(120)) pure 363 if(is(CharArrayT == char[]) || is(CharArrayT == const(char)[])) 364 { 365 // If it's not clear, if the user passes in "char[]" then it gets promoted into "const(char)[]". 366 return typeof(return)(text, options); 367 } 368 /// 369 unittest 370 { 371 auto constChars = cast(const(char)[])"Hello"; 372 auto mutableChars = ['H', 'e', 'l', 'l', 'o']; 373 374 // Mutable "char[]" is promoted to const "const(char)[]". 375 LineWrapRange!(const(char)[]) constRange = constChars.asLineWrapped; 376 LineWrapRange!(const(char)[]) mutableRange = mutableChars.asLineWrapped; 377 } 378 379 /++ 380 + A basic rectangle struct, used to specify the bounds of a `TextBufferWriter`. 381 + 382 + Notes: 383 + This struct is not fully @nogc due to the use of `std.format` within assert messages. 384 + ++/ 385 @safe 386 struct TextBufferBounds 387 { 388 /// x offset 389 size_t left; 390 /// y offset 391 size_t top; 392 /// width 393 size_t width; 394 /// height 395 size_t height; 396 397 /++ 398 + Finds the relative center point on the X axis, optionally taking into account the width of another object (e.g. text). 399 + 400 + Params: 401 + width = The optional width to take into account. 402 + 403 + Returns: 404 + The relative center X position, optionally offset by `width`. 405 + ++/ 406 size_t centerX(const size_t width = 0) pure 407 { 408 return this.centerAxis(this.width, width); 409 } 410 /// 411 @safe pure 412 unittest 413 { 414 auto bounds = TextBufferBounds(0, 0, 10, 0); 415 assert(bounds.centerX == 5); 416 assert(bounds.centerX(2) == 4); 417 assert(bounds.centerX(5) == 2); 418 419 bounds.left = 20000; 420 assert(bounds.centerX == 5); // centerX provides a relative point, not absolute. 421 } 422 423 /++ 424 + Finds the relative center point on the Y axis, optionally taking into account the height of another object (e.g. text). 425 + 426 + Params: 427 + height = The optional height to take into account. 428 + 429 + Returns: 430 + The relative center Y position, optionally offset by `height`. 431 + ++/ 432 size_t centerY(const size_t height = 0) pure 433 { 434 return this.centerAxis(this.height, height); 435 } 436 437 private size_t centerAxis(const size_t axis, const size_t offset) pure 438 { 439 import std.format : format; 440 assert(offset <= axis, "Cannot use offset as it's larger than the axis. Axis = %s, offset = %s".format(axis, offset)); 441 return (axis - offset) / 2; 442 } 443 444 /// 2D point to 1D array index. 445 @nogc 446 private size_t pointToIndex(size_t x, size_t y, size_t bufferWidth) const nothrow pure 447 { 448 return (x + this.left) + (bufferWidth * (y + this.top)); 449 } 450 /// 451 @safe @nogc nothrow pure 452 unittest 453 { 454 auto b = TextBufferBounds(0, 0, 5, 5); 455 assert(b.pointToIndex(0, 0, 5) == 0); 456 assert(b.pointToIndex(0, 1, 5) == 5); 457 assert(b.pointToIndex(4, 4, 5) == 24); 458 459 b = TextBufferBounds(1, 1, 3, 2); 460 assert(b.pointToIndex(0, 0, 5) == 6); 461 assert(b.pointToIndex(1, 0, 5) == 7); 462 assert(b.pointToIndex(0, 1, 5) == 11); 463 assert(b.pointToIndex(1, 1, 5) == 12); 464 } 465 466 private void assertPointInBounds(size_t x, size_t y, size_t bufferWidth, size_t bufferSize) const pure 467 { 468 import std.format : format; 469 470 assert(x < this.width, "X is larger than width. Width = %s, X = %s".format(this.width, x)); 471 assert(y < this.height, "Y is larger than height. Height = %s, Y = %s".format(this.height, y)); 472 473 const maxIndex = this.pointToIndex(this.width - 1, this.height - 1, bufferWidth); 474 const pointIndex = this.pointToIndex(x, y, bufferWidth); 475 assert(pointIndex <= maxIndex, "Index is outside alloted bounds. Max = %s, given = %s".format(maxIndex, pointIndex)); 476 assert(pointIndex < bufferSize, "Index is outside of the TextBuffer's bounds. Max = %s, given = %s".format(bufferSize, pointIndex)); 477 } 478 /// 479 unittest 480 { 481 // Testing what error messages look like. 482 auto b = TextBufferBounds(5, 5, 5, 5); 483 //b.assertPointInBounds(6, 0, 0, 0); 484 //b.assertPointInBounds(0, 6, 0, 0); 485 //b.assertPointInBounds(1, 0, 0, 0); 486 } 487 } 488 489 /++ 490 + A mutable random-access range of `AnsiChar`s that belongs to a certain bounded area (`TextBufferBound`) within a `TextBuffer`. 491 + 492 + You can use this range to go over a certain rectangular area of characters using the range API; directly index into this rectangular area, 493 + and directly modify elements in this rectangular range. 494 + 495 + Reading: 496 + Since this is a random-access range, you can either use the normal `foreach`, `popFront` + `front` combo, and you can directly index 497 + into this range. 498 + 499 + Note that popping the elements from this range $(B does) affect indexing. So if you `popFront`, then [0] is now what was previous [1], and so on. 500 + 501 + Writing: 502 + This range implements `opIndexAssign` for both `char` and `AnsiChar` parameters. 503 + 504 + You can either index in a 1D way (using 1 index), or a 2D ways (using 2 indicies, $(B not implemented yet)). 505 + 506 + So if you wanted to set the 7th index to a certain character, then you could do `range[6] = '0'`. 507 + 508 + You could also do it like so - `range[6] = AnsiChar(...params here)` 509 + 510 + See_Also: 511 + `TextBufferWriter.getArea` 512 + ++/ 513 @safe 514 struct TextBufferRange 515 { 516 private pure 517 { 518 TextBuffer _buffer; 519 TextBufferBounds _bounds; 520 size_t _cursorX; 521 size_t _cursorY; 522 AnsiChar _front; 523 524 this(TextBuffer buffer, TextBufferBounds bounds) 525 { 526 assert(buffer !is null, "Buffer is null."); 527 528 this._buffer = buffer; 529 this._bounds = bounds; 530 531 assert(bounds.width > 0, "Width is 0"); 532 assert(bounds.height > 0, "Height is 0"); 533 bounds.assertPointInBounds(bounds.width - 1, bounds.height - 1, buffer._width, buffer._chars.length); 534 535 this.popFront(); 536 } 537 538 @property 539 ref AnsiChar opIndexImpl(size_t i) 540 { 541 import std.format : format; 542 assert(i < this.length, "Index out of bounds. Length = %s, Index = %s.".format(this.length, i)); 543 544 i += this.progressedLength - 1; // Number's always off by 1. 545 const line = i / this._bounds.width; 546 const column = i % this._bounds.width; 547 const index = this._bounds.pointToIndex(column, line, this._buffer._width); 548 549 return this._buffer._chars[index]; 550 } 551 } 552 553 /// 554 void popFront() pure 555 { 556 if(this._cursorY == this._bounds.height) 557 { 558 this._buffer = null; 559 return; 560 } 561 562 const index = this._bounds.pointToIndex(this._cursorX++, this._cursorY, this._buffer._width); 563 this._front = this._buffer._chars[index]; 564 565 if(this._cursorX >= this._bounds.width) 566 { 567 this._cursorX = 0; 568 this._cursorY++; 569 } 570 } 571 572 @safe pure: 573 574 /// Returns: The character at the specified index. 575 @property 576 AnsiChar opIndex(size_t i) 577 { 578 return this.opIndexImpl(i); 579 } 580 581 /++ 582 + Sets the character value of the `AnsiChar` at index `i`. 583 + 584 + Notes: 585 + This preserves the colouring and styling of the `AnsiChar`, as we're simply changing its value. 586 + 587 + Params: 588 + ch = The character to use. 589 + i = The index of the ansi character to change. 590 + ++/ 591 @property 592 void opIndexAssign(char ch, size_t i) 593 { 594 this.opIndexImpl(i).value = ch; 595 this._buffer.makeDirty(); 596 } 597 598 /// ditto. 599 @property 600 void opIndexAssign(AnsiChar ch, size_t i) 601 { 602 this.opIndexImpl(i) = ch; 603 this._buffer.makeDirty(); 604 } 605 606 @safe @nogc nothrow pure: 607 608 /// 609 @property 610 AnsiChar front() 611 { 612 return this._front; 613 } 614 615 /// 616 @property 617 bool empty() const 618 { 619 return this._buffer is null; 620 } 621 622 /// The bounds that this range are constrained to. 623 @property 624 TextBufferBounds bounds() const 625 { 626 return this._bounds; 627 } 628 629 /// Effectively how many times `popFront` has been called. 630 @property 631 size_t progressedLength() const 632 { 633 return (this._cursorX + (this._cursorY * this._bounds.width)); 634 } 635 636 /// How many elements are left in the range. 637 @property 638 size_t length() const 639 { 640 return (this.empty) ? 0 : ((this._bounds.width * this._bounds.height) - this.progressedLength) + 1; // + 1 is to deal with the staggered empty logic, otherwise this is 1 off constantly. 641 } 642 alias opDollar = length; 643 } 644 645 /++ 646 + The main way to modify and read data into/from a `TextBuffer`. 647 + 648 + Performance: 649 + Outside of error messages (only in asserts), there shouldn't be any allocations. 650 + ++/ 651 @safe 652 struct TextBufferWriter 653 { 654 import jaster.cli.ansi : AnsiColour, AnsiTextFlags, AnsiText; 655 656 @nogc 657 private nothrow pure 658 { 659 TextBuffer _buffer; 660 TextBufferBounds _originalBounds; 661 TextBufferBounds _bounds; 662 AnsiColour _fg; 663 AnsiColour _bg; 664 AnsiTextFlags _flags; 665 666 this(TextBuffer buffer, TextBufferBounds bounds) 667 { 668 this._originalBounds = bounds; 669 this._buffer = buffer; 670 this._bounds = bounds; 671 672 this.updateSize(); 673 } 674 675 void setSingleChar(size_t index, char ch, AnsiColour fg, AnsiColour bg, AnsiTextFlags flags) 676 { 677 scope value = &this._buffer._chars[index]; 678 value.value = ch; 679 value.fg = fg; 680 value.bg = bg; 681 value.flags = flags; 682 } 683 684 void fixSize(ref size_t size, const size_t offset, const size_t maxSize) 685 { 686 if(size == TextBuffer.USE_REMAINING_SPACE) 687 size = maxSize - offset; 688 } 689 } 690 691 /++ 692 + Updates the size of this `TextBufferWriter` to reflect any size changes within the underlying 693 + `TextBuffer`. 694 + 695 + For example, if this `TextBufferWriter`'s height is set to `TextBuffer.USE_REMAINING_SPACE`, and the underlying 696 + `TextBuffer`'s height is changed, then this function is used to reflect these changes. 697 + ++/ 698 @nogc 699 void updateSize() nothrow pure 700 { 701 if(this._originalBounds.width == TextBuffer.USE_REMAINING_SPACE) 702 this._bounds.width = this._buffer._width - this._bounds.left; 703 if(this._originalBounds.height == TextBuffer.USE_REMAINING_SPACE) 704 this._bounds.height = this._buffer._height - this._bounds.top; 705 } 706 707 /++ 708 + Sets a character at a specific point. 709 + 710 + Assertions: 711 + The point (x, y) must be in bounds. 712 + 713 + Params: 714 + x = The x position of the point. 715 + y = The y position of the point. 716 + ch = The character to place. 717 + 718 + Returns: 719 + `this`, for function chaining. 720 + ++/ 721 TextBufferWriter set(size_t x, size_t y, char ch) pure 722 { 723 const index = this._bounds.pointToIndex(x, y, this._buffer._width); 724 this._bounds.assertPointInBounds(x, y, this._buffer._width, this._buffer._chars.length); 725 726 this.setSingleChar(index, ch, this._fg, this._bg, this._flags); 727 this._buffer.makeDirty(); 728 729 return this; 730 } 731 732 /++ 733 + Fills an area with a specific character. 734 + 735 + Assertions: 736 + The point (x, y) must be in bounds. 737 + 738 + Params: 739 + x = The starting x position. 740 + y = The starting y position. 741 + width = How many characters to fill. 742 + height = How many lines to fill. 743 + ch = The character to place. 744 + 745 + Returns: 746 + `this`, for function chaining. 747 + ++/ 748 TextBufferWriter fill(size_t x, size_t y, size_t width, size_t height, char ch) pure 749 { 750 this.fixSize(/*ref*/ width, x, this.bounds.width); 751 this.fixSize(/*ref*/ height, y, this.bounds.height); 752 753 const bufferLength = this._buffer._chars.length; 754 const bufferWidth = this._buffer._width; 755 foreach(line; 0..height) 756 { 757 foreach(column; 0..width) 758 { 759 // OPTIMISATION: We should be able to fill each line in batch, rather than one character at a time. 760 const newX = x + column; 761 const newY = y + line; 762 const index = this._bounds.pointToIndex(newX, newY, bufferWidth); 763 this._bounds.assertPointInBounds(newX, newY, bufferWidth, bufferLength); 764 this.setSingleChar(index, ch, this._fg, this._bg, this._flags); 765 } 766 } 767 768 this._buffer.makeDirty(); 769 return this; 770 } 771 772 /++ 773 + Writes some text starting at the given point. 774 + 775 + Notes: 776 + If there's too much text to write, it'll simply be left out. 777 + 778 + Text will automatically overflow onto the next line down, starting at the given `x` position on each new line. 779 + 780 + New line characters are handled properly. 781 + 782 + When text overflows onto the next line, any spaces before the next visible character are removed. 783 + 784 + ANSI text is $(B only) supported by the overload of this function that takes an `AnsiText` instead of a `char[]`. 785 + 786 + Params: 787 + x = The starting x position. 788 + y = The starting y position. 789 + text = The text to write. 790 + 791 + Returns: 792 + `this`, for function chaining. 793 + +/ 794 TextBufferWriter write(size_t x, size_t y, const char[] text) pure 795 { 796 // Underflow doesn't matter, since it'll fail the assert check a few lines down anyway, unless 797 // the buffer's size is in the billions+, which is... unlikely. 798 const width = this.bounds.width - x; 799 const height = this.bounds.height - y; 800 801 const bufferLength = this._buffer._chars.length; 802 const bufferWidth = this._buffer._width; 803 this.bounds.assertPointInBounds(x, y, bufferWidth, bufferLength); 804 this.bounds.assertPointInBounds(x + (width - 1), y + (height - 1), bufferWidth, bufferLength); // - 1 to make the width and height exclusive. 805 806 auto cursorX = x; 807 auto cursorY = y; 808 809 void nextLine(ref size_t i) 810 { 811 cursorX = x; 812 cursorY++; 813 814 // Eat any spaces, similar to how lineWrap functions. 815 // TODO: I think I should make a lineWrap range for situations like this, where I specifically won't use lineWrap due to allocations. 816 // And then I can just make the original lineWrap function call std.range.array on the range. 817 while(i < text.length - 1 && text[i + 1] == ' ') 818 i++; 819 } 820 821 for(size_t i = 0; i < text.length; i++) 822 { 823 const ch = text[i]; 824 if(ch == '\n') 825 { 826 nextLine(i); 827 828 if(cursorY >= height) 829 break; 830 } 831 832 const index = this.bounds.pointToIndex(cursorX, cursorY, bufferWidth); 833 this.setSingleChar(index, ch, this._fg, this._bg, this._flags); 834 835 cursorX++; 836 if(cursorX == x + width) 837 { 838 nextLine(i); 839 840 if(cursorY >= height) 841 break; 842 } 843 } 844 845 this._buffer.makeDirty(); 846 return this; 847 } 848 849 /// ditto. 850 TextBufferWriter write(size_t x, size_t y, AnsiText text) pure 851 { 852 const fg = this.fg; 853 const bg = this.bg; 854 const flags = this.flags; 855 856 this.fg = text.fg; 857 this.bg = text.bg; 858 this.flags = text.flags(); 859 860 this.write(x, y, text.rawText); // Can only fail asserts, never exceptions, so we don't need scope(exit/failure). 861 862 this.fg = fg; 863 this.bg = bg; 864 this.flags = flags; 865 return this; 866 } 867 868 /++ 869 + Assertions: 870 + The point (x, y) must be in bounds. 871 + 872 + Params: 873 + x = The x position. 874 + y = The y position. 875 + 876 + Returns: 877 + The `AnsiChar` at the given point (x, y) 878 + ++/ 879 AnsiChar get(size_t x, size_t y) pure 880 { 881 const index = this._bounds.pointToIndex(x, y, this._buffer._width); 882 this._bounds.assertPointInBounds(x, y, this._buffer._width, this._buffer._chars.length); 883 884 return this._buffer._chars[index]; 885 } 886 887 /++ 888 + Returns a mutable, random-access (indexable) range (`TextBufferRange`) containing the characters 889 + of the specified area. 890 + 891 + Params: 892 + x = The x position to start at. 893 + y = The y position to start at. 894 + width = How many characters per line. 895 + height = How many lines. 896 + 897 + Returns: 898 + A `TextBufferRange` that is configured for the given area. 899 + ++/ 900 TextBufferRange getArea(size_t x, size_t y, size_t width, size_t height) pure 901 { 902 auto bounds = TextBufferBounds(this._bounds.left + x, this._bounds.top + y); 903 this.fixSize(/*ref*/ width, bounds.left, this.bounds.width); 904 this.fixSize(/*ref*/ height, bounds.top, this.bounds.height); 905 906 bounds.width = width; 907 bounds.height = height; 908 909 return TextBufferRange(this._buffer, bounds); 910 } 911 912 @safe @nogc nothrow pure: 913 914 /// The bounds that this `TextWriter` is constrained to. 915 @property 916 TextBufferBounds bounds() const 917 { 918 return this._bounds; 919 } 920 921 /// Set the foreground for any newly-written characters. 922 /// Returns: `this`, for function chaining. 923 @property 924 TextBufferWriter fg(AnsiColour fg) { this._fg = fg; return this; } 925 /// Set the background for any newly-written characters. 926 /// Returns: `this`, for function chaining. 927 @property 928 TextBufferWriter bg(AnsiColour bg) { this._bg = bg; return this; } 929 /// Set the flags for any newly-written characters. 930 /// Returns: `this`, for function chaining. 931 @property 932 TextBufferWriter flags(AnsiTextFlags flags) { this._flags = flags; return this; } 933 934 /// Get the foreground. 935 @property 936 AnsiColour fg() { return this._fg; } 937 /// Get the foreground. 938 @property 939 AnsiColour bg() { return this._bg; } 940 /// Get the foreground. 941 @property 942 AnsiTextFlags flags() { return this._flags; } 943 } 944 /// 945 @safe 946 unittest 947 { 948 import std.format : format; 949 import jaster.cli.ansi; 950 951 auto buffer = new TextBuffer(5, 4); 952 auto writer = buffer.createWriter(1, 1, 3, 2); // Offset X, Offset Y, Width, Height. 953 auto fullGridWriter = buffer.createWriter(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 954 955 // Clear grid to be just spaces. 956 fullGridWriter.fill(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE, ' '); 957 958 // Write some stuff in the center. 959 with(writer) 960 { 961 set(0, 0, 'A'); 962 set(1, 0, 'B'); 963 set(2, 0, 'C'); 964 965 fg = AnsiColour(Ansi4BitColour.green); 966 967 set(0, 1, 'D'); 968 set(1, 1, 'E'); 969 set(2, 1, 'F'); 970 } 971 972 assert(buffer.toStringNoDupe() == 973 " " 974 ~" ABC " 975 ~" \033[32mDEF\033[0m " // \033 stuff is of course, the ANSI codes. In this case, green foreground, as we set above. 976 ~" ", 977 978 buffer.toStringNoDupe() ~ "\n%s".format(buffer.toStringNoDupe()) 979 ); 980 981 assert(writer.get(1, 1) == AnsiChar(AnsiColour(Ansi4BitColour.green), AnsiColour.bgInit, AnsiTextFlags.none, 'E')); 982 } 983 984 @("Testing that basic operations work") 985 @safe 986 unittest 987 { 988 import std.format : format; 989 990 auto buffer = new TextBuffer(3, 3); 991 auto writer = buffer.createWriter(1, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 992 993 foreach(ref ch; buffer._chars) 994 ch.value = '0'; 995 996 writer.set(0, 0, 'A'); 997 writer.set(1, 0, 'B'); 998 writer.set(0, 1, 'C'); 999 writer.set(1, 1, 'D'); 1000 writer.set(1, 2, 'E'); 1001 1002 assert(buffer.toStringNoDupe() == 1003 "0AB" 1004 ~"0CD" 1005 ~"00E" 1006 , "%s".format(buffer.toStringNoDupe())); 1007 } 1008 1009 @("Testing that ANSI works (but only when the entire thing is a single ANSI command)") 1010 @safe 1011 unittest 1012 { 1013 import std.format : format; 1014 import jaster.cli.ansi : AnsiText, Ansi4BitColour, AnsiColour; 1015 1016 auto buffer = new TextBuffer(3, 1); 1017 auto writer = buffer.createWriter(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1018 1019 writer.fg = AnsiColour(Ansi4BitColour.green); 1020 writer.set(0, 0, 'A'); 1021 writer.set(1, 0, 'B'); 1022 writer.set(2, 0, 'C'); 1023 1024 assert(buffer.toStringNoDupe() == 1025 "\033[%smABC%s".format(cast(int)Ansi4BitColour.green, AnsiText.RESET_COMMAND) 1026 , "%s".format(buffer.toStringNoDupe())); 1027 } 1028 1029 @("Testing that a mix of ANSI and plain text works") 1030 @safe 1031 unittest 1032 { 1033 import std.format : format; 1034 import jaster.cli.ansi : AnsiText, Ansi4BitColour, AnsiColour; 1035 1036 auto buffer = new TextBuffer(3, 4); 1037 auto writer = buffer.createWriter(0, 1, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1038 1039 buffer.createWriter(0, 0, 3, 1).fill(0, 0, 3, 1, ' '); // So we can also test that the y-offset works. 1040 1041 writer.fg = AnsiColour(Ansi4BitColour.green); 1042 writer.set(0, 0, 'A'); 1043 writer.set(1, 0, 'B'); 1044 writer.set(2, 0, 'C'); 1045 1046 writer.fg = AnsiColour.init; 1047 writer.set(0, 1, 'D'); 1048 writer.set(1, 1, 'E'); 1049 writer.set(2, 1, 'F'); 1050 1051 writer.fg = AnsiColour(Ansi4BitColour.green); 1052 writer.set(0, 2, 'G'); 1053 writer.set(1, 2, 'H'); 1054 writer.set(2, 2, 'I'); 1055 1056 assert(buffer.toStringNoDupe() == 1057 " " 1058 ~"\033[%smABC%s".format(cast(int)Ansi4BitColour.green, AnsiText.RESET_COMMAND) 1059 ~"DEF" 1060 ~"\033[%smGHI%s".format(cast(int)Ansi4BitColour.green, AnsiText.RESET_COMMAND) 1061 , "%s".format(buffer.toStringNoDupe())); 1062 } 1063 1064 @("Various fill tests") 1065 @safe 1066 unittest 1067 { 1068 auto buffer = new TextBuffer(5, 4); 1069 auto writer = buffer.createWriter(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1070 1071 writer.fill(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE, ' '); // Entire grid fill 1072 auto spaces = new char[buffer._chars.length]; 1073 spaces[] = ' '; 1074 assert(buffer.toStringNoDupe() == spaces); 1075 1076 writer.fill(0, 0, TextBuffer.USE_REMAINING_SPACE, 1, 'A'); // Entire line fill 1077 assert(buffer.toStringNoDupe()[0..5] == "AAAAA"); 1078 1079 writer.fill(1, 1, TextBuffer.USE_REMAINING_SPACE, 1, 'B'); // Partial line fill with X-offset and automatic width. 1080 assert(buffer.toStringNoDupe()[5..10] == " BBBB", buffer.toStringNoDupe()[5..10]); 1081 1082 writer.fill(1, 2, 3, 2, 'C'); // Partial line fill, across multiple lines. 1083 assert(buffer.toStringNoDupe()[10..20] == " CCC CCC "); 1084 } 1085 1086 @("Issue with TextBufferRange length") 1087 @safe 1088 unittest 1089 { 1090 import std.range : walkLength; 1091 1092 auto buffer = new TextBuffer(3, 2); 1093 auto writer = buffer.createWriter(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1094 auto range = writer.getArea(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1095 1096 foreach(i; 0..6) 1097 { 1098 assert(!range.empty); 1099 assert(range.length == 6 - i); 1100 range.popFront(); 1101 } 1102 assert(range.empty); 1103 assert(range.length == 0); 1104 } 1105 1106 @("Test TextBufferRange") 1107 @safe 1108 unittest 1109 { 1110 import std.algorithm : equal; 1111 import std.format : format; 1112 import std.range : walkLength; 1113 import jaster.cli.ansi : AnsiColour, AnsiTextFlags; 1114 1115 auto buffer = new TextBuffer(3, 2); 1116 auto writer = buffer.createWriter(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1117 1118 with(writer) 1119 { 1120 set(0, 0, 'A'); 1121 set(1, 0, 'B'); 1122 set(2, 0, 'C'); 1123 set(0, 1, 'D'); 1124 set(1, 1, 'E'); 1125 set(2, 1, 'F'); 1126 } 1127 1128 auto range = writer.getArea(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1129 assert(range.walkLength == buffer.toStringNoDupe().length, "%s != %s".format(range.walkLength, buffer.toStringNoDupe().length)); 1130 assert(range.equal!"a.value == b"(buffer.toStringNoDupe())); 1131 1132 range = writer.getArea(1, 1, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1133 assert(range.walkLength == 2); 1134 1135 range = writer.getArea(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1136 range[0] = '1'; 1137 range[1] = '2'; 1138 range[2] = '3'; 1139 1140 foreach(i; 0..3) 1141 range.popFront(); 1142 1143 // Since the range has been popped, the indicies have moved forward. 1144 range[1] = AnsiChar(AnsiColour.init, AnsiColour.init, AnsiTextFlags.none, '4'); 1145 1146 assert(buffer.toStringNoDupe() == "123D4F", buffer.toStringNoDupe()); 1147 } 1148 1149 @("Test write") 1150 @safe 1151 unittest 1152 { 1153 import std.format : format; 1154 import jaster.cli.ansi : AnsiColour, AnsiTextFlags, ansi, Ansi4BitColour; 1155 1156 auto buffer = new TextBuffer(6, 4); 1157 auto writer = buffer.createWriter(1, 1, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1158 1159 buffer.createWriter(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE) 1160 .fill(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE, ' '); 1161 writer.write(0, 0, "Hello World!"); 1162 1163 assert(buffer.toStringNoDupe() == 1164 " " 1165 ~" Hello" 1166 ~" World" 1167 ~" ! ", 1168 buffer.toStringNoDupe() 1169 ); 1170 1171 writer.write(1, 2, "Pog".ansi.fg(Ansi4BitColour.green)); 1172 assert(buffer.toStringNoDupe() == 1173 " " 1174 ~" Hello" 1175 ~" World" 1176 ~" !\033[32mPog\033[0m ", 1177 buffer.toStringNoDupe() 1178 ); 1179 } 1180 1181 @("Test addNewLine mode") 1182 @safe 1183 unittest 1184 { 1185 import jaster.cli.ansi : AnsiColour, Ansi4BitColour; 1186 1187 auto buffer = new TextBuffer(3, 3, TextBufferOptions(TextBufferLineMode.addNewLine)); 1188 auto writer = buffer.createWriter(0, 0, TextBuffer.USE_REMAINING_SPACE, TextBuffer.USE_REMAINING_SPACE); 1189 1190 writer.write(0, 0, "ABC DEF GHI"); 1191 assert(buffer.toStringNoDupe() == 1192 "ABC\n" 1193 ~"DEF\n" 1194 ~"GHI" 1195 ); 1196 1197 writer.fg = AnsiColour(Ansi4BitColour.green); 1198 writer.write(0, 0, "ABC DEF GHI"); 1199 assert(buffer.toStringNoDupe() == 1200 "\033[32mABC\n" 1201 ~"DEF\n" 1202 ~"GHI\033[0m" 1203 ); 1204 } 1205 1206 @("Test height changes") 1207 @safe 1208 unittest 1209 { 1210 auto buffer = new TextBuffer(4, 0); 1211 auto leftColumn = buffer.createWriter(0, 0, 1, TextBuffer.USE_REMAINING_SPACE); 1212 1213 void addLine(string text) 1214 { 1215 buffer.height = buffer.height + 1; 1216 auto writer = buffer.createWriter(1, buffer.height - 1, 3, 1); 1217 1218 leftColumn.updateSize(); 1219 leftColumn.fill(0, 0, 1, TextBuffer.USE_REMAINING_SPACE, '#'); 1220 1221 writer.write(0, 0, text); 1222 } 1223 1224 addLine("lol"); 1225 addLine("omg"); 1226 addLine("owo"); 1227 1228 assert(buffer.toStringNoDupe() == 1229 "#lol" 1230 ~"#omg" 1231 ~"#owo" 1232 ); 1233 1234 buffer.height = 2; 1235 assert(buffer.toStringNoDupe() == 1236 "#lol" 1237 ~"#omg", 1238 buffer.toStringNoDupe() 1239 ); 1240 } 1241 1242 /++ 1243 + Determines how a `TextBuffer` handles writing out each of its internal "lines". 1244 + ++/ 1245 enum TextBufferLineMode 1246 { 1247 /// Each "line" inside of the `TextBuffer` is written sequentially, without any non-explicit new lines between them. 1248 sideBySide, 1249 1250 /// Each "line" inside of the `TextBuffer` is written sequentially, with an automatically inserted new line between them. 1251 /// Note that the inserted new line doesn't count towards the character limit for each line. 1252 addNewLine 1253 } 1254 1255 /++ 1256 + Options for a `TextBuffer` 1257 + ++/ 1258 struct TextBufferOptions 1259 { 1260 /// Determines how a `TextBuffer` writes each of its "lines". 1261 TextBufferLineMode lineMode = TextBufferLineMode.sideBySide; 1262 } 1263 1264 // I want reference semantics. 1265 /++ 1266 + An ANSI-enabled class used to easily manipulate a text buffer of a fixed width and height. 1267 + 1268 + Description: 1269 + This class was inspired by the GPU component from the OpenComputers Minecraft mod. 1270 + 1271 + I thought having something like this, where you can easily manipulate a 2D grid of characters (including colour and the like) 1272 + would be quite valuable. 1273 + 1274 + For example, the existence of this class can be the stepping stone into various other things such as: a basic (and I mean basic) console-based UI functionality; 1275 + other ANSI-enabled components such as tables which can otherwise be a pain due to the non-uniform length of ANSI text (e.g. ANSI codes being invisible characters), 1276 + and so on. 1277 + 1278 + Examples: 1279 + For now you'll have to explore the source (text.d) and have a look at the module-level unittests to see some testing examples. 1280 + 1281 + When I can be bothered, I'll add user-facing examples :) 1282 + 1283 + Limitations: 1284 + Currently the buffer can only be resized vertically, not horizontally. 1285 + 1286 + This is due to how the memory's laid out, resizing vertically requires a slightly more complicated algorithm that I'm too lazy to do right now. 1287 + 1288 + Creating the final string output (via `toString` or `toStringNoDupe`) is unoptimised. It performs pretty well for a 180x180 buffer with a sparing amount of colours, 1289 + but don't expect it to perform too well right now. 1290 + One big issue is that *any* change will cause the entire output to be reconstructed, which I'm sure can be changed to be a bit more optimal. 1291 + ++/ 1292 @safe 1293 final class TextBuffer 1294 { 1295 /// Used to specify that a writer's width or height should use all the space it can. 1296 enum USE_REMAINING_SPACE = size_t.max; 1297 1298 private 1299 { 1300 AnsiChar[] _charsBuffer; 1301 AnsiChar[] _chars; 1302 size_t _width; 1303 size_t _height; 1304 1305 char[] _output; 1306 char[] _cachedOutput; 1307 1308 TextBufferOptions _options; 1309 1310 @nogc 1311 void makeDirty() nothrow pure 1312 { 1313 this._cachedOutput = null; 1314 } 1315 } 1316 1317 /++ 1318 + Creates a new `TextBuffer` with the specified width and height. 1319 + 1320 + Params: 1321 + width = How many characters each line can contain. 1322 + height = How many lines in total there are. 1323 + options = Configuration options for this `TextBuffer`. 1324 + ++/ 1325 this(size_t width, size_t height, TextBufferOptions options = TextBufferOptions.init) nothrow pure 1326 { 1327 this._width = width; 1328 this._height = height; 1329 this._options = options; 1330 this._charsBuffer.length = width * height; 1331 this._chars = this._charsBuffer[0..$]; 1332 } 1333 1334 /++ 1335 + Creates a new `TextBufferWriter` bound to this `TextBuffer`. 1336 + 1337 + Description: 1338 + The only way to read and write to certain sections of a `TextBuffer` is via the `TextBufferWriter`. 1339 + 1340 + Writers are constrained to the given `bounds`, allowing careful control of where certain parts of your code are allowed to modify. 1341 + 1342 + Params: 1343 + bounds = The bounds to constrain the writer to. 1344 + You can use `TextBuffer.USE_REMAINING_SPACE` as the width and height to specify that the writer's width/height will use 1345 + all the space that they have available. 1346 + 1347 + Returns: 1348 + A `TextBufferWriter`, constrained to the given `bounds`, which is also bounded to this specific `TextBuffer`. 1349 + ++/ 1350 @nogc 1351 TextBufferWriter createWriter(TextBufferBounds bounds) nothrow pure 1352 { 1353 return TextBufferWriter(this, bounds); 1354 } 1355 1356 /// ditto. 1357 @nogc 1358 TextBufferWriter createWriter(size_t left, size_t top, size_t width = USE_REMAINING_SPACE, size_t height = USE_REMAINING_SPACE) nothrow pure 1359 { 1360 return this.createWriter(TextBufferBounds(left, top, width, height)); 1361 } 1362 1363 /// Returns: The height of this `TextBuffer`. 1364 @property @nogc 1365 size_t height() const nothrow pure 1366 { 1367 return this._height; 1368 } 1369 1370 /++ 1371 + Sets the height (number of lines) for this `TextBuffer`. 1372 + 1373 + Notes: 1374 + `TextBufferWriters` will not automatically update their sizes to take into account this size change. 1375 + 1376 + You will have to manually call `TextBufferWriter.updateSize` to reflect any changes, such as properly updating 1377 + writers that use `TextBuffer.USE_REMAINING_SPACE` as one of their sizes. 1378 + 1379 + Performance: 1380 + As is a common theme with this class, it will try to reuse an internal buffer. 1381 + 1382 + So if you're shrinking the height, no allocations should be made. 1383 + 1384 + If you're growing the height, allocations are only made if the new height is larger than its ever been set to. 1385 + 1386 + As a side effect, whenever you grow the buffer the data that occupies the new space will either be `AnsiChar.init`, or whatever 1387 + was previously left there. 1388 + 1389 + Side_Effects: 1390 + This function clears any cached output, so `toStringNoDupe` and `toString` will have to completely reconstruct the output. 1391 + 1392 + Any `TextBufferWriter`s with a non-dynamic size (e.g. no `TextBuffer.USE_REMAINING_SPACE`) that end up becoming out-of-bounds, 1393 + will not be useable until they're remade, or until the height is changed again. 1394 + 1395 + Any `TextBufferRange`s that exist prior to resizing are not affected in anyway, and can still access parts of the buffer that 1396 + should now technically be "out of bounds" (in the event of shrinking). 1397 + 1398 + Params: 1399 + lines = The new amount of lines. 1400 + ++/ 1401 @property 1402 void height(size_t lines) nothrow 1403 { 1404 this._height = lines; 1405 const newCount = this._width * this._height; 1406 1407 if(newCount > this._charsBuffer.length) 1408 this._charsBuffer.length = newCount; 1409 1410 this._chars = this._charsBuffer[0..newCount]; 1411 this._cachedOutput = null; 1412 } 1413 1414 // This is a very slow (well, I assume, tired code is usually slow code), very naive function, but as long as it works for now, then it can be rewritten later. 1415 /++ 1416 + Converts this `TextBuffer` into a string. 1417 + 1418 + Description: 1419 + The value returned by this function is a slice into an internal buffer. This buffer gets 1420 + reused between every call to this function. 1421 + 1422 + So basically, if you don't need the guarentees of `immutable` (which `toString` will provide), and are aware 1423 + that the value from this function can and will change, then it is faster to use this function as otherwise, with `toString`, 1424 + a call to `.idup` is made. 1425 + 1426 + Optimisation: 1427 + This function isn't terribly well optimised in all honesty, but that isn't really too bad of an issue because, at least on 1428 + my computer, it only takes around 2ms to create the output for a 180x180 grid, in the worst case scenario - where every character 1429 + requires a different ANSI code. 1430 + 1431 + Worst case is O(3n), best case is O(2n), backtracking only occurs whenever a character is found that cannot be written with the same ANSI codes 1432 + as the previous one(s), so the worst case only occurs if every single character requires a new ANSI code. 1433 + 1434 + Small test output - `[WORST CASE | SHARED] ran 1000 times -> 1 sec, 817 ms, 409 us, and 5 hnsecs -> AVERAGING -> 1 ms, 817 us, and 4 hnsecs` 1435 + 1436 + While I haven't tested memory usage, by default all of the initial allocations will be `(width * height) * 2`, which is then reused between runs. 1437 + 1438 + This function will initially set the internal buffer to `width * height * 2` in an attempt to overallocate more than it needs. 1439 + 1440 + This function caches its output (using the same buffer), and will reuse the cached output as long as there hasn't been any changes. 1441 + 1442 + If there *have* been changes, then the internal buffer is simply reused without clearing or reallocation. 1443 + 1444 + Finally, this function will automatically group together ranges of characters that can be used with the same ANSI codes, as an 1445 + attempt to minimise the amount of characters actually required. So the more characters in a row there are that are the same colour and style, 1446 + the faster this function performs. 1447 + 1448 + Returns: 1449 + An (internally mutable) slice to this class' output buffer. 1450 + ++/ 1451 const(char[]) toStringNoDupe() 1452 { 1453 import std.algorithm : joiner; 1454 import std.utf : byChar; 1455 import jaster.cli.ansi : AnsiComponents, populateActiveAnsiComponents, AnsiText; 1456 1457 if(this._output is null) 1458 this._output = new char[this._chars.length * 2]; // Optimistic overallocation, to lower amount of resizes 1459 1460 if(this._cachedOutput !is null) 1461 return this._cachedOutput; 1462 1463 size_t outputCursor; 1464 size_t ansiCharCursor; 1465 size_t nonAnsiCount; 1466 1467 // Auto-increments outputCursor while auto-resizing the output buffer. 1468 void putChar(char c, bool isAnsiChar) 1469 { 1470 if(!isAnsiChar) 1471 nonAnsiCount++; 1472 1473 if(outputCursor >= this._output.length) 1474 this._output.length *= 2; 1475 1476 this._output[outputCursor++] = c; 1477 1478 // Add new lines if the option is enabled. 1479 if(!isAnsiChar 1480 && this._options.lineMode == TextBufferLineMode.addNewLine 1481 && nonAnsiCount == this._width) 1482 { 1483 this._output[outputCursor++] = '\n'; 1484 nonAnsiCount = 0; 1485 } 1486 } 1487 1488 // Finds the next sequence of characters that have the same foreground, background, and flags. 1489 // e.g. the next sequence of characters that can be used with the same ANSI command. 1490 // Returns `null` once we reach the end. 1491 AnsiChar[] getNextAnsiRange() 1492 { 1493 if(ansiCharCursor >= this._chars.length) 1494 return null; 1495 1496 const startCursor = ansiCharCursor; 1497 const start = this._chars[ansiCharCursor++]; 1498 while(ansiCharCursor < this._chars.length) 1499 { 1500 auto current = this._chars[ansiCharCursor++]; 1501 current.value = start.value; // cheeky way to test equality. 1502 if(start != current) 1503 { 1504 ansiCharCursor--; 1505 break; 1506 } 1507 } 1508 1509 return this._chars[startCursor..ansiCharCursor]; 1510 } 1511 1512 auto sequence = getNextAnsiRange(); 1513 while(sequence.length > 0) 1514 { 1515 const first = sequence[0]; // Get the first character so we can get the ANSI settings for the entire sequence. 1516 if(first.usesAnsi) 1517 { 1518 AnsiComponents components; 1519 const activeCount = components.populateActiveAnsiComponents(first.fg, first.bg, first.flags); 1520 1521 putChar('\033', true); 1522 putChar('[', true); 1523 foreach(ch; components[0..activeCount].joiner(";").byChar) 1524 putChar(ch, true); 1525 putChar('m', true); 1526 } 1527 1528 foreach(ansiChar; sequence) 1529 putChar(ansiChar.value, false); 1530 1531 // Remove final new line, since it's more expected for it not to be there. 1532 if(this._options.lineMode == TextBufferLineMode.addNewLine 1533 && ansiCharCursor >= this._chars.length 1534 && this._output[outputCursor - 1] == '\n') 1535 outputCursor--; // For non ANSI, we just leave the \n orphaned, for ANSI, we just overwrite it with the reset command below. 1536 1537 if(first.usesAnsi) 1538 { 1539 foreach(ch; AnsiText.RESET_COMMAND) 1540 putChar(ch, true); 1541 } 1542 1543 sequence = getNextAnsiRange(); 1544 } 1545 1546 this._cachedOutput = this._output[0..outputCursor]; 1547 return this._cachedOutput; 1548 } 1549 1550 /// Returns: `toStringNoDupe`, but then `.idup`s the result. 1551 override string toString() 1552 { 1553 return this.toStringNoDupe().idup; 1554 } 1555 }