1 /// This is just JANSI, but ported over 2 module libd.console.ansi; 3 4 import libd.algorithm : isOutputRange; 5 import libd.datastructures..string; 6 7 /// Used to determine if an `AnsiColour` is a background or foreground colour. 8 enum IsBgColour : bool { no, yes } 9 10 /// Used by certain functions to determine if they should only output an ANSI sequence, or output their entire sequence + data. 11 enum AnsiOnly : bool { no, yes } 12 13 /// An 8-bit ANSI colour - an index into the terminal's colour palette. 14 alias Ansi8BitColour = ubyte; 15 16 /// The string that starts an ANSI command sequence. 17 immutable ANSI_CSI = "\033["; 18 19 /// The character that delimits ANSI parameters. 20 immutable ANSI_SEPARATOR = ';'; 21 22 /// The character used to denote that the sequence is an SGR sequence. 23 immutable ANSI_COLOUR_END = 'm'; 24 25 /// The sequence used to reset all styling. 26 immutable ANSI_COLOUR_RESET = ANSI_CSI~"0"~ANSI_COLOUR_END; 27 28 /// The amount to increment an `Ansi4BitColour` by in order to access the background version of the colour. 29 immutable ANSI_FG_TO_BG_INCREMENT = 10; 30 31 /+++ COLOUR TYPES +++/ 32 33 /++ 34 + Defines what type of colour an `AnsiColour` stores. 35 + ++/ 36 enum AnsiColourType 37 { 38 /// Default, failsafe. 39 none, 40 41 /// 4-bit colours. 42 fourBit, 43 44 /// 8-bit colours. 45 eightBit, 46 47 /// 24-bit colours. 48 rgb 49 } 50 51 /++ 52 + An enumeration of standard 4-bit colours. 53 + 54 + These colours will have the widest support between platforms. 55 + ++/ 56 enum Ansi4BitColour 57 { 58 black = 30, 59 red = 31, 60 green = 32, 61 /// On Powershell, this is displayed as a very white colour. 62 yellow = 33, 63 blue = 34, 64 magenta = 35, 65 cyan = 36, 66 /// More gray than true white, use `BrightWhite` for true white. 67 white = 37, 68 /// Grayer than `White`. 69 brightBlack = 90, 70 brightRed = 91, 71 brightGreen = 92, 72 brightYellow = 93, 73 brightBlue = 94, 74 brightMagenta = 95, 75 brightCyan = 96, 76 brightWhite = 97 77 } 78 79 /++ 80 + Contains a 3-byte, RGB colour. 81 + ++/ 82 @safe 83 struct AnsiRgbColour 84 { 85 union 86 { 87 /// The RGB components as an array. 88 ubyte[3] components; 89 90 struct { 91 /// The red component. 92 ubyte r; 93 94 /// The green component. 95 ubyte g; 96 97 /// The blue component. 98 ubyte b; 99 } 100 } 101 102 @safe @nogc nothrow pure: 103 104 /++ 105 + Construct this colour from 3 ubyte components, in RGB order. 106 + 107 + Params: 108 + components = The components to use. 109 + ++/ 110 this(ubyte[3] components) 111 { 112 this.components = components; 113 } 114 115 /++ 116 + Construct this colour from the 3 provided ubyte components. 117 + 118 + Params: 119 + r = The red component. 120 + g = The green component. 121 + b = The blue component. 122 + ++/ 123 this(ubyte r, ubyte g, ubyte b) 124 { 125 this.r = r; 126 this.g = g; 127 this.b = b; 128 } 129 } 130 131 private union AnsiColourUnion 132 { 133 Ansi4BitColour fourBit; 134 Ansi8BitColour eightBit; 135 AnsiRgbColour rgb; 136 } 137 138 /++ 139 + Contains any type of ANSI colour and provides the ability to create a valid SGR command to set the foreground/background. 140 + 141 + This struct overloads `opAssign` allowing easy assignment from `Ansi4BitColour`, `Ansi8BitColour`, `AnsiRgbColour`, and any user-defined type 142 + that satisfies `isUserDefinedRgbType`. 143 + ++/ 144 @safe 145 struct AnsiColour 146 { 147 private static immutable FG_MARKER = "38"; 148 private static immutable BG_MARKER = "48"; 149 private static immutable EIGHT_BIT_MARKER = '5'; 150 private static immutable RGB_MARKER = '2'; 151 152 /++ 153 + The maximum amount of characters any singular `AnsiColour` sequence may use. 154 + 155 + This is often used to create a static array to temporarily, and without allocation, store the sequence for an `AnsiColour`. 156 + ++/ 157 enum MAX_CHARS_NEEDED = "38;2;255;255;255".length; 158 159 private 160 { 161 AnsiColourUnion _value; 162 AnsiColourType _type; 163 IsBgColour _isBg; 164 165 @safe @nogc nothrow 166 this(IsBgColour isBg) pure 167 { 168 this._isBg = isBg; 169 } 170 } 171 172 /// A variant of `.init` that is used for background colours. 173 static immutable bgInit = AnsiColour(IsBgColour.yes); 174 175 /+++ CTORS AND PROPERTIES +++/ 176 @safe @nogc nothrow pure 177 { 178 // Seperate, non-templated constructors as that's a lot more documentation-generator-friendly. 179 180 /++ 181 + Construct a 4-bit colour. 182 + 183 + Params: 184 + colour = The 4-bit colour to use. 185 + isBg = Determines whether this colour sets the foreground or the background. 186 + ++/ 187 this(Ansi4BitColour colour, IsBgColour isBg = IsBgColour.no) 188 { 189 this = colour; 190 this._isBg = isBg; 191 } 192 193 /++ 194 + Construct an 8-bit colour. 195 + 196 + Params: 197 + colour = The 8-bit colour to use. 198 + isBg = Determines whether this colour sets the foreground or the background. 199 + ++/ 200 this(Ansi8BitColour colour, IsBgColour isBg = IsBgColour.no) 201 { 202 this = colour; 203 this._isBg = isBg; 204 } 205 206 /++ 207 + Construct an RGB colour. 208 + 209 + Params: 210 + colour = The RGB colour to use. 211 + isBg = Determines whether this colour sets the foreground or the background. 212 + ++/ 213 this(AnsiRgbColour colour, IsBgColour isBg = IsBgColour.no) 214 { 215 this = colour; 216 this._isBg = isBg; 217 } 218 219 /++ 220 + Construct an RGB colour. 221 + 222 + Params: 223 + r = The red component. 224 + g = The green component. 225 + b = The blue component. 226 + isBg = Determines whether this colour sets the foreground or the background. 227 + ++/ 228 this(ubyte r, ubyte g, ubyte b, IsBgColour isBg = IsBgColour.no) 229 { 230 this(AnsiRgbColour(r, g, b), isBg); 231 } 232 233 /++ 234 + Construct an RGB colour. 235 + 236 + Params: 237 + colour = The user-defined colour type that satisfies `isUserDefinedRgbType`. 238 + isBg = Determines whether this colour sets the foreground or the background. 239 + ++/ 240 this(T)(T colour, IsBgColour isBg = IsBgColour.no) 241 if(isUserDefinedRgbType!T) 242 { 243 this = colour; 244 this._isBg = isBg; 245 } 246 247 /++ 248 + Allows direct assignment from any type that can also be used in any of this struct's ctors. 249 + ++/ 250 auto opAssign(T)(T colour) return 251 if(!is(T == typeof(this))) 252 { 253 static if(is(T == Ansi4BitColour)) 254 { 255 this._value.fourBit = colour; 256 this._type = AnsiColourType.fourBit; 257 } 258 else static if(is(T == Ansi8BitColour)) 259 { 260 this._value.eightBit = colour; 261 this._type = AnsiColourType.eightBit; 262 } 263 else static if(is(T == AnsiRgbColour)) 264 { 265 this._value.rgb = colour; 266 this._type = AnsiColourType.rgb; 267 } 268 else static if(isUserDefinedRgbType!T) 269 { 270 this = colour.to!AnsiColour(); 271 } 272 else static assert(false, "Cannot implicitly convert "~T.stringof~" into an AnsiColour."); 273 274 return this; 275 } 276 277 /// Returns: The `AnsiColourType` of this `AnsiColour`. 278 @property 279 AnsiColourType type() const 280 { 281 return this._type; 282 } 283 284 /// Returns: Whether this `AnsiColour` is for a background or not (it affects the output!). 285 @property 286 IsBgColour isBg() const 287 { 288 return this._isBg; 289 } 290 291 /// ditto 292 @property 293 void isBg(IsBgColour bg) 294 { 295 this._isBg = bg; 296 } 297 298 /// ditto 299 @property 300 void isBg(bool bg) 301 { 302 this._isBg = cast(IsBgColour)bg; 303 } 304 305 /++ 306 + Assertions: 307 + This colour's type must be `AnsiColourType.fourBit` 308 + 309 + Returns: 310 + This `AnsiColour` as an `Ansi4BitColour`. 311 + ++/ 312 @property 313 Ansi4BitColour asFourBit() const 314 { 315 assert(this.type == AnsiColourType.fourBit); 316 return this._value.fourBit; 317 } 318 319 /++ 320 + Assertions: 321 + This colour's type must be `AnsiColourType.eightBit` 322 + 323 + Returns: 324 + This `AnsiColour` as a `ubyte`. 325 + ++/ 326 @property 327 ubyte asEightBit() const 328 { 329 assert(this.type == AnsiColourType.eightBit); 330 return this._value.eightBit; 331 } 332 333 /++ 334 + Assertions: 335 + This colour's type must be `AnsiColourType.rgb` 336 + 337 + Returns: 338 + This `AnsiColour` as an `AnsiRgbColour`. 339 + ++/ 340 @property 341 AnsiRgbColour asRgb() const 342 { 343 assert(this.type == AnsiColourType.rgb); 344 return this._value.rgb; 345 } 346 } 347 348 /+++ OUTPUT +++/ 349 350 /++ 351 + Converts this `AnsiColour` into a sequence String. 352 + 353 + See_Also: 354 + `toSequence` 355 + ++/ 356 @trusted nothrow 357 String toString() const 358 { 359 char[MAX_CHARS_NEEDED] chars; 360 return String(this.toSequence(chars[0..MAX_CHARS_NEEDED])); 361 } 362 /// 363 @("AnsiColour.toString") 364 unittest 365 { 366 assert(AnsiColour(255, 128, 64).toString() == "38;2;255;128;64"); 367 } 368 369 /++ 370 + Creates an ANSI SGR command that either sets the foreground, or the background (`isBg`) to the colour 371 + stored inside of this `AnsiColour`. 372 + 373 + Please note that the CSI (`ANSI_CSI`/`\033[`) and the SGR marker (`ANSI_COLOUR_END`/`m`) are not included 374 + in this output. 375 + 376 + Notes: 377 + Any characters inside of `buffer` that are not covered by the returned slice, are left unmodified. 378 + 379 + If this colour hasn't been initialised or assigned a value, then the returned value is simply `null`. 380 + 381 + Params: 382 + buffer = The statically allocated buffer used to store the result of this function. 383 + 384 + Returns: 385 + A slice into `buffer` that contains the output of this function. 386 + ++/ 387 @safe @nogc 388 char[] toSequence(ref return char[MAX_CHARS_NEEDED] buffer) nothrow const 389 { 390 if(this.type == AnsiColourType.none) 391 return null; 392 393 size_t cursor; 394 395 void numIntoBuffer(ubyte num) 396 { 397 char[3] text; 398 const slice = numToStrBase10(text[0..3], num); 399 buffer[cursor..cursor + slice.length] = slice[]; 400 cursor += slice.length; 401 } 402 403 if(this.type != AnsiColourType.fourBit) 404 { 405 // 38; or 48; 406 auto marker = (this.isBg) ? BG_MARKER : FG_MARKER; 407 buffer[cursor..cursor+2] = marker[0..$]; 408 cursor += 2; 409 buffer[cursor++] = ANSI_SEPARATOR; 410 } 411 412 // 4bit, 5;8bit, or 2;r;g;b 413 final switch(this.type) with(AnsiColourType) 414 { 415 case none: assert(false); 416 case fourBit: 417 numIntoBuffer(cast(ubyte)((this.isBg) ? this._value.fourBit + 10 : this._value.fourBit)); 418 break; 419 420 case eightBit: 421 buffer[cursor++] = EIGHT_BIT_MARKER; 422 buffer[cursor++] = ANSI_SEPARATOR; 423 numIntoBuffer(this._value.eightBit); 424 break; 425 426 case rgb: 427 buffer[cursor++] = RGB_MARKER; 428 buffer[cursor++] = ANSI_SEPARATOR; 429 430 numIntoBuffer(this._value.rgb.r); 431 buffer[cursor++] = ANSI_SEPARATOR; 432 numIntoBuffer(this._value.rgb.g); 433 buffer[cursor++] = ANSI_SEPARATOR; 434 numIntoBuffer(this._value.rgb.b); 435 break; 436 } 437 438 return buffer[0..cursor]; 439 } 440 /// 441 @("AnsiColour.toSequence(char[])") 442 unittest 443 { 444 char[AnsiColour.MAX_CHARS_NEEDED] buffer; 445 446 void test(string expected, AnsiColour colour) 447 { 448 const slice = colour.toSequence(buffer); 449 assert(slice == expected); 450 } 451 452 test("32", AnsiColour(Ansi4BitColour.green)); 453 test("42", AnsiColour(Ansi4BitColour.green, IsBgColour.yes)); 454 test("38;5;1", AnsiColour(Ansi8BitColour(1))); 455 test("48;5;1", AnsiColour(Ansi8BitColour(1), IsBgColour.yes)); 456 test("38;2;255;255;255", AnsiColour(255, 255, 255)); 457 test("48;2;255;128;64", AnsiColour(255, 128, 64, IsBgColour.yes)); 458 } 459 } 460 461 /+++ MISC TYPES +++/ 462 463 /++ 464 + A list of styling options provided by ANSI SGR. 465 + 466 + As a general rule of thumb, assume most of these won't work inside of a Windows command prompt (unless it's the new Windows Terminal). 467 + ++/ 468 enum AnsiSgrStyle 469 { 470 none = 0, 471 bold = 1, 472 dim = 2, 473 italic = 3, 474 underline = 4, 475 slowBlink = 5, 476 fastBlink = 6, 477 invert = 7, 478 strike = 9 479 } 480 481 private template getMaxSgrStyleCharCount() 482 { 483 // Can't even use non-betterC features in CTFE, so no std.conv.to!string :( 484 size_t numberOfChars(int num) 485 { 486 size_t amount; 487 488 do 489 { 490 amount++; 491 num /= 10; 492 } while(num > 0); 493 494 return amount; 495 } 496 497 size_t calculate() 498 { 499 size_t amount; 500 static foreach(name; __traits(allMembers, AnsiSgrStyle)) 501 amount += numberOfChars(cast(int)__traits(getMember, AnsiSgrStyle, name)) + 1; // + 1 for the semi-colon after. 502 503 return amount; 504 } 505 506 enum getMaxSgrStyleCharCount = calculate(); 507 } 508 509 /++ 510 + Contains any number of styling options from `AnsiStyleSgr`, and provides the ability to generate 511 + an ANSI SGR command to apply all of the selected styling options. 512 + ++/ 513 @safe 514 struct AnsiStyle 515 { 516 /++ 517 + The maximum amount of characters any singular `AnsiStyle` sequence may use. 518 + 519 + This is often used to create a static array to temporarily, and without allocation, store the sequence for an `AnsiStyle`. 520 + ++/ 521 enum MAX_CHARS_NEEDED = getMaxSgrStyleCharCount!(); 522 523 private 524 { 525 ushort _sgrBitmask; // Each set bit index corresponds to the value from `AnsiSgrStyle`. 526 527 @safe @nogc nothrow 528 int sgrToBit(AnsiSgrStyle style) pure const 529 { 530 return 1 << (cast(int)style); 531 } 532 533 @safe @nogc nothrow 534 void setSgrBit(bool setOrUnset)(AnsiSgrStyle style) pure 535 { 536 static if(setOrUnset) 537 this._sgrBitmask |= this.sgrToBit(style); 538 else 539 this._sgrBitmask &= ~this.sgrToBit(style); 540 } 541 542 @safe @nogc nothrow 543 bool getSgrBit(AnsiSgrStyle style) pure const 544 { 545 return (this._sgrBitmask & this.sgrToBit(style)) > 0; 546 } 547 } 548 549 // Seperate functions for better documentation generation. 550 // 551 // Tedious, as this otherwise could've all been auto-generated. 552 /+++ SETTERS +++/ 553 @safe @nogc nothrow pure 554 { 555 /// Removes all styling from this `AnsiStyle`. 556 AnsiStyle reset() return 557 { 558 this._sgrBitmask = 0; 559 return this; 560 } 561 562 /++ 563 + Enables/Disables a certain styling option. 564 + 565 + Params: 566 + style = The styling option to enable/disable. 567 + enable = If true, enable the option. If false, disable it. 568 + 569 + Returns: 570 + `this` for chaining. 571 + ++/ 572 AnsiStyle set(AnsiSgrStyle style, bool enable) return 573 { 574 if(enable) 575 this.setSgrBit!true(style); 576 else 577 this.setSgrBit!false(style); 578 return this; 579 } 580 581 /// 582 AnsiStyle bold(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.bold); return this; } 583 /// 584 AnsiStyle dim(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.dim); return this; } 585 /// 586 AnsiStyle italic(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.italic); return this; } 587 /// 588 AnsiStyle underline(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.underline); return this; } 589 /// 590 AnsiStyle slowBlink(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.slowBlink); return this; } 591 /// 592 AnsiStyle fastBlink(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.fastBlink); return this; } 593 /// 594 AnsiStyle invert(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.invert); return this; } 595 /// 596 AnsiStyle strike(bool enable = true) return { this.setSgrBit!true(AnsiSgrStyle.strike); return this; } 597 } 598 599 /+++ GETTERS +++/ 600 @safe @nogc nothrow pure const 601 { 602 /++ 603 + Get the status of a certain styling option. 604 + 605 + Params: 606 + style = The styling option to get. 607 + 608 + Returns: 609 + `true` if the styling option is enabled, `false` otherwise. 610 + ++/ 611 bool get(AnsiSgrStyle style) 612 { 613 return this.getSgrBit(style); 614 } 615 616 /// 617 bool bold() { return this.getSgrBit(AnsiSgrStyle.bold); } 618 /// 619 bool dim() { return this.getSgrBit(AnsiSgrStyle.dim); } 620 /// 621 bool italic() { return this.getSgrBit(AnsiSgrStyle.italic); } 622 /// 623 bool underline() { return this.getSgrBit(AnsiSgrStyle.underline); } 624 /// 625 bool slowBlink() { return this.getSgrBit(AnsiSgrStyle.slowBlink); } 626 /// 627 bool fastBlink() { return this.getSgrBit(AnsiSgrStyle.fastBlink); } 628 /// 629 bool invert() { return this.getSgrBit(AnsiSgrStyle.invert); } 630 /// 631 bool strike() { return this.getSgrBit(AnsiSgrStyle.strike); } 632 } 633 634 /+++ OUTPUT +++/ 635 636 /++ 637 + Converts this `AnsiStyle` into a sequence string. 638 + 639 + See_Also: 640 + `toSequence` 641 + ++/ 642 @trusted nothrow 643 String toString() const 644 { 645 char[MAX_CHARS_NEEDED] chars; 646 return String(this.toSequence(chars[0..MAX_CHARS_NEEDED])); 647 } 648 649 /++ 650 + Creates an ANSI SGR command that enables all of the desired styling options, while leaving all of the other options unchanged. 651 + 652 + Please note that the CSI (`ANSI_CSI`/`\033[`) and the SGR marker (`ANSI_COLOUR_END`/`m`) are not included 653 + in this output. 654 + 655 + Notes: 656 + Any characters inside of `buffer` that are not covered by the returned slice, are left unmodified. 657 + 658 + If this colour hasn't been initialised or assigned a value, then the returned value is simply `null`. 659 + 660 + Params: 661 + buffer = The statically allocated buffer used to store the result of this function. 662 + 663 + Returns: 664 + A slice into `buffer` that contains the output of this function. 665 + ++/ 666 @safe @nogc 667 char[] toSequence(ref return char[MAX_CHARS_NEEDED] buffer) nothrow const 668 { 669 if(this._sgrBitmask == 0) 670 return null; 671 672 size_t cursor; 673 void numIntoBuffer(uint num) 674 { 675 char[10] text; 676 const slice = numToStrBase10(text[0..$], num); 677 buffer[cursor..cursor + slice.length] = slice[]; 678 cursor += slice.length; 679 } 680 681 bool isFirstValue = true; 682 static foreach(name; __traits(allMembers, AnsiSgrStyle)) 683 {{ 684 const flag = __traits(getMember, AnsiSgrStyle, name); 685 if(this.getSgrBit(flag)) 686 { 687 if(!isFirstValue) 688 buffer[cursor++] = ANSI_SEPARATOR; 689 isFirstValue = false; 690 691 numIntoBuffer(cast(uint)flag); 692 } 693 }} 694 695 return buffer[0..cursor]; 696 } 697 /// 698 @("AnsiStyle.toSequence(char[])") 699 unittest 700 { 701 char[AnsiStyle.MAX_CHARS_NEEDED] buffer; 702 703 void test(string expected, AnsiStyle style) 704 { 705 const slice = style.toSequence(buffer); 706 assert(slice == expected); 707 } 708 709 test("", AnsiStyle.init); 710 test("1;2;3", AnsiStyle.init.bold.dim.italic); 711 } 712 } 713 714 /+++ DATA WITH COLOUR TYPES +++/ 715 716 /++ 717 + Contains an `AnsiColour` for the foreground, an `AnsiColour` for the background, and an `AnsiStyle` for additional styling, 718 + and provides the ability to create an ANSI SGR command to set the foreground, background, and overall styling of the terminal. 719 + 720 + A.k.a This is just a container over two `AnsiColour`s and an `AnsiStyle`. 721 + ++/ 722 @safe 723 struct AnsiStyleSet 724 { 725 /++ 726 + The maximum amount of characters any singular `AnsiStyle` sequence may use. 727 + 728 + This is often used to create a static array to temporarily, and without allocation, store the sequence for an `AnsiStyle`. 729 + ++/ 730 enum MAX_CHARS_NEEDED = (AnsiColour.MAX_CHARS_NEEDED * 2) + AnsiStyle.MAX_CHARS_NEEDED; 731 732 private AnsiColour _fg; 733 private AnsiColour _bg; 734 private AnsiStyle _style; 735 736 // As usual, functions are manually made for better documentation. 737 738 /+++ SETTERS +++/ 739 @safe @nogc nothrow 740 { 741 /// 742 AnsiStyleSet fg(AnsiColour colour) return { this._fg = colour; this._fg.isBg = IsBgColour.no; return this; } 743 /// 744 AnsiStyleSet fg(Ansi4BitColour colour) return { return this.fg(AnsiColour(colour)); } 745 /// 746 AnsiStyleSet fg(Ansi8BitColour colour) return { return this.fg(AnsiColour(colour)); } 747 /// 748 AnsiStyleSet fg(AnsiRgbColour colour) return { return this.fg(AnsiColour(colour)); } 749 750 /// 751 AnsiStyleSet bg(AnsiColour colour) return { this._bg = colour; this._bg.isBg = IsBgColour.yes; return this; } 752 /// 753 AnsiStyleSet bg(Ansi4BitColour colour) return { return this.bg(AnsiColour(colour)); } 754 /// 755 AnsiStyleSet bg(Ansi8BitColour colour) return { return this.bg(AnsiColour(colour)); } 756 /// 757 AnsiStyleSet bg(AnsiRgbColour colour) return { return this.bg(AnsiColour(colour)); } 758 /// 759 760 /// 761 AnsiStyleSet style(AnsiStyle style) return { this._style = style; return this; } 762 } 763 764 /+++ GETTERS +++/ 765 @safe @nogc nothrow const 766 { 767 /// 768 AnsiColour fg() { return this._fg; } 769 /// 770 AnsiColour bg() { return this._bg; } 771 /// 772 AnsiStyle style() { return this._style; } 773 } 774 775 /+++ OUTPUT ++/ 776 /++ 777 + Creates an ANSI SGR command that sets the foreground colour, sets the background colour, 778 + and enables all of the desired styling options, while leaving all of the other options unchanged. 779 + 780 + Please note that the CSI (`ANSI_CSI`/`\033[`) and the SGR marker (`ANSI_COLOUR_END`/`m`) are not included 781 + in this output. 782 + 783 + Notes: 784 + Any characters inside of `buffer` that are not covered by the returned slice, are left unmodified. 785 + 786 + If this colour hasn't been initialised or assigned a value, then the returned value is simply `null`. 787 + 788 + Params: 789 + buffer = The statically allocated buffer used to store the result of this function. 790 + 791 + Returns: 792 + A slice into `buffer` that contains the output of this function. 793 + ++/ 794 @safe @nogc 795 char[] toSequence(ref return char[MAX_CHARS_NEEDED] buffer) nothrow const 796 { 797 size_t cursor; 798 799 char[AnsiColour.MAX_CHARS_NEEDED] colour; 800 char[AnsiStyle.MAX_CHARS_NEEDED] style; 801 802 auto slice = this._fg.toSequence(colour); 803 buffer[cursor..cursor + slice.length] = slice[]; 804 cursor += slice.length; 805 806 slice = this._bg.toSequence(colour); 807 if(slice.length > 0 && cursor > 0) 808 buffer[cursor++] = ANSI_SEPARATOR; 809 buffer[cursor..cursor + slice.length] = slice[]; 810 cursor += slice.length; 811 812 slice = this.style.toSequence(style); 813 if(slice.length > 0 && cursor > 0) 814 buffer[cursor++] = ANSI_SEPARATOR; 815 buffer[cursor..cursor + slice.length] = slice[]; 816 cursor += slice.length; 817 818 return buffer[0..cursor]; 819 } 820 /// 821 @("AnsiStyleSet.toSequence") 822 unittest 823 { 824 char[AnsiStyleSet.MAX_CHARS_NEEDED] buffer; 825 826 void test(string expected, AnsiStyleSet ch) 827 { 828 auto slice = ch.toSequence(buffer); 829 assert(slice == expected); 830 } 831 832 test("", AnsiStyleSet.init); 833 test( 834 "32;48;2;255;128;64;1;4", 835 AnsiStyleSet.init 836 .fg(Ansi4BitColour.green) 837 .bg(AnsiRgbColour(255, 128, 64)) 838 .style(AnsiStyle.init.bold.underline) 839 ); 840 } 841 } 842 843 /++ 844 + An enumeration used by an `AnsiText` implementation to describe any special features that `AnsiText` needs to mold 845 + itself around. 846 + ++/ 847 enum AnsiTextImplementationFeatures 848 { 849 /// Supports at least `.put`, `.toSink`, `char[] .newSlice`, and allows `AnsiText` to handle the encoding. 850 basic = 0, 851 } 852 853 /++ 854 + A lightweight alternative to `AnsiText` which only supports a singular coloured string, at the cost 855 + of removing most of the other complexity & dynamic allocation needs of `AnsiText`. 856 + 857 + If you only need to style your string in one certain way, or want to avoid `AnsiText` altogether, then this struct 858 + is the way to go. 859 + 860 + Usage_(Manually): 861 + First, retrieve and the ANSI styling sequence via `AnsiTextLite.toFullStartSequence` and output it. 862 + 863 + Second, output `AnsiTextLite.text`. 864 + 865 + Finally, and optionally, retrieve the ANSI reset sequence via `AnsiTextLite.toFullEndSequence` and output it. 866 + 867 + Usage_(Range): 868 + Call `AnsiTextLite.toRange` to get the range, please read its documentation as it is important (it'll return slices to stack-allocated memory). 869 + 870 + Usage_(GC): 871 + If you're not compiling under `-betterc`, then `AnsiTextLite.toString()` will provide you with a GC-allocated string containing: 872 + the start ANSI sequence; the text to display; and the end ANSI sequence. i.e. A string that is just ready to be printed. 873 + 874 + This struct also implements the sink-based version of `toString`, which means that when used directly with things like `writeln`, this struct 875 + is able to avoid allocations (unless the sink itself allocates). See the unittest for an example. 876 + 877 + See_Also: 878 + `ansi` for fluent creation of an `AnsiTextLite`. 879 + 880 + This struct's unittest for an example of usage. 881 + ++/ 882 struct AnsiTextLite 883 { 884 /++ 885 + The maximum amount of chars required by the start sequence of an `AnsiTextLite` (`toFullStartSequence`). 886 + ++/ 887 enum MAX_CHARS_NEEDED = AnsiStyleSet.MAX_CHARS_NEEDED + ANSI_CSI.length + 1; // + 1 for the ANSI_COLOUR_END 888 889 /// The text to output. 890 const(char)[] text; 891 892 /// The styling to apply to the text. 893 AnsiStyleSet styleSet; 894 895 /+++ SETTERS +++/ 896 // TODO: Should probably make a mixin template for this, but I need to see how the documentation generators handle that. 897 // I also can't just do an `alias this`, as otherwise the style functions wouldn't return `AnsiTextLite`, but instead `AnsiStyleSet`. 898 // Or just suck it up and make some of the setters templatised, much to the dismay of documentation. 899 @safe @nogc nothrow 900 { 901 /// 902 AnsiTextLite fg(AnsiColour colour) return { this.styleSet.fg = colour; this.styleSet.fg.isBg = IsBgColour.no; return this; } 903 /// 904 AnsiTextLite fg(Ansi4BitColour colour) return { return this.fg(AnsiColour(colour)); } 905 /// 906 AnsiTextLite fg(Ansi8BitColour colour) return { return this.fg(AnsiColour(colour)); } 907 /// 908 AnsiTextLite fg(AnsiRgbColour colour) return { return this.fg(AnsiColour(colour)); } 909 910 /// 911 AnsiTextLite bg(AnsiColour colour) return { this.styleSet.bg = colour; this.styleSet.bg.isBg = IsBgColour.yes; return this; } 912 /// 913 AnsiTextLite bg(Ansi4BitColour colour) return { return this.bg(AnsiColour(colour)); } 914 /// 915 AnsiTextLite bg(Ansi8BitColour colour) return { return this.bg(AnsiColour(colour)); } 916 /// 917 AnsiTextLite bg(AnsiRgbColour colour) return { return this.bg(AnsiColour(colour)); } 918 /// 919 920 /// 921 AnsiTextLite style(AnsiStyle style) return { this.styleSet.style = style; return this; } 922 } 923 924 /+++ GETTERS +++/ 925 @safe @nogc nothrow const 926 { 927 /// 928 AnsiColour fg() { return this.styleSet.fg; } 929 /// 930 AnsiColour bg() { return this.styleSet.bg; } 931 /// 932 AnsiStyle style() { return this.styleSet.style; } 933 } 934 935 @safe @nogc nothrow const 936 { 937 /++ 938 + Populates the given buffer with the full ANSI sequence needed to enable the styling 939 + defined within this `AnsiTextLite` 940 + 941 + Unlike the usual `toSequence` functions, this function includes the `ANSI_CSI` and `ANSI_COLOUR_END` markers, 942 + meaning the output from this function is ready to be printed as-is. 943 + 944 + Do note that this function doesn't insert a null-terminator, so if you're using anything based on C strings, you need 945 + to insert that yourself. 946 + 947 + Notes: 948 + Any parts of the `buffer` that are not populated by this function are left untouched. 949 + 950 + Params: 951 + buffer = The buffer to populate. 952 + 953 + Returns: 954 + The slice of `buffer` that has been populated. 955 + ++/ 956 char[] toFullStartSequence(ref return char[MAX_CHARS_NEEDED] buffer) 957 { 958 size_t cursor; 959 960 buffer[0..ANSI_CSI.length] = ANSI_CSI[]; 961 cursor += ANSI_CSI.length; 962 963 char[AnsiStyleSet.MAX_CHARS_NEEDED] styleBuffer; 964 const styleSlice = this.styleSet.toSequence(styleBuffer); 965 buffer[cursor..cursor+styleSlice.length] = styleSlice[]; 966 cursor += styleSlice.length; 967 968 buffer[cursor++] = ANSI_COLOUR_END; 969 970 return buffer[0..cursor]; 971 } 972 973 /++ 974 + Returns: 975 + The end ANSI sequence for `AnsiTextLite`, which is simply a statically allocated version of the `ANSI_COLOUR_RESET` constant. 976 + ++/ 977 char[ANSI_COLOUR_RESET.length] toFullEndSequence() 978 { 979 typeof(return) buffer; 980 buffer[0..$] = ANSI_COLOUR_RESET[]; 981 return buffer; 982 } 983 984 /++ 985 + Provides a range that returns, in this order: The start sequence (`.toFullStartSequence`); the output text (`.text`), 986 + and finally the end sequence (`.toFullEndSequence`). 987 + 988 + This range is $(B weakly-safe) as it $(B returns slices to stack memory) so please ensure that $(B any returned slices don't outlive the origin range object). 989 + 990 + Please also note that non of the returned slices contain null terminators. 991 + 992 + Returns: 993 + An Input Range that returns all the slices required to correctly display this `AnsiTextLite` onto a console. 994 + ++/ 995 auto toRange() 996 { 997 static struct Range 998 { 999 char[MAX_CHARS_NEEDED] start; 1000 const(char)[] middle; 1001 char[ANSI_COLOUR_RESET.length] end; 1002 char[] startSlice; 1003 1004 size_t sliceCount; 1005 1006 @safe @nogc nothrow: 1007 1008 bool empty() 1009 { 1010 return this.sliceCount >= 3; 1011 } 1012 1013 void popFront() 1014 { 1015 this.sliceCount++; 1016 } 1017 1018 @trusted 1019 const(char)[] front() return 1020 { 1021 switch(sliceCount) 1022 { 1023 case 0: return this.startSlice; 1024 case 1: return this.middle; 1025 case 2: return this.end[0..$]; 1026 default: assert(false, "Cannot use empty range."); 1027 } 1028 } 1029 } 1030 1031 Range r; 1032 r.startSlice = this.toFullStartSequence(r.start); 1033 r.middle = this.text; 1034 r.end = this.toFullEndSequence(); 1035 1036 return r; 1037 } 1038 } 1039 1040 /++ 1041 + Notes: 1042 + This struct implements the sink-based `toString` which performs no allocations, so the likes of `std.stdio.writeln` will 1043 + automatically use the sink-based version if you pass this struct to it directly. 1044 + 1045 + Returns: 1046 + A string containing this `AnsiTextLite` as an ANSI-encoded string, ready for printing. 1047 + ++/ 1048 @trusted nothrow // @trusted due to .assumeUnique 1049 String toString() const 1050 { 1051 char[MAX_CHARS_NEEDED] styleBuffer; 1052 const styleSlice = this.toFullStartSequence(styleBuffer); 1053 1054 String buffer; 1055 buffer.length = styleSlice.length + this.text.length + ANSI_COLOUR_RESET.length; 1056 buffer[0..styleSlice.length] = styleSlice[]; 1057 buffer[styleSlice.length..styleSlice.length+this.text.length] = this.text[]; 1058 buffer[$-ANSI_COLOUR_RESET.length..$] = ANSI_COLOUR_RESET[]; 1059 1060 return buffer; 1061 } 1062 1063 /++ 1064 + The sink-based version of `toString`, which doesn't allocate by itself unless the `sink` decides to allocate. 1065 + 1066 + Params: 1067 + sink = The sink to output into. 1068 + 1069 + See_Also: 1070 + `toSink` for a templatised version of this function which can infer attributes, and supports any form of Output Range instead of just a delegate. 1071 + ++/ 1072 void toString(scope void delegate(const(char)[]) sink) const 1073 { 1074 foreach(slice; this.toRange()) 1075 sink(slice); 1076 } 1077 1078 /++ 1079 + Outputs in order: The start sequence (`.toFullStartSequence`), the output text (`.text`), and the end sequence (`.toFullEndSequence`) 1080 + into the given `sink`. 1081 + 1082 + This function by itself does not allocate memory. 1083 + 1084 + This function will infer attributes, so as to be used in whatever attribute-souped environment your sink supports. 1085 + 1086 + $(B Please read the warnings described in `.toRange`) TLDR; don't persist the slices given to the sink under any circumstance. You must 1087 + copy the data as soon as you get it. 1088 + 1089 + Params: 1090 + sink = The sink to output into. 1091 + ++/ 1092 void toSink(Sink)(scope ref Sink sink) const 1093 { 1094 foreach(slice; this.toRange()) 1095 sink.put(slice); 1096 } 1097 } 1098 /// 1099 @("AnsiTextLite") 1100 version(none) unittest 1101 { 1102 auto text = "Hello!".ansi 1103 .fg(Ansi4BitColour.green) 1104 .bg(AnsiRgbColour(128, 128, 128)) 1105 .style(AnsiStyle.init.bold.underline); 1106 1107 // Usage 1: Manually 1108 import core.stdc.stdio : printf; 1109 import std.stdio : writeln, write; 1110 version(JANSI_TestOutput) // Just so test output isn't clogged. This still shows you how to use things though. 1111 { 1112 char[AnsiTextLite.MAX_CHARS_NEEDED + 1] startSequence; // + 1 for null terminator. 1113 const sliceFromStartSequence = text.toFullStartSequence(startSequence[0..AnsiTextLite.MAX_CHARS_NEEDED]); 1114 startSequence[sliceFromStartSequence.length] = '\0'; 1115 1116 char[200] textBuffer; 1117 textBuffer[0..text.text.length] = text.text[]; 1118 textBuffer[text.text.length] = '\0'; 1119 1120 char[ANSI_COLOUR_RESET.length + 1] endSequence; 1121 endSequence[0..ANSI_COLOUR_RESET.length] = text.toFullEndSequence()[]; 1122 endSequence[$-1] = '\0'; 1123 1124 printf("%s%s%s\n", startSequence.ptr, textBuffer.ptr, endSequence.ptr); 1125 } 1126 1127 // Usage 2: Range (RETURNS STACK MEMORY, DO NOT ALLOW SLICES TO OUTLIVE RANGE OBJECT WITHOUT EXPLICIT COPY) 1128 version(JANSI_TestOutput) 1129 { 1130 // -betterC 1131 foreach(slice; text.toRange) 1132 { 1133 char[200] buffer; 1134 buffer[0..slice.length] = slice[]; 1135 buffer[slice.length] = '\0'; 1136 printf("%s", buffer.ptr); 1137 } 1138 printf("\n"); 1139 1140 // GC 1141 foreach(slice; text.toRange) 1142 write(slice); 1143 writeln(); 1144 } 1145 1146 // Usage 3: toString (Sink-based, so AnsiTextLite doesn't allocate, but writeln/the sink might) 1147 version(JANSI_TestOutput) 1148 { 1149 writeln(text); // Calls the sink-based .toString(); 1150 } 1151 1152 // Usage 4: toString (non-sink, non-betterc only) 1153 version(JANSI_TestOutput) 1154 { 1155 writeln(text.toString()); 1156 } 1157 1158 // Usage 5: toSink 1159 version(JANSI_TestOutput) 1160 { 1161 struct CustomOutputRange 1162 { 1163 char[] output; 1164 @safe 1165 void put(const(char)[] slice) nothrow 1166 { 1167 const start = output.length; 1168 output.length += slice.length; 1169 output[start..$] = slice[]; 1170 } 1171 } 1172 1173 CustomOutputRange sink; 1174 ()@safe nothrow{ text.toSink(sink); }(); 1175 1176 writeln(sink.output); 1177 } 1178 } 1179 1180 /++ 1181 + Contains a string that supports the ability for different parts of the string to be styled seperately. 1182 + 1183 + This struct is highly flexible and dynamic, as it requires the use of external code to provide some 1184 + of the implementation. 1185 + 1186 + Because this is provided via a `mixin template`, implementations can also $(B extend) this struct to 1187 + provide their own functionality, make things non-copyable if needed, allows data to be stored via ref-counting, etc. 1188 + 1189 + This struct itself is mostly just a consistant user-facing interface that all implementations share, while the implementations 1190 + themselves can transform this struct to any level it requires. 1191 + 1192 + Implementations_: 1193 + While implementations can add whatever functions, operator overloads, constructors, etc. that they want, there is a small 1194 + set of functions and value that each implmentation must define in order to be useable. 1195 + 1196 + Every implementation must define an enum called `Features` who's value is one of the values of `AnsiTextImplementationFeatures`. 1197 + For example: `enum Features = AnsiTextImplementationFeatures.xxx` 1198 + 1199 + Builtin implementations consist of `AnsiTextGC` (not enabled with -betterC), `AnsiTextStack`, and `AnsiTextMalloc`, which are self-descriptive. 1200 + 1201 + Basic_Implemetations: 1202 + An implementation that doesn't require anything noteworthy from `AnsiText` itself should define their features as `AnsiTextImplementationFeatures.basic`. 1203 + 1204 + This type of implementation must implement the following functions (expressed here as an interface for simplicity): 1205 + 1206 + ``` 1207 interface BasicImplementation 1208 { 1209 /// Provides `AnsiText` with a slice that is of at least `minLength` in size. 1210 /// 1211 /// This function is called `AnsiText` needs to insert more styled characters into the string. 1212 /// 1213 /// How this slice is stored and allocated and whatever else, is completely down to the implementation. 1214 /// Remember that because you're a mixin template, you can use referencing counting, disable the copy ctor, etc! 1215 /// 1216 /// The slice will never be escaped by `AnsiText` itself, and will not be stored beyond a single function call. 1217 char[] newSlice(size_t minLength); 1218 1219 /// Outputs the styled string into the provided sink. 1220 /// 1221 /// Typically this is an OutputRange that can handle `char[]`s, but it can really be whatever the implementation wants to support. 1222 void toSink(Sink)(Sink sink); 1223 1224 static if(NotCompilingUnderBetterC && ImplementationDoesntDefineToString) 1225 final string toString() 1226 { 1227 // Autogenerated GC-based implementation provided by `AnsiText`. 1228 // 1229 // For implementations where this can be generated, it just makes them a little easier for the user 1230 // to use with things like `writeln`. 1231 // 1232 // The `static if` shows the conditions for this to happen. 1233 } 1234 } 1235 + ``` 1236 + ++/ 1237 struct AnsiText(alias ImplementationMixin) 1238 { 1239 mixin ImplementationMixin; 1240 alias ___TEST = TestAnsiTextImpl!(typeof(this)); 1241 1242 void put()(const(char)[] text, AnsiColour fg = AnsiColour.init, AnsiColour bg = AnsiColour.bgInit, AnsiStyle style = AnsiStyle.init) 1243 { 1244 fg.isBg = IsBgColour.no; 1245 bg.isBg = IsBgColour.yes; 1246 1247 char[AnsiStyleSet.MAX_CHARS_NEEDED] sequence; 1248 auto sequenceSlice = AnsiStyleSet.init.fg(fg).bg(bg).style(style).toSequence(sequence); 1249 1250 auto minLength = ANSI_CSI.length + sequenceSlice.length + /*ANSI_COLOUR_END*/1 + text.length + ((sequenceSlice.length > 0) ? 2 : 1); // Last one is for the '0' or '0;' 1251 char[] slice = this.newSlice(minLength); 1252 size_t cursor; 1253 1254 void appendToSlice(const(char)[] source) 1255 { 1256 slice[cursor..cursor+source.length] = source[]; 1257 cursor += source.length; 1258 } 1259 1260 appendToSlice(ANSI_CSI); 1261 appendToSlice("0"); // Reset all previous styling 1262 if(sequenceSlice.length > 0) 1263 slice[cursor++] = ANSI_SEPARATOR; 1264 appendToSlice(sequenceSlice); 1265 slice[cursor++] = ANSI_COLOUR_END; 1266 appendToSlice(text); 1267 } 1268 1269 /// ditto. 1270 void put()(const(char)[] text, AnsiStyleSet styling) 1271 { 1272 this.put(text, styling.fg, styling.bg, styling.style); 1273 } 1274 1275 /// ditto. 1276 void put()(AnsiTextLite text) 1277 { 1278 this.put(text.text, text.fg, text.bg, text.style); 1279 } 1280 1281 // Generate a String-based toString if circumstances allow. 1282 static if( 1283 Features == AnsiTextImplementationFeatures.basic 1284 && !__traits(hasMember, typeof(this), "toString") 1285 && __traits(compiles, { struct S{void put(const(char)[]){}} S s; typeof(this).init.toSink(s); }) // Check if this toSink can take a char[] output range. 1286 ) 1287 { 1288 /++ 1289 + Provides this `AnsiText` as a printable string. 1290 + 1291 + If the implementation is a basic implementation (see the documentation for `AnsiText`); if the 1292 + implementation doesn't define its own `toString then 1293 + `AnsiText` will generate this function on behalf of the implementation. 1294 + 1295 + Description: 1296 + For basic implementations this function will call `toSink` with an `Appender!(char[])` as the sink. 1297 + 1298 + For $(B this default generated) implementation of `toString`, it is a seperate String so is 1299 + fine for any usage. If an implementation defines its own `toString` then it should also document what the lifetime 1300 + of its returned string is. 1301 + 1302 + Returns: 1303 + This `AnsiText` as a useable string. 1304 + ++/ 1305 String toString()() 1306 { 1307 String data; 1308 this.toSink(data); 1309 1310 return data; 1311 } 1312 1313 /++ 1314 + [Not enabled with -betterC] Provides the sink-based version of the autogenerated `toString`. 1315 + 1316 + This functions and is generated under the same conditions as the parameterless `toString`, except it 1317 + supports the sink-based interface certain parts of Phobos recognises, helping to prevent needless allocations. 1318 + 1319 + This function simply wraps the given `sink` and forwards it to the implementation's `toSink` function, so there's no 1320 + implicit GC overhead as with the other `toString`. (At least, not by `AnsiText` itself.) 1321 + ++/ 1322 void toString(scope void delegate(const(char)[]) sink) 1323 { 1324 struct Sink 1325 { 1326 void put(const(char)[] slice) 1327 { 1328 sink(slice); 1329 } 1330 } 1331 1332 Sink s; 1333 this.toSink(s); 1334 } 1335 } 1336 } 1337 1338 private template TestAnsiTextImpl(alias TextT) 1339 { 1340 // Ensures that the implementation has the required functions, and that they can be used in every required way. 1341 static assert(__traits(hasMember, TextT, "Features"), 1342 "Implementation must define: `enum Features = AnsiTextImplementationFeatures.xxx;`" 1343 ); 1344 1345 static if(TextT.Features == AnsiTextImplementationFeatures.basic) 1346 { 1347 static assert(__traits(hasMember, TextT, "newSlice"), 1348 "Implementation must define: `char[] newSlice(size_t minLength)`" 1349 ); 1350 static assert(__traits(hasMember, TextT, "toSink"), 1351 "Implementation must define: `void toSink(Sink)(Sink sink)`" 1352 ); 1353 } 1354 } 1355 1356 /// 1357 template AnsiTextStackImplementation(size_t Capacity) 1358 { 1359 mixin template AnsiTextStackImplementation() 1360 { 1361 enum Features = AnsiTextImplementationFeatures.basic; 1362 1363 private char[Capacity] _output; 1364 private size_t _cursor; 1365 1366 // This code by itself is *technically* safe, but the way the user uses it might not be. 1367 1368 @safe @nogc 1369 char[] newSlice(size_t minLength) nothrow 1370 { 1371 const end = this._cursor + minLength; 1372 assert(end <= this._output.length, "Ran out of space."); 1373 1374 auto slice = this._output[this._cursor..end]; 1375 this._cursor = end; 1376 1377 return slice; 1378 } 1379 1380 void toSink(Sink)(ref Sink sink) 1381 if(isOutputRange!(Sink, char[])) 1382 { 1383 sink.put(this.asStackSlice); 1384 sink.put(ANSI_COLOUR_RESET); 1385 } 1386 1387 @safe @nogc 1388 char[] asStackSlice() nothrow 1389 { 1390 return this._output[0..this._cursor]; 1391 } 1392 1393 @safe @nogc 1394 char[Capacity] asStackSliceCopy(ref size_t lengthInUse) nothrow 1395 { 1396 lengthInUse = this._cursor; 1397 return this._output; 1398 } 1399 } 1400 } 1401 1402 /++ 1403 + A basic implementation using a static amount of stack memory. 1404 + 1405 + Sinks should keep in mind that they're being passed a slice to stack memory, so should not persist slices outside of their `.put` function, 1406 + they must instead make a copy of the data. 1407 + 1408 + This implementation will fail an assert if the user attempts to push more data into it than it can handle. 1409 + 1410 + Params: 1411 + Capacity = The amount of characters to use on the stack. 1412 + ++/ 1413 alias AnsiTextStack(size_t Capacity) = AnsiText!(AnsiTextStackImplementation!Capacity); 1414 1415 /+++ READING/PARSING +++/ 1416 1417 /++ 1418 + Executes the SGR sequence found in `input`, and populates the passed in `style` based on the command sequence. 1419 + 1420 + Anything directly provided by this library is supported. 1421 + 1422 + The previous state of `style` is preserved unless specifically untoggled/reset via the command sequence (e.g. `ESC[0m` to reset everything). 1423 + 1424 + If an error occurs during execution of the sequence, the given `style` is left completely unmodified. 1425 + 1426 + Params: 1427 + input = The slice containing the command sequence. The first character should be the start (`ANSI_CSI`) character of the sequence (`\033`), and 1428 + characters will continue to be read until the command sequence has been finished. Any characters after the command sequence are left unread. 1429 + style = A reference to an `AnsiStyleSet` to populate. As mentioned, this function will only untoggle styling, or reset the style if the command sequence specifies. 1430 + This value is left unmodified if an error is encountered. 1431 + charsRead = This value will be set to the amount of chars read from the given `input`, so the caller knows where to continue reading from (if applicable). 1432 + This value is populated on both error and success. 1433 + 1434 + Returns: 1435 + Either `null` on success, or a string describing the error that was encountered. 1436 + ++/ 1437 @safe @nogc 1438 string ansiExecuteSgrSequence(const(char)[] input, ref AnsiStyleSet style, out size_t charsRead) nothrow 1439 { 1440 enum ReadResult { foundEndMarker, foundSemiColon, foundEnd, foundBadCharacter } 1441 1442 if(input.length < 3) 1443 return "A valid SGR is at least 3 characters long: ESC[m"; 1444 1445 if(input[0..ANSI_CSI.length] != ANSI_CSI) 1446 return "Input does not start with the CSI: ESC["; 1447 1448 auto styleCopy = style; 1449 1450 charsRead = 2; 1451 ReadResult readToSemiColonOrEndMarker(ref const(char)[] slice) 1452 { 1453 const start = charsRead; 1454 while(true) 1455 { 1456 if(charsRead >= input.length) 1457 return ReadResult.foundEnd; 1458 1459 const ch = input[charsRead]; 1460 if(ch == 'm') 1461 { 1462 slice = input[start..charsRead]; 1463 return ReadResult.foundEndMarker; 1464 } 1465 else if(ch == ';') 1466 { 1467 slice = input[start..charsRead]; 1468 return ReadResult.foundSemiColon; 1469 } 1470 else if(ch >= '0' && ch <= '9') 1471 { 1472 charsRead++; 1473 continue; 1474 } 1475 else 1476 return ReadResult.foundBadCharacter; 1477 } 1478 } 1479 1480 int toValue(const(char)[] slice) 1481 { 1482 import libd.data.conv; 1483 return (slice.length == 0) ? 0 : fromBase10!int(slice).value; 1484 } 1485 1486 string resultToString(ReadResult result) 1487 { 1488 final switch(result) with(ReadResult) 1489 { 1490 case foundEnd: return "Unexpected end of input."; 1491 case foundBadCharacter: return "Unexpected character in input."; 1492 1493 case foundSemiColon: return "Unexpected semi-colon."; 1494 case foundEndMarker: return "Unexpected end marker ('m')."; 1495 } 1496 } 1497 1498 const(char)[] generalSlice; 1499 while(charsRead < input.length) 1500 { 1501 const ch = input[charsRead]; 1502 1503 switch(ch) 1504 { 1505 case '0':..case '9': 1506 auto result = readToSemiColonOrEndMarker(generalSlice); 1507 if(result != ReadResult.foundSemiColon && result != ReadResult.foundEndMarker) 1508 return resultToString(result); 1509 1510 const commandAsNum = toValue(generalSlice); 1511 Switch: switch(commandAsNum) 1512 { 1513 // Full reset 1514 case 0: styleCopy = AnsiStyleSet.init; break; 1515 1516 // Basic style flag setters. 1517 static foreach(name; __traits(allMembers, AnsiSgrStyle)) 1518 {{ 1519 enum member = __traits(getMember, AnsiSgrStyle, name); 1520 static if(member != AnsiSgrStyle.none) 1521 { 1522 case cast(int)member: 1523 styleCopy.style = styleCopy.style.set(member, true); 1524 break Switch; 1525 } 1526 }} 1527 1528 // Set foreground to a 4-bit colour. 1529 case 30:..case 37: 1530 case 90:..case 97: 1531 styleCopy.fg = cast(Ansi4BitColour)commandAsNum; 1532 break; 1533 1534 // Set background to a 4-bit colour. 1535 case 40:..case 47: 1536 case 100:..case 107: 1537 styleCopy.bg = cast(Ansi4BitColour)(commandAsNum - ANSI_FG_TO_BG_INCREMENT); // Since we work in the foreground colour until we're outputting to sequences. 1538 break; 1539 1540 // Set foreground (38) or background (48) to an 8-bit (5) or 24-bit (2) colour. 1541 case 38: 1542 case 48: 1543 if(result == ReadResult.foundEndMarker) 1544 return "Incomplete 'set foreground/background' command, expected another parameter, got none."; 1545 charsRead++; // Skip semi-colon. 1546 1547 result = readToSemiColonOrEndMarker(generalSlice); 1548 if(result != ReadResult.foundEndMarker && result != ReadResult.foundSemiColon) 1549 return resultToString(result); 1550 if(result == ReadResult.foundSemiColon) 1551 charsRead++; 1552 1553 const subcommand = toValue(generalSlice); 1554 if(subcommand == 5) 1555 { 1556 result = readToSemiColonOrEndMarker(generalSlice); 1557 if(result != ReadResult.foundEndMarker && result != ReadResult.foundSemiColon) 1558 return resultToString(result); 1559 if(result == ReadResult.foundSemiColon) 1560 charsRead++; 1561 1562 if(commandAsNum == 38) styleCopy.fg = cast(Ansi8BitColour)toValue(generalSlice); 1563 else styleCopy.bg = cast(Ansi8BitColour)toValue(generalSlice); 1564 } 1565 else if(subcommand == 2) 1566 { 1567 ubyte[3] components; 1568 foreach(i; 0..3) 1569 { 1570 result = readToSemiColonOrEndMarker(generalSlice); 1571 if(result != ReadResult.foundEndMarker && result != ReadResult.foundSemiColon) 1572 return resultToString(result); 1573 if(result == ReadResult.foundSemiColon) 1574 charsRead++; 1575 1576 components[i] = cast(ubyte)toValue(generalSlice); 1577 } 1578 1579 if(commandAsNum == 38) styleCopy.fg = AnsiRgbColour(components); 1580 else styleCopy.bg = AnsiRgbColour(components); 1581 } 1582 else 1583 break; // Assume it's a valid command, just that we don't support this specific sub command. 1584 break; 1585 1586 default: continue; // Assume it's just a command we don't support. 1587 } 1588 break; 1589 1590 case 'm': 1591 charsRead++; 1592 style = styleCopy; 1593 return null; 1594 1595 case ';': charsRead++; continue; 1596 default: return null; // Assume we've hit an end-marker we don't support. 1597 } 1598 } 1599 1600 return "Input did not contain an end marker."; 1601 } 1602 /// 1603 @("ansiExecuteSgrSequence") 1604 unittest 1605 { 1606 import libd.data.conv : to; 1607 1608 void test(AnsiStyleSet sourceAndExpected) 1609 { 1610 char[AnsiStyleSet.MAX_CHARS_NEEDED] buffer; 1611 String sequence; 1612 sequence.putMany(ANSI_CSI, sourceAndExpected.toSequence(buffer), ANSI_COLOUR_END); 1613 1614 AnsiStyleSet got; 1615 size_t charsRead; 1616 const error = ansiExecuteSgrSequence(sequence[0..$], got, charsRead); 1617 if(error !is null) 1618 assert(false, error); 1619 1620 assert(charsRead == sequence.length); 1621 assert(sourceAndExpected == got); 1622 } 1623 1624 test(AnsiStyleSet.init.fg(Ansi4BitColour.green)); 1625 test(AnsiStyleSet.init.fg(Ansi4BitColour.brightGreen)); 1626 test(AnsiStyleSet.init.bg(Ansi4BitColour.green)); 1627 test(AnsiStyleSet.init.bg(Ansi4BitColour.brightGreen)); 1628 test(AnsiStyleSet.init.fg(Ansi4BitColour.green).bg(Ansi4BitColour.brightRed)); 1629 1630 test(AnsiStyleSet.init.fg(20)); 1631 test(AnsiStyleSet.init.bg(40)); 1632 test(AnsiStyleSet.init.fg(20).bg(40)); 1633 1634 test(AnsiStyleSet.init.fg(AnsiRgbColour(255, 128, 64))); 1635 test(AnsiStyleSet.init.bg(AnsiRgbColour(255, 128, 64))); 1636 test(AnsiStyleSet.init.fg(AnsiRgbColour(255, 128, 64)).bg(AnsiRgbColour(64, 128, 255))); 1637 1638 static foreach(name; __traits(allMembers, AnsiSgrStyle)) 1639 {{ 1640 enum member = __traits(getMember, AnsiSgrStyle, name); 1641 static if(member != AnsiSgrStyle.none) 1642 test(AnsiStyleSet.init.style(AnsiStyle.init.set(member, true))); 1643 }} 1644 1645 test(AnsiStyleSet.init.style(AnsiStyle.init.bold.underline.slowBlink.italic)); 1646 } 1647 1648 /++ 1649 + The resulting object from `AnsiSectionRange`, describes whether a slice of text is an ANSI sequence or not. 1650 + ++/ 1651 struct AnsiSection 1652 { 1653 /// `true` if the slice is an ANSI sequence, `false` if it's just text. 1654 bool isAnsiSequence; 1655 1656 /// The slice of text that this section consists of. 1657 const(char)[] slice; 1658 } 1659 1660 /++ 1661 + An input range of `AnsiSection`s that splits a piece of text up into ANSI sequence and plain text sections. 1662 + 1663 + For example, the text "\033[37mABC\033[0m" has three sections: [ANSI "\033[37m", TEXT "ABC", ANSI "\033[0m"]. 1664 + ++/ 1665 struct AnsiSectionRange 1666 { 1667 private 1668 { 1669 const(char)[] _input; 1670 size_t _cursor; 1671 AnsiSection _front; 1672 bool _empty = true; // So .init.empty is true 1673 } 1674 1675 @safe @nogc nothrow: 1676 1677 /// 1678 this(const(char)[] input) 1679 { 1680 this._input = input; 1681 this._empty = false; 1682 this.popFront(); 1683 } 1684 1685 /// 1686 bool empty() const 1687 { 1688 return this._empty; 1689 } 1690 1691 /// 1692 AnsiSection front() const 1693 { 1694 return this._front; 1695 } 1696 1697 /// 1698 void popFront() 1699 { 1700 assert(!this.empty, "Cannot pop empty range."); 1701 1702 if(this._cursor >= this._input.length) 1703 { 1704 this._empty = true; 1705 return; 1706 } 1707 1708 if((this._input.length - this._cursor) >= ANSI_CSI.length 1709 && this._input[this._cursor..this._cursor + ANSI_CSI.length] == ANSI_CSI) 1710 this.readSequence(); 1711 else 1712 this.readText(); 1713 } 1714 1715 private void readText() 1716 { 1717 const start = this._cursor; 1718 1719 while(this._cursor < this._input.length) 1720 { 1721 if((this._input.length - this._cursor) >= 2 && this._input[this._cursor..this._cursor+2] == ANSI_CSI) 1722 break; 1723 1724 this._cursor++; 1725 } 1726 1727 this._front.isAnsiSequence = false; 1728 this._front.slice = this._input[start..this._cursor]; 1729 } 1730 1731 private void readSequence() 1732 { 1733 const start = this._cursor; 1734 this._cursor += ANSI_CSI.length; // Already validated by popFront. 1735 1736 while(this._cursor < this._input.length 1737 && this.isValidAnsiChar(this._input[this._cursor])) 1738 this._cursor++; 1739 1740 if(this._cursor < this._input.length) 1741 this._cursor++; // We've hit a non-ansi character, so we increment to include it in the output. 1742 1743 this._front.isAnsiSequence = true; 1744 this._front.slice = this._input[start..this._cursor]; 1745 } 1746 1747 private bool isValidAnsiChar(char ch) 1748 { 1749 return ( 1750 (ch >= '0' && ch <= '9') 1751 || ch == ';' 1752 ); 1753 } 1754 } 1755 /// 1756 @("AnsiSectionRange") 1757 unittest 1758 { 1759 assert(AnsiSectionRange.init.empty); 1760 assert("".asAnsiSections.empty); 1761 1762 auto r = "No Ansi".asAnsiSections; 1763 assert(!r.empty); 1764 assert(!r.front.isAnsiSequence); 1765 assert(r.front.slice == "No Ansi"); 1766 1767 r = "\033[m".asAnsiSections; 1768 assert(!r.empty); 1769 assert(r.front.isAnsiSequence); 1770 assert(r.front.slice == "\033[m"); 1771 1772 r = "\033[38;2;255;128;64;1;4;48;5;2m".asAnsiSections; 1773 assert(!r.empty); 1774 assert(r.front.isAnsiSequence); 1775 assert(r.front.slice == "\033[38;2;255;128;64;1;4;48;5;2m"); 1776 1777 r = "\033[mABC\033[m".asAnsiSections; 1778 assert(r.front.isAnsiSequence); 1779 assert(r.front.slice == "\033[m", r.front.slice); 1780 r.popFront(); 1781 assert(!r.empty); 1782 assert(!r.front.isAnsiSequence); 1783 assert(r.front.slice == "ABC", r.front.slice); 1784 r.popFront(); 1785 assert(!r.empty); 1786 assert(r.front.isAnsiSequence); 1787 assert(r.front.slice == "\033[m"); 1788 r.popFront(); 1789 assert(r.empty); 1790 1791 r = "ABC\033[mDEF".asAnsiSections; 1792 assert(!r.front.isAnsiSequence); 1793 assert(r.front.slice == "ABC"); 1794 r.popFront(); 1795 assert(r.front.isAnsiSequence); 1796 assert(r.front.slice == "\033[m"); 1797 r.popFront(); 1798 assert(!r.front.isAnsiSequence); 1799 assert(r.front.slice == "DEF"); 1800 r.popFront(); 1801 assert(r.empty); 1802 } 1803 1804 /+++ PUBLIC HELPERS +++/ 1805 1806 /// Determines if `CT` is a valid RGB data type. 1807 enum isUserDefinedRgbType(CT) = 1808 ( 1809 __traits(hasMember, CT, "r") 1810 && __traits(hasMember, CT, "g") 1811 && __traits(hasMember, CT, "b") 1812 ); 1813 1814 /++ 1815 + Converts any suitable data type into an `AnsiColour`. 1816 + 1817 + Params: 1818 + colour = The colour to convert. 1819 + 1820 + Returns: 1821 + An `AnsiColour` created from the given `colour`. 1822 + 1823 + See_Also: 1824 + `isUserDefinedRgbType` 1825 + ++/ 1826 AnsiColour to(T : AnsiColour, CT)(CT colour) 1827 if(isUserDefinedRgbType!CT) 1828 { 1829 return AnsiColour(colour.r, colour.g, colour.b); 1830 } 1831 /// 1832 @("to!AnsiColour(User defined)") 1833 @safe @nogc nothrow pure 1834 unittest 1835 { 1836 static struct RGB 1837 { 1838 ubyte r; 1839 ubyte g; 1840 ubyte b; 1841 } 1842 1843 assert(RGB(255, 128, 64).to!AnsiColour == AnsiColour(255, 128, 64)); 1844 } 1845 1846 /// ditto. 1847 AnsiColour toBg(T)(T c) 1848 { 1849 auto colour = to!AnsiColour(c); 1850 colour.isBg = IsBgColour.yes; 1851 return colour; 1852 } 1853 /// 1854 @("toBg") 1855 @safe @nogc nothrow pure 1856 unittest 1857 { 1858 static struct RGB 1859 { 1860 ubyte r; 1861 ubyte g; 1862 ubyte b; 1863 } 1864 1865 assert(RGB(255, 128, 64).toBg == AnsiColour(255, 128, 64, IsBgColour.yes)); 1866 } 1867 1868 /++ 1869 + Creates an `AnsiTextLite` from the given `text`. This function is mostly used when using 1870 + the fluent UFCS chaining pattern. 1871 + 1872 + Params: 1873 + text = The text to use. 1874 + 1875 + Returns: 1876 + An `AnsiTextLite` from the given `text`. 1877 + ++/ 1878 @safe @nogc 1879 AnsiTextLite ansi(const(char)[] text) nothrow pure 1880 { 1881 return AnsiTextLite(text); 1882 } 1883 /// 1884 @("ansi") 1885 unittest 1886 { 1887 version(none) 1888 { 1889 import std.stdio; 1890 writeln("Hello, World!".ansi 1891 .fg(Ansi4BitColour.red) 1892 .bg(AnsiRgbColour(128, 128, 128)) 1893 .style(AnsiStyle.init.bold.underline) 1894 ); 1895 } 1896 } 1897 1898 /++ 1899 + Constructs an `AnsiSectionRange` from the given `slice`. 1900 + ++/ 1901 @safe @nogc 1902 AnsiSectionRange asAnsiSections(const(char)[] slice) nothrow 1903 { 1904 return AnsiSectionRange(slice); 1905 } 1906 1907 /+++ PRIVATE HELPERS +++/ 1908 private char[] numToStrBase10(NumT)(char[] buffer, NumT num) 1909 { 1910 if(num == 0) 1911 { 1912 if(buffer.length > 0) 1913 { 1914 buffer[0] = '0'; 1915 return buffer[0..1]; 1916 } 1917 else 1918 return null; 1919 } 1920 1921 const CHARS = "0123456789"; 1922 1923 ptrdiff_t i = buffer.length; 1924 while(i > 0 && num > 0) 1925 { 1926 buffer[--i] = CHARS[num % 10]; 1927 num /= 10; 1928 } 1929 1930 return buffer[i..$]; 1931 } 1932 /// 1933 @("numToStrBase10") 1934 unittest 1935 { 1936 char[2] b; 1937 assert(numToStrBase10(b, 32) == "32"); 1938 }