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