1 /// Utilities to create ANSI coloured text.
2 module jaster.cli.ansi;
3 
4 import std.traits : EnumMembers, isSomeChar;
5 import std.typecons : Flag;
6 
7 alias IsBgColour = Flag!"isBackgroundAnsi";
8 
9 /++
10  + Defines what type of colour an `AnsiColour` stores.
11  + ++/
12 enum AnsiColourType
13 {
14     /// Default, failsafe.
15     none,
16 
17     /// 4-bit colours.
18     fourBit,
19 
20     /// 8-bit colours.
21     eightBit,
22 
23     /// 24-bit colours.
24     rgb
25 }
26 
27 /++
28  + An enumeration of standard 4-bit colours.
29  +
30  + These colours will have the widest support between platforms.
31  + ++/
32 enum Ansi4BitColour
33 {
34     // To get Background code, just add 10
35     black           = 30,
36     red             = 31,
37     green           = 32,
38     /// On Powershell, this is displayed as a very white colour.
39     yellow          = 33,
40     blue            = 34,
41     magenta         = 35,
42     cyan            = 36,
43     /// More gray than true white, use `BrightWhite` for true white.
44     white           = 37,
45     /// Grayer than `White`.
46     brightBlack     = 90,
47     brightRed       = 91,
48     brightGreen     = 92,
49     brightYellow    = 93,
50     brightBlue      = 94,
51     brightMagenta   = 95,
52     brightCyan      = 96,
53     brightWhite     = 97
54 }
55 
56 private union AnsiColourUnion
57 {
58     Ansi4BitColour fourBit;
59     ubyte          eightBit;
60     AnsiRgbColour  rgb;
61 }
62 
63 /// A very simple RGB struct, used to store an RGB value.
64 struct AnsiRgbColour
65 {
66     /// The red component.
67     ubyte r;
68     
69     /// The green component.
70     ubyte g;
71 
72     /// The blue component.
73     ubyte b;
74 }
75 
76 /++
77  + Contains either a 4-bit, 8-bit, or 24-bit colour, which can then be turned into
78  + an its ANSI form (not a valid command, just the actual values needed to form the final command).
79  + ++/
80 @safe
81 struct AnsiColour
82 {
83     private 
84     {
85         AnsiColourUnion _value;
86         AnsiColourType  _type;
87         IsBgColour      _isBg;
88 
89         this(IsBgColour isBg)
90         {
91             this._isBg = isBg;
92         }
93     }
94 
95     /// A variant of `.init` that is used for background colours.
96     static immutable bgInit = AnsiColour(IsBgColour.yes);
97 
98     /// Ctor for an `AnsiColourType.fourBit`.
99     @nogc
100     this(Ansi4BitColour fourBit, IsBgColour isBg = IsBgColour.no) nothrow pure
101     {
102         this._value.fourBit = fourBit;
103         this._type          = AnsiColourType.fourBit;
104         this._isBg          = isBg;
105     }
106 
107     /// Ctor for an `AnsiColourType.eightBit`
108     @nogc
109     this(ubyte eightBit, IsBgColour isBg = IsBgColour.no) nothrow pure
110     {
111         this._value.eightBit = eightBit;
112         this._type           = AnsiColourType.eightBit;
113         this._isBg           = isBg;
114     }
115 
116     /// Ctor for an `AnsiColourType.rgb`.
117     @nogc
118     this(ubyte r, ubyte g, ubyte b, IsBgColour isBg = IsBgColour.no) nothrow pure
119     {        
120         this._value.rgb = AnsiRgbColour(r, g, b);
121         this._type      = AnsiColourType.rgb;
122         this._isBg      = isBg;
123     }
124 
125     /// ditto
126     @nogc
127     this(AnsiRgbColour rgb, IsBgColour isBg = IsBgColour.no) nothrow pure
128     {
129         this(rgb.r, rgb.g, rgb.b, isBg);
130     }
131 
132     /++
133      + Notes:
134      +  To create a valid ANSI command from these values, prefix it with "\033[" and suffix it with "m", then place your text after it.
135      +
136      + Returns:
137      +  This `AnsiColour` as an incomplete ANSI command.
138      + ++/
139     string toString() const pure
140     {
141         import std.format : format;
142 
143         final switch(this._type) with(AnsiColourType)
144         {
145             case none: return null;
146             case fourBit:
147                 auto value = cast(int)this._value.fourBit;
148                 return "%s".format(this._isBg ? value + 10 : value);
149 
150             case eightBit:
151                 auto marker = (this._isBg) ? "48" : "38";
152                 auto value  = this._value.eightBit;
153                 return "%s;5;%s".format(marker, value);
154 
155             case rgb:
156                 auto marker = (this._isBg) ? "48" : "38";
157                 auto value  = this._value.rgb;
158                 return "%s;2;%s;%s;%s".format(marker, value.r, value.g, value.b);
159         }
160     }
161 
162     @safe @nogc nothrow pure:
163     
164     /// Returns: The `AnsiColourType` of this `AnsiColour`.
165     @property
166     AnsiColourType type() const
167     {
168         return this._type;
169     }
170 
171     /// Returns: Whether this `AnsiColour` is for a background or not (it affects the output!).
172     @property
173     IsBgColour isBg() const
174     {
175         return this._isBg;
176     }
177 
178     /// ditto
179     @property
180     void isBg(IsBgColour bg)
181     {
182         this._isBg = bg;
183     }
184 
185     /// ditto
186     @property
187     void isBg(bool bg)
188     {
189         this._isBg = cast(IsBgColour)bg;
190     }
191 
192     /++
193      + Assertions:
194      +  This colour's type must be `AnsiColourType.fourBit`
195      +
196      + Returns:
197      +  This `AnsiColour` as an `Ansi4BitColour`.
198      + ++/
199     @property
200     Ansi4BitColour asFourBit()
201     {
202         assert(this.type == AnsiColourType.fourBit);
203         return this._value.fourBit;
204     }
205 
206     /++
207      + Assertions:
208      +  This colour's type must be `AnsiColourType.eightBit`
209      +
210      + Returns:
211      +  This `AnsiColour` as a `ubyte`.
212      + ++/
213     @property
214     ubyte asEightBit()
215     {
216         assert(this.type == AnsiColourType.eightBit);
217         return this._value.eightBit;
218     }
219 
220     /++
221      + Assertions:
222      +  This colour's type must be `AnsiColourType.rgb`
223      +
224      + Returns:
225      +  This `AnsiColour` as an `AnsiRgbColour`.
226      + ++/
227     @property
228     AnsiRgbColour asRgb()
229     {
230         assert(this.type == AnsiColourType.rgb);
231         return this._value.rgb;
232     }
233 }
234 
235 enum AnsiTextFlags
236 {
237     none      = 0,
238     bold      = 1 << 0,
239     dim       = 1 << 1,
240     italic    = 1 << 2,
241     underline = 1 << 3,
242     slowBlink = 1 << 4,
243     fastBlink = 1 << 5,
244     invert    = 1 << 6,
245     strike    = 1 << 7
246 }
247 
248 private immutable FLAG_COUNT = EnumMembers!AnsiTextFlags.length - 1; // - 1 to ignore the `none` option
249 private immutable FLAG_AS_ANSI_CODE_MAP = 
250 [
251     // Index correlates to the flag's position in the bitmap.
252     // So bold would be index 0.
253     // Strike would be index 7, etc.
254     
255     "1", // 0
256     "2", // 1
257     "3", // 2
258     "4", // 3
259     "5", // 4
260     "6", // 5
261     "7", // 6
262     "9"  // 7
263 ];
264 static assert(FLAG_AS_ANSI_CODE_MAP.length == FLAG_COUNT);
265 
266 /// An alias for a string[] containing exactly enough elements for the following ANSI strings:
267 ///
268 /// * [0]    = Foreground ANSI code.
269 /// * [1]    = Background ANSI code.
270 /// * [2..n] = The code for any `AnsiTextFlags` that are set.
271 alias AnsiComponents = string[2 + FLAG_COUNT]; // fg + bg + all supported flags.
272 
273 /++
274  + Populates an `AnsiComponents` with all the strings required to construct a full ANSI command string.
275  +
276  + Params:
277  +  components = The `AnsiComponents` to populate. $(B All values will be set to null before hand).
278  +  fg         = The `AnsiColour` representing the foreground.
279  +  bg         = The `AnsiColour` representing the background.
280  +  flags      = The `AnsiTextFlags` to apply.
281  +
282  + Returns:
283  +  How many components in total are active.
284  +
285  + See_Also:
286  +  `createAnsiCommandString` to create an ANSI command string from an `AnsiComponents`.
287  + ++/
288 @safe
289 size_t populateActiveAnsiComponents(ref scope AnsiComponents components, AnsiColour fg, AnsiColour bg, AnsiTextFlags flags) pure
290 {
291     size_t componentIndex;
292     components[] = null;
293 
294     if(fg.type != AnsiColourType.none)
295         components[componentIndex++] = fg.toString();
296 
297     if(bg.type != AnsiColourType.none)
298         components[componentIndex++] = bg.toString();
299 
300     foreach(i; 0..FLAG_COUNT)
301     {
302         if((flags & (1 << i)) > 0)
303             components[componentIndex++] = FLAG_AS_ANSI_CODE_MAP[i];
304     }
305 
306     return componentIndex;
307 }
308 
309 /++
310  + Creates an ANSI command string using the given active `components`.
311  +
312  + Params:
313  +  components = An `AnsiComponents` that has been populated with flags, ideally from `populateActiveAnsiComponents`.
314  +
315  + Returns:
316  +  All of the component strings inside of `components`, formatted as a valid ANSI command string.
317  + ++/
318 @safe
319 string createAnsiCommandString(ref scope AnsiComponents components) pure
320 {
321     import std.algorithm : joiner, filter;
322     import std.format    : format;
323 
324     return "\033[%sm".format(components[].filter!(s => s !is null).joiner(";")); 
325 }
326 
327 /// Contains a single character, with ANSI styling.
328 @safe
329 struct AnsiChar 
330 {
331     import jaster.cli.ansi : AnsiColour, AnsiTextFlags, IsBgColour;
332 
333     /// foreground
334     AnsiColour    fg;
335     /// background by reference
336     AnsiColour    bgRef;
337     /// flags
338     AnsiTextFlags flags;
339     /// character
340     char          value;
341 
342     @nogc nothrow pure:
343 
344     /++
345      + Returns:
346      +  Whether this character needs an ANSI control code or not.
347      + ++/
348     @property
349     bool usesAnsi() const
350     {
351         return this.fg    != AnsiColour.init
352             || (this.bg   != AnsiColour.init && this.bg != AnsiColour.bgInit)
353             || this.flags != AnsiTextFlags.none;
354     }
355 
356     /// Set the background (automatically sets `value.isBg` to `yes`)
357     @property
358     void bg(AnsiColour value)
359     {
360         value.isBg = IsBgColour.yes;
361         this.bgRef = value;
362     }
363 
364     /// Get the background.
365     @property
366     AnsiColour bg() const { return this.bgRef; }
367 }
368 
369 /++
370  + A struct used to compose together a piece of ANSI text.
371  +
372  + Notes:
373  +  A reset command (`\033[0m`) is automatically appended, so you don't have to worry about that.
374  +
375  +  This struct is simply a wrapper around `AnsiColour`, `AnsiTextFlags` types, and the `populateActiveAnsiComponents` and
376  +  `createAnsiCommandString` functions.
377  +
378  + Usage:
379  +  This struct uses the Fluent Builder pattern, so you can easily string together its
380  +  various functions when creating your text.
381  +
382  +  Set the background colour with `AnsiText.bg`
383  +
384  +  Set the foreground/text colour with `AnsiText.fg`
385  +
386  +  AnsiText uses `toString` to provide the final output, making it easily used with the likes of `writeln` and `format`.
387  + ++/
388 @safe
389 struct AnsiText
390 {
391     import std.format : format;
392 
393     /// The ANSI command to reset all styling.
394     public static const RESET_COMMAND = "\033[0m";
395 
396     @nogc
397     private nothrow pure
398     {
399         string        _cachedText;
400         const(char)[] _text;
401         AnsiColour    _fg;
402         AnsiColour    _bg;
403         AnsiTextFlags _flags;
404 
405         ref AnsiText setColour(T)(ref AnsiColour colour, T value, IsBgColour isBg) return
406         {
407             static if(is(T == AnsiColour))
408             {
409                 colour      = value;
410                 colour.isBg = isBg;
411             }
412             else
413                 colour = AnsiColour(value, isBg);
414 
415             this._cachedText = null;
416             return this;
417         }
418 
419         ref AnsiText setColour4(ref AnsiColour colour, Ansi4BitColour value, IsBgColour isBg) return
420         {
421             return this.setColour(colour, Ansi4BitColour(value), isBg);
422         }
423 
424         ref AnsiText setColour8(ref AnsiColour colour, ubyte value, IsBgColour isBg) return
425         {
426             return this.setColour(colour, value, isBg);
427         }
428 
429         ref AnsiText setColourRgb(ref AnsiColour colour, ubyte r, ubyte g, ubyte b, IsBgColour isBg) return
430         {
431             return this.setColour(colour, AnsiRgbColour(r, g, b), isBg);
432         }
433 
434         ref AnsiText setFlag(AnsiTextFlags flag, bool isSet) return
435         {
436             if(isSet)
437                 this._flags |= flag;
438             else
439                 this._flags &= ~flag;
440 
441             this._cachedText = null;
442             return this;
443         }
444     }
445 
446     ///
447     @safe @nogc
448     this(const(char)[] text) nothrow pure
449     {
450         this._text = text;
451         this._bg.isBg = true;
452     }
453 
454     /++
455      + Notes:
456      +  If no ANSI escape codes are used, then this function will simply return a `.idup` of the
457      +  text provided to this struct's constructor.
458      +
459      + Returns:
460      +  The ANSI escape-coded text.
461      + ++/
462     @safe
463     string toString() pure
464     {
465         if(this._bg.type == AnsiColourType.none 
466         && this._fg.type == AnsiColourType.none
467         && this._flags   == AnsiTextFlags.none)
468             this._cachedText = this._text.idup;
469 
470         if(this._cachedText !is null)
471             return this._cachedText;
472 
473         // Find all 'components' that have been enabled
474         AnsiComponents components;
475         components.populateActiveAnsiComponents(this._fg, this._bg, this._flags);
476 
477         // Then join them together.
478         this._cachedText = "%s%s%s".format(
479             components.createAnsiCommandString(), 
480             this._text,
481             AnsiText.RESET_COMMAND
482         ); 
483         return this._cachedText;
484     }
485 
486     @safe @nogc nothrow pure:
487 
488     /// Sets the foreground/background as a 4-bit colour. Widest supported option.
489     ref AnsiText fg(Ansi4BitColour fourBit) return    { return this.setColour4  (this._fg, fourBit, IsBgColour.no);   }
490     /// ditto
491     ref AnsiText bg(Ansi4BitColour fourBit) return    { return this.setColour4  (this._bg, fourBit, IsBgColour.yes);  }
492 
493     /// Sets the foreground/background as an 8-bit colour. Please see this image for reference: https://i.stack.imgur.com/KTSQa.png
494     ref AnsiText fg(ubyte eightBit) return            { return this.setColour8  (this._fg, eightBit, IsBgColour.no);  }
495     /// ditto
496     ref AnsiText bg(ubyte eightBit) return            { return this.setColour8  (this._bg, eightBit, IsBgColour.yes); }
497 
498     /// Sets the forground/background as an RGB colour.
499     ref AnsiText fg(ubyte r, ubyte g, ubyte b) return { return this.setColourRgb(this._fg, r, g, b, IsBgColour.no);   }
500     /// ditto
501     ref AnsiText bg(ubyte r, ubyte g, ubyte b) return { return this.setColourRgb(this._bg, r, g, b, IsBgColour.yes);  }
502 
503     /// Sets the forground/background to an `AnsiColour`. Background colours will have their `isBg` flag set automatically.
504     ref AnsiText fg(AnsiColour colour) return { return this.setColour(this._fg, colour, IsBgColour.no);   }
505     /// ditto
506     ref AnsiText bg(AnsiColour colour) return { return this.setColour(this._bg, colour, IsBgColour.yes);  }
507 
508     /// Sets whether the text is bold.
509     ref AnsiText bold     (bool isSet = true) return { return this.setFlag(AnsiTextFlags.bold,      isSet); }
510     /// Sets whether the text is dimmed (opposite of bold).
511     ref AnsiText dim      (bool isSet = true) return { return this.setFlag(AnsiTextFlags.dim,       isSet); }
512     /// Sets whether the text should be displayed in italics.
513     ref AnsiText italic   (bool isSet = true) return { return this.setFlag(AnsiTextFlags.italic,    isSet); }
514     /// Sets whether the text has an underline.
515     ref AnsiText underline(bool isSet = true) return { return this.setFlag(AnsiTextFlags.underline, isSet); }
516     /// Sets whether the text should blink slowly.
517     ref AnsiText slowBlink(bool isSet = true) return { return this.setFlag(AnsiTextFlags.slowBlink, isSet); }
518     /// Sets whether the text should blink rapidly.
519     ref AnsiText fastBlink(bool isSet = true) return { return this.setFlag(AnsiTextFlags.fastBlink, isSet); }
520     /// Sets whether the text should have its fg and bg colours inverted.
521     ref AnsiText invert   (bool isSet = true) return { return this.setFlag(AnsiTextFlags.invert,    isSet); }
522     /// Sets whether the text should have a strike through it.
523     ref AnsiText strike   (bool isSet = true) return { return this.setFlag(AnsiTextFlags.strike,    isSet); }
524 
525     /// Sets the `AnsiTextFlags` for this piece of text.
526     ref AnsiText setFlags(AnsiTextFlags flags) return 
527     { 
528         this._flags = flags; 
529         return this; 
530     }
531 
532     /// Gets the `AnsiTextFlags` for this piece of text.
533     @property
534     AnsiTextFlags flags() const
535     {
536         return this._flags;
537     }
538 
539     /// Gets the `AnsiColour` used as the foreground (text colour).
540     //@property
541     AnsiColour fg() const
542     {
543         return this._fg;
544     }
545 
546     /// Gets the `AnsiColour` used as the background.
547     //@property
548     AnsiColour bg() const
549     {
550         return this._bg;
551     }
552 
553     /// Returns: The raw text of this `AnsiText`.
554     @property
555     const(char[]) rawText() const
556     {
557         return this._text;
558     }
559 
560     /// Sets the raw text used.
561     @property
562     void rawText(const char[] text)
563     {
564         this._text       = text;
565         this._cachedText = null;
566     }
567 }
568 
569 /++
570  + A helper UFCS function used to fluently convert any piece of text into an `AnsiText`.
571  + ++/
572 @safe @nogc
573 AnsiText ansi(const char[] text) nothrow pure
574 {
575     return AnsiText(text);
576 }
577 ///
578 @safe
579 unittest
580 {
581     assert("Hello".ansi.toString() == "Hello");
582     assert("Hello".ansi.fg(Ansi4BitColour.black).toString() == "\033[30mHello\033[0m");
583     assert("Hello".ansi.bold.strike.bold(false).italic.toString() == "\033[3;9mHello\033[0m");
584 }
585 
586 /// Describes whether an `AnsiSectionBase` contains a piece of text, or an ANSI escape sequence.
587 enum AnsiSectionType
588 {
589     /// Default/Failsafe value
590     ERROR,
591     text,
592     escapeSequence
593 }
594 
595 /++
596  + Contains an section of text, with an additional field to specify whether the
597  + section contains plain text, or an ANSI escape sequence.
598  +
599  + Params:
600  +  Char = What character type is used.
601  +
602  + See_Also:
603  +  `AnsiSection` alias for ease-of-use.
604  + ++/
605 @safe
606 struct AnsiSectionBase(Char)
607 if(isSomeChar!Char)
608 {
609     /// The type of data stored in this section.
610     AnsiSectionType type;
611 
612     /++
613      + The value of this section.
614      +
615      + Notes:
616      +  For sections that contain an ANSI sequence (`AnsiSectionType.escapeSequence`), the starting characters (`\033[`) and
617      +  ending character ('m') are stripped from this value.
618      + ++/
619     const(Char)[] value;
620 
621     // Making comparisons with enums can be a bit too clunky, so these helper functions should hopefully
622     // clean things up.
623 
624     @safe @nogc nothrow pure const:
625 
626     /// Returns: Whether this section contains plain text.
627     bool isTextSection()
628     {
629         return this.type == AnsiSectionType.text;
630     }
631 
632     /// Returns: Whether this section contains an ANSI escape sequence.
633     bool isSequenceSection()
634     {
635         return this.type == AnsiSectionType.escapeSequence;
636     }
637 }
638 
639 /// An `AnsiSectionBase` that uses `char` as the character type, a.k.a what's going to be used 99% of the time.
640 alias AnsiSection = AnsiSectionBase!char;
641 
642 /++
643  + An InputRange that turns an array of `Char`s into a range of `AnsiSection`s.
644  +
645  + This isn't overly useful on its own, and is mostly so other ranges can be built on top of this.
646  +
647  + Notes:
648  +  Please see `AnsiSectionBase.value`'s documentation comment, as it explains that certain characters of an ANSI sequence are
649  +  omitted from the final output (the starting `"\033["` and the ending `'m'` specifically).
650  +
651  + Limitations:
652  +  To prevent the need for allocations or possibly buggy behaviour regarding a reusable buffer, this range can only work directly
653  +  on arrays, and not any generic char range.
654  + ++/
655 @safe
656 struct AnsiSectionRange(Char)
657 if(isSomeChar!Char)
658 {
659     private
660     {
661         const(Char)[]        _input;
662         size_t               _index;
663         AnsiSectionBase!Char _current;
664     }
665 
666     @nogc pure nothrow:
667 
668     /// Creates an `AnsiSectionRange` from the given `input`.
669     this(const Char[] input)
670     {
671         this._input = input;
672         this.popFront();
673     }
674 
675     /// Returns: The latest-parsed `AnsiSection`.
676     AnsiSectionBase!Char front()
677     {
678         return this._current;
679     }
680 
681     /// Returns: Whether there's no more text to parse.
682     bool empty()
683     {
684         return this._current == AnsiSectionBase!Char.init;
685     }
686 
687     /// Parses the next section.
688     void popFront()
689     {
690         if(this._index >= this._input.length)
691         {
692             this._current = AnsiSectionBase!Char.init; // .empty condition.
693             return;
694         }
695         
696         const isNextSectionASequence = this._index <= this._input.length - 2
697                                     && this._input[this._index..this._index + 2] == "\033[";
698 
699         // Read until end, or until an 'm'.
700         if(isNextSectionASequence)
701         {
702             // Skip the start codes
703             this._index += 2;
704 
705             bool foundM  = false;
706             size_t start = this._index;
707             for(; this._index < this._input.length; this._index++)
708             {
709                 if(this._input[this._index] == 'm')
710                 {
711                     foundM = true;
712                     break;
713                 }
714             }
715 
716             // I don't know 100% what to do here, but, if we don't find an 'm' then we'll treat the sequence as text, since it's technically malformed.
717             this._current.value = this._input[start..this._index];
718             if(foundM)
719             {
720                 this._current.type = AnsiSectionType.escapeSequence;
721                 this._index++; // Skip over the 'm'
722             }
723             else
724                 this._current.type = AnsiSectionType.text;
725 
726             return;
727         }
728 
729         // Otherwise, read until end, or an ansi start sequence.
730         size_t start              = this._index;
731         bool   foundEscape        = false;
732         bool   foundStartSequence = false;
733 
734         for(; this._index < this._input.length; this._index++)
735         {
736             const ch = this._input[this._index];
737 
738             if(ch == '[' && foundEscape)
739             {
740                 foundStartSequence = true;
741                 this._index -= 1; // To leave the start code for the next call to popFront.
742                 break;
743             }
744 
745             foundEscape = (ch == '\033');
746         }
747 
748         this._current.value = this._input[start..this._index];
749         this._current.type  = AnsiSectionType.text;
750     }
751 }
752 ///
753 @("Test AnsiSectionRange for only text, only ansi, and a mixed string.")
754 @safe
755 unittest
756 {
757     import std.array : array;
758 
759     const onlyText = "Hello, World!";
760     const onlyAnsi = "\033[30m\033[0m";
761     const mixed    = "\033[30;1;2mHello, \033[0mWorld!";
762 
763     void test(string input, AnsiSection[] expectedSections)
764     {
765         import std.algorithm : equal;
766         import std.format    : format;
767 
768         auto range = input.asAnsiSections();
769         assert(range.equal(expectedSections), "Expected:\n%s\nGot:\n%s".format(expectedSections, range));
770     }
771 
772     test(onlyText, [AnsiSection(AnsiSectionType.text, "Hello, World!")]);
773     test(onlyAnsi, [AnsiSection(AnsiSectionType.escapeSequence, "30"), AnsiSection(AnsiSectionType.escapeSequence, "0")]);
774     test(mixed,
775     [
776         AnsiSection(AnsiSectionType.escapeSequence, "30;1;2"),
777         AnsiSection(AnsiSectionType.text,           "Hello, "),
778         AnsiSection(AnsiSectionType.escapeSequence, "0"),
779         AnsiSection(AnsiSectionType.text,           "World!")
780     ]);
781 
782     assert(mixed.asAnsiSections.array.length == 4);
783 }
784 
785 /// Returns: A new `AnsiSectionRange` using the given `input`.
786 @safe @nogc
787 AnsiSectionRange!Char asAnsiSections(Char)(const Char[] input) nothrow pure
788 if(isSomeChar!Char)
789 {
790     return AnsiSectionRange!Char(input);
791 }
792 
793 /++
794  + Provides an InputRange that iterates over all non-ANSI related parts of `input`.
795  +
796  + This can effectively be used to parse over text that is/might contain ANSI encoded text.
797  +
798  + Params:
799  +  input = The input to strip.
800  +
801  + Returns:
802  +  An InputRange that provides all ranges of characters from `input` that do not belong to an
803  +  ANSI command sequence.
804  +
805  + See_Also:
806  +  `asAnsiSections`
807  + ++/
808 @safe @nogc
809 auto stripAnsi(const char[] input) nothrow pure
810 {
811     import std.algorithm : filter, map;
812     return input.asAnsiSections.filter!(s => s.isTextSection).map!(s => s.value);
813 }
814 ///
815 unittest
816 {
817     import std.array : array;
818 
819     auto ansiText = "ABC".ansi.fg(Ansi4BitColour.red).toString()
820                   ~ "Doe Ray Me".ansi.bg(Ansi4BitColour.green).toString()
821                   ~ "123";
822 
823     assert(ansiText.stripAnsi.array == ["ABC", "Doe Ray Me", "123"]);
824 }
825 
826 private enum MAX_SGR_ARGS         = 4;     // 2;r;g;b being max... I think
827 private immutable DEFAULT_SGR_ARG = ['0']; // Missing params are treated as 0
828 
829 @trusted @nogc
830 private void executeSgrCommand(ubyte command, ubyte[MAX_SGR_ARGS] args, ref AnsiColour foreground, ref AnsiColour background, ref AnsiTextFlags flags) nothrow pure
831 {
832     // Pre-testing me: I hope to god this works first time.
833     // During-testing me: It didn't
834     switch(command)
835     {
836         case 0:
837             foreground = AnsiColour.init;
838             background = AnsiColour.init;
839             flags      = AnsiTextFlags.init;
840             break;
841 
842         case 1: flags |= AnsiTextFlags.bold;        break;
843         case 2: flags |= AnsiTextFlags.dim;         break;
844         case 3: flags |= AnsiTextFlags.italic;      break;
845         case 4: flags |= AnsiTextFlags.underline;   break;
846         case 5: flags |= AnsiTextFlags.slowBlink;   break;
847         case 6: flags |= AnsiTextFlags.fastBlink;   break;
848         case 7: flags |= AnsiTextFlags.invert;      break;
849         case 9: flags |= AnsiTextFlags.strike;      break;
850 
851         case 22: flags &= ~(AnsiTextFlags.bold | AnsiTextFlags.dim);            break;
852         case 23: flags &= ~AnsiTextFlags.italic;                                break;
853         case 24: flags &= ~AnsiTextFlags.underline;                             break;
854         case 25: flags &= ~(AnsiTextFlags.slowBlink | AnsiTextFlags.fastBlink); break;
855         case 27: flags &= ~AnsiTextFlags.invert;                                break;
856         case 29: flags &= ~AnsiTextFlags.strike;                                break;
857 
858         //   FG      +FG       BG       +BG
859         case 30: case 90: case 40: case 100:
860         case 31: case 91: case 41: case 101:
861         case 32: case 92: case 42: case 102:
862         case 33: case 93: case 43: case 103:
863         case 34: case 94: case 44: case 104:
864         case 35: case 95: case 45: case 105:
865         case 36: case 96: case 46: case 106:
866         case 37: case 97: case 47: case 107:
867             scope colour = (command >= 30 && command <= 37) || (command >= 90 && command <= 97)
868                            ? &foreground
869                            : &background;
870             *colour = AnsiColour(cast(Ansi4BitColour)command);
871             break;
872 
873         case 38: case 48:
874             const isFg   = (command == 38);
875             scope colour = (isFg) ? &foreground : &background;
876                  *colour = (args[0] == 5) // 5 = Pallette, 2 = RGB.
877                            ? AnsiColour(args[1])
878                            : (args[0] == 2)
879                              ? AnsiColour(args[1], args[2], args[3])
880                              : AnsiColour.init;
881                 colour.isBg = cast(IsBgColour)!isFg;
882             break;
883 
884         default: break; // Ignore anything we don't support or care about.
885     }
886 
887     if(background != AnsiColour.init)
888         background.isBg = IsBgColour.yes;
889 }
890 
891 /++
892  + An InputRange that converts a range of `AnsiSection`s into a range of `AnsiChar`s.
893  +
894  + TLDR; If you have a piece of ANSI-encoded text, and you want to easily step through character by character, keeping the ANSI info, then
895  +       this range is for you.
896  +
897  + Notes:
898  +  This struct is @nogc, except for when it throws exceptions.
899  +
900  + Behaviour:
901  +  This range will only return characters that are not part of an ANSI sequence, which should hopefully end up only being visible ones.
902  +
903  +  For example, a string containing nothing but ANSI sequences won't produce any values.
904  +
905  + Params:
906  +  R = The range of `AnsiSection`s.
907  +
908  + See_Also:
909  +  `asAnsiChars` for easy creation of this struct.
910  + ++/
911 struct AsAnsiCharRange(R)
912 {
913     import std.range : ElementType;
914     static assert(
915         is(ElementType!R == AnsiSection), 
916         "Range "~R.stringof~" must be a range of AnsiSections, not "~ElementType!R.stringof
917     );
918 
919     private
920     {
921         R           _sections;
922         AnsiChar    _front;
923         AnsiSection _currentSection;
924         size_t      _indexIntoSection;
925     }
926 
927     @safe pure:
928 
929     /// Creates a new instance of this struct, using `range` as the range of sections.
930     this(R range)
931     {
932         this._sections = range;
933         this.popFront();
934     }
935 
936     /// Returns: Last parsed character.
937     AnsiChar front()
938     {
939         return this._front;
940     }
941 
942     /// Returns: Whether there's no more characters left to parse.
943     bool empty()
944     {
945         return this._front == AnsiChar.init;
946     }
947 
948     /++
949      + Parses the next sections.
950      +
951      + Optimisation:
952      +  Pretty sure this is O(n)
953      + ++/
954     void popFront()
955     {
956         if(this._sections.empty && this._currentSection == AnsiSection.init)
957         {
958             this._front = AnsiChar.init;
959             return;
960         }
961 
962         // Check if we need to fetch the next section.
963         if(this._indexIntoSection >= this._currentSection.value.length)
964             this.nextSection();
965 
966         // For text sections, just return the next character. For sequences, set the new settings.
967         if(this._currentSection.isTextSection)
968         {
969             this._front.value = this._currentSection.value[this._indexIntoSection++];
970             
971             // If we've reached the end of the final section, make the next call to this function set .empty to true.
972             if(this._sections.empty && this._indexIntoSection >= this._currentSection.value.length)
973                 this._currentSection = AnsiSection.init;
974 
975             return;
976         }
977 
978         ubyte[MAX_SGR_ARGS] args;
979         while(this._indexIntoSection < this._currentSection.value.length)
980         {
981             import std.conv : to;
982             const param = this.readNextAnsiParam().to!ubyte;
983 
984             // Again, since this code might become a function later, I'm doing things a bit weirdly as pre-prep
985             switch(param)
986             {
987                 // Set fg or bg.
988                 case 38:
989                 case 48:
990                     args[] = 0;
991                     args[0] = this.readNextAnsiParam().to!ubyte; // 5 = Pallette, 2 = RGB
992 
993                     if(args[0] == 5)
994                         args[1] = this.readNextAnsiParam().to!ubyte;
995                     else if(args[0] == 2)
996                     {
997                         foreach(i; 0..3)
998                             args[1 + i] = this.readNextAnsiParam().to!ubyte;
999                     }
1000                     break;
1001                 
1002                 default: break;
1003             }
1004 
1005             executeSgrCommand(param, args, this._front.fg, this._front.bgRef, this._front.flags);
1006         }
1007 
1008         // If this was the last section, then we need to set .empty to true since we have no more text to give back anyway.
1009         if(this._sections.empty())
1010         {
1011             import std.stdio : writeln;
1012 
1013             this._front = AnsiChar.init;
1014             this._currentSection = AnsiSection.init;
1015         }
1016         else // Otherwise, get the next char!
1017             this.popFront();
1018     }
1019 
1020     private void nextSection()
1021     {
1022         if(this._sections.empty)
1023             return;
1024 
1025         this._indexIntoSection = 0;
1026         this._currentSection   = this._sections.front;
1027         this._sections.popFront();
1028     }
1029 
1030     private const(char)[] readNextAnsiParam()
1031     {
1032         size_t start = this._indexIntoSection;
1033         const(char)[] slice;
1034 
1035         // Read until end or semi-colon. We're only expecting SGR codes because... it doesn't really make sense for us to handle the others.
1036         for(; this._indexIntoSection < this._currentSection.value.length; this._indexIntoSection++)
1037         {
1038             const ch = this._currentSection.value[this._indexIntoSection];
1039             switch(ch)
1040             {
1041                 case '0': .. case '9':
1042                     break;
1043 
1044                 case ';':
1045                     slice = this._currentSection.value[start..this._indexIntoSection++]; // ++ to move past the semi-colon.
1046                     break;
1047 
1048                 default:
1049                     throw new Exception("Unexpected character in ANSI escape sequence: '"~ch~"'");
1050             }
1051 
1052             if(slice !is null)
1053                 break;
1054         }
1055 
1056         // In case the final delim is simply EOF
1057         if(slice is null && start < this._currentSection.value.length)
1058             slice = this._currentSection.value[start..$];
1059 
1060         return (slice.length == 0) ? DEFAULT_SGR_ARG : slice; // Empty params are counted as 0.
1061     }
1062 }
1063 ///
1064 @("Test AsAnsiCharRange")
1065 @safe
1066 unittest
1067 {
1068     import std.array  : array;
1069     import std.format : format;
1070 
1071     const input = "Hello".ansi.fg(Ansi4BitColour.green).bg(20).bold.toString()
1072                 ~ "World".ansi.fg(255, 0, 255).italic.toString();
1073 
1074     const chars = input.asAnsiChars.array;
1075     assert(
1076         chars.length == "HelloWorld".length, 
1077         "Expected length of %s not %s\n%s".format("HelloWorld".length, chars.length, chars)
1078     );
1079 
1080     // Styling for both sections
1081     const style1 = AnsiChar(AnsiColour(Ansi4BitColour.green), AnsiColour(20, IsBgColour.yes), AnsiTextFlags.bold);
1082     const style2 = AnsiChar(AnsiColour(255, 0, 255), AnsiColour.init, AnsiTextFlags.italic);
1083 
1084     foreach(i, ch; chars)
1085     {
1086         AnsiChar style = (i < 5) ? style1 : style2;
1087         style.value    = ch.value;
1088 
1089         assert(ch == style, "Char #%s doesn't match.\nExpected: %s\nGot: %s".format(i, style, ch));
1090     }
1091 
1092     assert("".asAnsiChars.array.length == 0);
1093 }
1094 
1095 /++
1096  + Notes:
1097  +  Reminder that `AnsiSection.value` shouldn't include the starting `"\033["` and ending `'m'` when it
1098  +  contains an ANSI sequence.
1099  +
1100  + Returns:
1101  +  An `AsAnsiCharRange` wrapped around `range`.
1102  + ++/
1103 AsAnsiCharRange!R asAnsiChars(R)(R range)
1104 {
1105     return typeof(return)(range);
1106 }
1107 
1108 /// Returns: An `AsAnsiCharRange` wrapped around an `AnsiSectionRange` wrapped around `input`.
1109 @safe
1110 AsAnsiCharRange!(AnsiSectionRange!char) asAnsiChars(const char[] input) pure
1111 {
1112     return typeof(return)(input.asAnsiSections);
1113 }
1114 
1115 /++
1116  + An InputRange that converts a range of `AnsiSection`s into a range of `AnsiText`s.
1117  +
1118  + Notes:
1119  +  This struct is @nogc, except for when it throws exceptions.
1120  +
1121  + Behaviour:
1122  +  This range will only return text that isn't part of an ANSI sequence, which should hopefully end up only being visible ones.
1123  +
1124  +  For example, a string containing nothing but ANSI sequences won't produce any values.
1125  +
1126  + Params:
1127  +  R = The range of `AnsiSection`s.
1128  +
1129  + See_Also:
1130  +  `asAnsiTexts` for easy creation of this struct.
1131  + ++/
1132 struct AsAnsiTextRange(R)
1133 {
1134     // TODO: DRY this struct, since it's just a copy-pasted modification of `AsAnsiCharRange`.
1135 
1136     import std.range : ElementType;
1137     static assert(
1138         is(ElementType!R == AnsiSection), 
1139         "Range "~R.stringof~" must be a range of AnsiSections, not "~ElementType!R.stringof
1140     );
1141 
1142     private
1143     {
1144         R           _sections;
1145         AnsiText    _front;
1146         AnsiChar    _settings; // Just to store styling settings.
1147         AnsiSection _currentSection;
1148         size_t      _indexIntoSection;
1149     }
1150 
1151     @safe pure:
1152 
1153     /// Creates a new instance of this struct, using `range` as the range of sections.
1154     this(R range)
1155     {
1156         this._sections = range;
1157         this.popFront();
1158     }
1159 
1160     /// Returns: Last parsed character.
1161     AnsiText front()
1162     {
1163         return this._front;
1164     }
1165 
1166     /// Returns: Whether there's no more text left to parse.
1167     bool empty()
1168     {
1169         return this._front == AnsiText.init;
1170     }
1171 
1172     /++
1173      + Parses the next sections.
1174      +
1175      + Optimisation:
1176      +  Pretty sure this is O(n)
1177      + ++/
1178     void popFront()
1179     {
1180         if(this._sections.empty && this._currentSection == AnsiSection.init)
1181         {
1182             this._front = AnsiText.init;
1183             return;
1184         }
1185 
1186         // Check if we need to fetch the next section.
1187         if(this._indexIntoSection >= this._currentSection.value.length)
1188             this.nextSection();
1189 
1190         // For text sections, just return them. For sequences, set the new settings.
1191         if(this._currentSection.isTextSection)
1192         {
1193             this._front.fg         = this._settings.fg;
1194             this._front.bg         = this._settings.bg;
1195             this._front.setFlags   = this._settings.flags; // mmm, why is that setter prefixed with "set"?
1196             this._front.rawText    = this._currentSection.value;
1197             this._indexIntoSection = this._currentSection.value.length;
1198             return;
1199         }
1200 
1201         ubyte[MAX_SGR_ARGS] args;
1202         while(this._indexIntoSection < this._currentSection.value.length)
1203         {
1204             import std.conv : to;
1205             const param = this.readNextAnsiParam().to!ubyte;
1206 
1207             // Again, since this code might become a function later, I'm doing things a bit weirdly as pre-prep
1208             switch(param)
1209             {
1210                 // Set fg or bg.
1211                 case 38:
1212                 case 48:
1213                     args[] = 0;
1214                     args[0] = this.readNextAnsiParam().to!ubyte; // 5 = Pallette, 2 = RGB
1215 
1216                     if(args[0] == 5)
1217                         args[1] = this.readNextAnsiParam().to!ubyte;
1218                     else if(args[0] == 2)
1219                     {
1220                         foreach(i; 0..3)
1221                             args[1 + i] = this.readNextAnsiParam().to!ubyte;
1222                     }
1223                     break;
1224                 
1225                 default: break;
1226             }
1227 
1228             executeSgrCommand(param, args, this._settings.fg, this._settings.bgRef, this._settings.flags);
1229         }
1230 
1231         // If this was the last section, then we need to set .empty to true since we have no more text to give back anyway.
1232         if(this._sections.empty())
1233         {
1234             import std.stdio : writeln;
1235 
1236             this._front = AnsiText.init;
1237             this._currentSection = AnsiSection.init;
1238         }
1239         else // Otherwise, get the next text!
1240             this.popFront();
1241     }
1242 
1243     private void nextSection()
1244     {
1245         if(this._sections.empty)
1246             return;
1247 
1248         this._indexIntoSection = 0;
1249         this._currentSection   = this._sections.front;
1250         this._sections.popFront();
1251     }
1252 
1253     private const(char)[] readNextAnsiParam()
1254     {
1255         size_t start = this._indexIntoSection;
1256         const(char)[] slice;
1257 
1258         // Read until end or semi-colon. We're only expecting SGR codes because... it doesn't really make sense for us to handle the others.
1259         for(; this._indexIntoSection < this._currentSection.value.length; this._indexIntoSection++)
1260         {
1261             const ch = this._currentSection.value[this._indexIntoSection];
1262             switch(ch)
1263             {
1264                 case '0': .. case '9':
1265                     break;
1266 
1267                 case ';':
1268                     slice = this._currentSection.value[start..this._indexIntoSection++]; // ++ to move past the semi-colon.
1269                     break;
1270 
1271                 default:
1272                     throw new Exception("Unexpected character in ANSI escape sequence: '"~ch~"'");
1273             }
1274 
1275             if(slice !is null)
1276                 break;
1277         }
1278 
1279         // In case the final delim is simply EOF
1280         if(slice is null && start < this._currentSection.value.length)
1281             slice = this._currentSection.value[start..$];
1282 
1283         return (slice.length == 0) ? DEFAULT_SGR_ARG : slice; // Empty params are counted as 0.
1284     }
1285 }
1286 ///
1287 @("Test AsAnsiTextRange")
1288 @safe
1289 unittest
1290 {
1291     // Even this test is copy-pasted, I'm so lazy today T.T
1292 
1293     import std.array  : array;
1294     import std.format : format;
1295 
1296     const input = "Hello".ansi.fg(Ansi4BitColour.green).bg(20).bold.toString()
1297                 ~ "World".ansi.fg(255, 0, 255).italic.toString();
1298 
1299     const text = input.asAnsiTexts.array;
1300     assert(
1301         text.length == 2, 
1302         "Expected length of %s not %s\n%s".format(2, text.length, text)
1303     );
1304 
1305     // Styling for both sections
1306     const style1 = AnsiChar(AnsiColour(Ansi4BitColour.green), AnsiColour(20, IsBgColour.yes), AnsiTextFlags.bold);
1307     auto  style2 = AnsiChar(AnsiColour(255, 0, 255), AnsiColour.init, AnsiTextFlags.italic);
1308 
1309     assert(text[0].fg      == style1.fg);
1310     assert(text[0].bg      == style1.bg);
1311     assert(text[0].flags   == style1.flags);
1312     assert(text[0].rawText == "Hello");
1313     
1314     style2.bgRef.isBg = IsBgColour.yes; // AnsiText is a bit better at keeping this value set to `yes` than `AnsiChar`.
1315     assert(text[1].fg      == style2.fg);
1316     assert(text[1].bg      == style2.bg);
1317     assert(text[1].flags   == style2.flags);
1318     assert(text[1].rawText == "World");
1319 
1320     assert("".asAnsiTexts.array.length == 0);
1321 }
1322 
1323 /++
1324  + Notes:
1325  +  Reminder that `AnsiSection.value` shouldn't include the starting `"\033["` and ending `'m'` when it
1326  +  contains an ANSI sequence.
1327  +
1328  + Returns:
1329  +  An `AsAnsiTextRange` wrapped around `range`.
1330  + ++/
1331 AsAnsiTextRange!R asAnsiTexts(R)(R range)
1332 {
1333     return typeof(return)(range);
1334 }
1335 
1336 /// Returns: An `AsAnsiTextRange` wrapped around an `AnsiSectionRange` wrapped around `input`.
1337 @safe
1338 AsAnsiTextRange!(AnsiSectionRange!char) asAnsiTexts(const char[] input) pure
1339 {
1340     return typeof(return)(input.asAnsiSections);
1341 }
1342 
1343 /// On windows - enable ANSI support.
1344 version(Windows)
1345 {
1346     static this()
1347     {
1348         import core.sys.windows.windows : HANDLE, DWORD, GetStdHandle, STD_OUTPUT_HANDLE, GetConsoleMode, SetConsoleMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING;
1349 
1350         HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);
1351         DWORD mode = 0;
1352 
1353         GetConsoleMode(stdOut, &mode);
1354         mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
1355         SetConsoleMode(stdOut, mode);
1356     }
1357 }