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 }