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 }