1 /++ 2 A collection of functions that relate to applying ANSI effects to text. 3 4 This submodule has to do with terminal text colouring and is therefore 5 gated behind version `Colours`. 6 7 Example: 8 --- 9 Appender!(char[]) sink; 10 11 // Output range version 12 sink.put("Hello "); 13 sink.applyANSI(TerminalForeground.red, ANSICodeType.foreground); 14 sink.put("world!"); 15 sink.applyANSI(TerminalForeground.default_, ANSICodeType.foreground); 16 17 with (TerminalForeground) 18 { 19 // Normal string-returning versions 20 writeln("Hello ", red.asANSI, "world!", default_.asANSI); 21 writeln("H3LL0".withANSI(red), ' ', "W0RLD!".withANSI(default_)); 22 } 23 24 // Also accepts RGB form 25 sink.put(" Also"); 26 sink.applyTruecolour(128, 128, 255); 27 sink.put("magic"); 28 sink.applyANSI(TerminalForeground.default_); 29 30 with (TerminalForeground) 31 { 32 writeln("Also ", asTruecolour(128, 128, 255), "magic", default_.asANSI); 33 } 34 35 immutable line = "Surrounding text kameloso surrounding text"; 36 immutable kamelosoInverted = line.invert("kameloso"); 37 assert(line != kamelosoInverted); 38 39 immutable nicknameTint = "nickname".getColourByHash(*kameloso.common.settings); 40 immutable substringTint = "substring".getColourByHash(*kameloso.common.settings); 41 --- 42 43 It is used heavily in the Printer plugin, to format sections of its output 44 in different colours, but it's generic enough to use anywhere. 45 46 The output range versions are cumbersome but necessary to minimise the number 47 of strings generated. 48 49 See_Also: 50 [kameloso.terminal.colours.defs], 51 [kameloso.terminal.colours.tags], 52 [kameloso.terminal] 53 54 Copyright: [JR](https://github.com/zorael) 55 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 56 57 Authors: 58 [JR](https://github.com/zorael) 59 +/ 60 module kameloso.terminal.colours; 61 62 version(Colours): 63 64 private: 65 66 import kameloso.terminal : TerminalToken; 67 import kameloso.terminal.colours.defs : ANSICodeType; 68 import kameloso.pods : CoreSettings; 69 import std.range : isOutputRange; 70 import std.typecons : Flag, No, Yes; 71 72 public: 73 74 75 // applyANSI 76 /++ 77 Applies an ANSI code to a passed output range. 78 79 Example: 80 --- 81 Appender!(char[]) sink; 82 83 sink.put("Hello "); 84 sink.applyANSI(TerminalForeground.red, ANSICodeType.foreground); 85 sink.put("world!"); 86 sink.applyANSI(TerminalForeground.default_, ANSICodeType.foreground); 87 --- 88 89 Params: 90 sink = Output range sink to write to. 91 code = ANSI code to apply. 92 overrideType = Force a specific [kameloso.terminal.colours.defs.ANSICodeType|ANSICodeType] 93 in cases where there is ambiguity. 94 +/ 95 void applyANSI(Sink) 96 (auto ref Sink sink, 97 const uint code, 98 const ANSICodeType overrideType = ANSICodeType.unset) 99 if (isOutputRange!(Sink, char[])) 100 { 101 import lu.conv : toAlphaInto; 102 103 void putBasic() 104 { 105 code.toAlphaInto(sink); 106 } 107 108 void putExtendedForegroundColour() 109 { 110 enum foregroundPrelude = "38;5;"; 111 sink.put(foregroundPrelude); 112 code.toAlphaInto(sink); 113 } 114 115 void putExtendedBackgroundColour() 116 { 117 enum backgroundPrelude = "48;5;"; 118 sink.put(backgroundPrelude); 119 code.toAlphaInto(sink); 120 } 121 122 sink.put(cast(char)TerminalToken.format); 123 sink.put('['); 124 scope(exit) sink.put('m'); 125 126 with (ANSICodeType) 127 final switch (overrideType) 128 { 129 case foreground: 130 if (((code >= 30) && (code <= 39)) || 131 ((code >= 90) && (code <= 97))) 132 { 133 // Basic foreground colour 134 return putBasic(); 135 } 136 else 137 { 138 // Extended foreground colour 139 return putExtendedForegroundColour(); 140 } 141 142 case background: 143 if (((code >= 40) && (code <= 49)) || 144 ((code >= 100) && (code <= 107))) 145 { 146 // Basic background colour 147 return putBasic(); 148 } 149 else 150 { 151 // Extended background colour 152 return putExtendedBackgroundColour(); 153 } 154 155 case format: 156 case reset: 157 return putBasic(); 158 159 case unset: 160 // Infer as best as possible 161 switch (code) 162 { 163 case 1: 164 .. 165 case 8: 166 // Format 167 goto case; 168 169 case 0: 170 case 21: 171 .. 172 case 28: 173 // Reset token 174 goto case; 175 176 case 40: 177 .. 178 case 49: 179 case 100: 180 .. 181 case 107: 182 // Background colour 183 //enum backgroundPrelude = "48;5;"; 184 //sink.put(backgroundPrelude); 185 goto case; 186 187 case 30: 188 .. 189 case 39: 190 case 90: 191 .. 192 case 97: 193 // Basic foreground colour 194 return putBasic(); 195 196 default: 197 // Extended foreground colour 198 return putExtendedForegroundColour(); 199 } 200 } 201 } 202 203 204 // withANSI 205 /++ 206 Applies an ANSI code to a passed string and returns it as a new one. 207 Convenience function to colour a piece of text without being passed an 208 output sink to fill into. 209 210 Example: 211 --- 212 with (TerminalForeground) 213 { 214 // Normal string-returning versions 215 writeln("Hello ", red.asANSI, "world!", default_.asANSI); 216 writeln("H3LL0".withANSI(red), ' ', "W0RLD!".withANSI(default_)); 217 } 218 --- 219 220 Params: 221 text = Original string. 222 code = ANSI code. 223 overrideType = Force a specific [kameloso.terminal.colours.defs.ANSICodeType|ANSICodeType] 224 in cases where there is ambiguity. 225 226 Returns: 227 A new string consisting of the passed `text` argument, but with the supplied 228 ANSI code applied. 229 +/ 230 string withANSI( 231 const string text, 232 const uint code, 233 const ANSICodeType overrideType = ANSICodeType.unset) pure @safe nothrow 234 { 235 import kameloso.terminal.colours.defs : TerminalReset; 236 import std.array : Appender; 237 238 Appender!(char[]) sink; 239 sink.reserve(text.length + 8); 240 sink.applyANSI(code, overrideType); 241 sink.put(text); 242 sink.applyANSI(TerminalReset.all); 243 return sink.data; 244 } 245 246 247 // asANSI 248 /++ 249 Returns an ANSI format sequence containing the passed code. 250 251 Params: 252 code = ANSI code. 253 254 Returns: 255 A string containing the passed ANSI `code` as an ANSI sequence. 256 +/ 257 string asANSI(const uint code) pure @safe nothrow 258 { 259 import std.array : Appender; 260 261 Appender!(char[]) sink; 262 sink.reserve(16); 263 sink.applyANSI(code); 264 return sink.data; 265 } 266 267 268 // normaliseColoursBright 269 /++ 270 Takes a colour and, if it deems it is too bright to see on a light terminal 271 background, makes it darker. 272 273 Example: 274 --- 275 int r = 255; 276 int g = 128; 277 int b = 100; 278 normaliseColoursBright(r, g, b); 279 assert(r != 255); 280 assert(g != 128); 281 assert(b != 100); 282 --- 283 284 Params: 285 r = Reference to a red value. 286 g = Reference to a green value. 287 b = Reference to a blue value. 288 +/ 289 private void normaliseColoursBright(ref uint r, ref uint g, ref uint b) pure @safe nothrow @nogc 290 { 291 enum pureWhiteReplacement = 120; 292 enum pureWhiteRange = 200; 293 294 enum darkenUpperLimit = 255; 295 enum darkenLowerLimit = 200; 296 enum darken = 45; 297 298 // Sanity check 299 if (r > 255) r = 255; 300 if (g > 255) g = 255; 301 if (b > 255) b = 255; 302 303 if ((r + g + b) == 3*255) 304 { 305 // Specialcase pure white, set to grey and return 306 r = pureWhiteReplacement; 307 g = pureWhiteReplacement; 308 b = pureWhiteReplacement; 309 return; 310 } 311 312 // Darken high colours at high levels 313 r -= ((r <= darkenUpperLimit) && (r > darkenLowerLimit)) * darken; 314 g -= ((g <= darkenUpperLimit) && (g > darkenLowerLimit)) * darken; 315 b -= ((b <= darkenUpperLimit) && (b > darkenLowerLimit)) * darken; 316 317 if ((r > pureWhiteRange) && (b > pureWhiteRange) && (g > pureWhiteRange)) 318 { 319 r = pureWhiteReplacement; 320 g = pureWhiteReplacement; 321 b = pureWhiteReplacement; 322 } 323 324 // Sanity check 325 if (r > 255) r = 255; 326 if (g > 255) g = 255; 327 if (b > 255) b = 255; 328 } 329 330 331 // normaliseColours 332 /++ 333 Takes a colour and, if it deems it is too dark to see on a black terminal 334 background, makes it brighter. 335 336 Example: 337 --- 338 int r = 255; 339 int g = 128; 340 int b = 100; 341 normaliseColours(r, g, b); 342 assert(r != 255); 343 assert(g != 128); 344 assert(b != 100); 345 --- 346 347 Params: 348 r = Reference to a red value. 349 g = Reference to a green value. 350 b = Reference to a blue value. 351 +/ 352 private void normaliseColours(ref uint r, ref uint g, ref uint b) pure @safe nothrow @nogc 353 { 354 enum pureBlackReplacement = 120; 355 356 enum tooDarkThreshold = 100; 357 enum tooDarkIncrement = 40; 358 359 enum tooBlue = 130; 360 enum tooBlueOtherColourThreshold = 45; 361 362 enum highlight = 40; 363 364 enum darkenThreshold = 240; 365 enum darken = 20; 366 367 // Sanity check 368 if (r > 255) r = 255; 369 if (g > 255) g = 255; 370 if (b > 255) b = 255; 371 372 if ((r + g + b) == 0) 373 { 374 // Specialcase pure black, set to grey and return 375 r = pureBlackReplacement; 376 g = pureBlackReplacement; 377 b = pureBlackReplacement; 378 return; 379 } 380 381 // Raise all low colours 382 r += (r < tooDarkThreshold) * tooDarkIncrement; 383 g += (g < tooDarkThreshold) * tooDarkIncrement; 384 b += (b < tooDarkThreshold) * tooDarkIncrement; 385 386 // Make dark colours more vibrant 387 r += ((r > g) & (r > b)) * highlight; 388 g += ((g > b) & (g > r)) * highlight; 389 b += ((b > g) & (b > r)) * highlight; 390 391 // Whitewash blue slightly 392 if ((b > tooBlue) && (r < tooBlueOtherColourThreshold) && (g < tooBlueOtherColourThreshold)) 393 { 394 r += tooBlueOtherColourThreshold; 395 g += tooBlueOtherColourThreshold; 396 } 397 398 // Make bright colours more biased toward one colour 399 r -= ((r > darkenThreshold) && ((r < b) | (r < g))) * darken; 400 g -= ((g > darkenThreshold) && ((g < r) | (g < b))) * darken; 401 b -= ((b > darkenThreshold) && ((b < r) | (b < g))) * darken; 402 403 // Sanity check 404 if (r > 255) r = 255; 405 if (g > 255) g = 255; 406 if (b > 255) b = 255; 407 } 408 409 version(none) 410 unittest 411 { 412 import std.conv : to; 413 import std.stdio : write, writeln; 414 415 enum bright = Yes.brightTerminal; 416 // ▄█▀ 417 418 writeln("BRIGHT: ", bright); 419 420 foreach (r; 0..256) 421 { 422 immutable n = r % 10; 423 write(n.to!string.truecolour(r, 0, 0, bright)); 424 if (n == 0) write(r); 425 } 426 427 writeln(); 428 429 foreach (g; 0..256) 430 { 431 immutable n = g % 10; 432 write(n.to!string.truecolour(0, g, 0, bright)); 433 if (n == 0) write(g); 434 } 435 436 writeln(); 437 438 foreach (b; 0..256) 439 { 440 immutable n = b % 10; 441 write(n.to!string.truecolour(0, 0, b, bright)); 442 if (n == 0) write(b); 443 } 444 445 writeln(); 446 447 foreach (rg; 0..256) 448 { 449 immutable n = rg % 10; 450 write(n.to!string.truecolour(rg, rg, 0, bright)); 451 if (n == 0) write(rg); 452 } 453 454 writeln(); 455 456 foreach (rb; 0..256) 457 { 458 immutable n = rb % 10; 459 write(n.to!string.truecolour(rb, 0, rb, bright)); 460 if (n == 0) write(rb); 461 } 462 463 writeln(); 464 465 foreach (gb; 0..256) 466 { 467 immutable n = gb % 10; 468 write(n.to!string.truecolour(0, gb, gb, bright)); 469 if (n == 0) write(gb); 470 } 471 472 writeln(); 473 474 foreach (rgb; 0..256) 475 { 476 immutable n = rgb % 10; 477 write(n.to!string.truecolour(rgb, rgb, rgb, bright)); 478 if (n == 0) write(rgb); 479 } 480 481 writeln(); 482 } 483 484 485 // applyTruecolour 486 /++ 487 Produces a terminal colour token for the colour passed, expressed in terms 488 of red, green and blue, then writes it to the passed output range. 489 490 Example: 491 --- 492 Appender!(char[]) sink; 493 int r, g, b; 494 numFromHex("3C507D", r, g, b); 495 sink.applyTruecolour(r, g, b); 496 sink.put("Foo"); 497 sink.applyANSI(TerminalReset.all); 498 writeln(sink); // "Foo" in #3C507D 499 --- 500 501 Params: 502 sink = Output range to write the final code into. 503 r = Red value. 504 g = Green value. 505 b = Blue value. 506 bright = Whether the terminal has a bright background or not. 507 normalise = Whether or not to normalise colours so that they aren't too 508 dark or too bright. 509 +/ 510 void applyTruecolour(Sink) 511 (auto ref Sink sink, 512 uint r, 513 uint g, 514 uint b, 515 const Flag!"brightTerminal" bright = No.brightTerminal, 516 const Flag!"normalise" normalise = Yes.normalise) 517 if (isOutputRange!(Sink, char[])) 518 { 519 import lu.conv : toAlphaInto; 520 521 // \033[ 522 // 38 foreground 523 // 2 truecolour? 524 // r;g;bm 525 526 if (normalise) 527 { 528 if (bright) 529 { 530 normaliseColoursBright(r, g, b); 531 } 532 else 533 { 534 normaliseColours(r, g, b); 535 } 536 } 537 538 sink.put(cast(char)TerminalToken.format); 539 sink.put("[38;2;"); 540 r.toAlphaInto(sink); 541 sink.put(';'); 542 g.toAlphaInto(sink); 543 sink.put(';'); 544 b.toAlphaInto(sink); 545 sink.put('m'); 546 } 547 548 549 // asTruecolour 550 /++ 551 Produces a terminal colour token for the colour passed, expressed in terms 552 of red, green and blue. Convenience function to colour a piece of text 553 without being passed an output sink to fill into. 554 555 Example: 556 --- 557 string foo = "Foo Bar".asTruecolour(172, 172, 255); 558 559 int r, g, b; 560 numFromHex("003388", r, g, b); 561 string bar = "Bar Foo".asTruecolour(r, g, b); 562 --- 563 564 Params: 565 word = String to tint. 566 r = Red value. 567 g = Green value. 568 b = Blue value. 569 bright = Whether the terminal has a bright background or not. 570 normalise = Whether or not to normalise colours so that they aren't too 571 dark or too bright. 572 573 Returns: 574 The passed string word encompassed by terminal colour tags. 575 +/ 576 string asTruecolour( 577 const string word, 578 const uint r, 579 const uint g, 580 const uint b, 581 const Flag!"brightTerminal" bright = No.brightTerminal, 582 const Flag!"normalise" normalise = Yes.normalise) pure @safe 583 { 584 import kameloso.terminal.colours.defs : TerminalReset; 585 import std.array : Appender; 586 587 Appender!(char[]) sink; 588 // \033[38;2;255;255;255m<word>\033[m 589 // \033[48 for background 590 sink.reserve(word.length + 23); 591 592 sink.applyTruecolour(r, g, b, bright, normalise); 593 sink.put(word); 594 sink.applyANSI(TerminalReset.all); 595 return sink.data; 596 } 597 598 /// 599 unittest 600 { 601 import std.format : format; 602 603 immutable name = "blarbhl".asTruecolour(255, 255, 255, No.brightTerminal, No.normalise); 604 immutable alsoName = "%c[38;2;%d;%d;%dm%s%c[0m" 605 .format(cast(char)TerminalToken.format, 255, 255, 255, 606 "blarbhl", cast(char)TerminalToken.format); 607 608 assert((name == alsoName), alsoName); 609 } 610 611 612 // invert 613 /++ 614 Terminal-inverts the colours of a piece of text in a string. 615 616 Example: 617 --- 618 immutable line = "This is an example!"; 619 writeln(line.invert("example")); // "example" substring visually inverted 620 writeln(line.invert("EXAMPLE", Yes.caseInsensitive)); // "example" inverted as "EXAMPLE" 621 --- 622 623 Params: 624 line = Line to examine and invert a substring of. 625 toInvert = Substring to invert. 626 caseSensitive = Whether or not to look for matches case-insensitively, 627 yet invert with the casing passed. 628 629 Returns: 630 Line with the substring in it inverted, if inversion was successful, 631 else (a duplicate of) the line unchanged. 632 +/ 633 string invert( 634 const string line, 635 const string toInvert, 636 const Flag!"caseSensitive" caseSensitive = Yes.caseSensitive) pure @safe 637 { 638 import kameloso.terminal.colours.defs : TerminalFormat, TerminalReset; 639 import dialect.common : isValidNicknameCharacter; 640 import std.array : Appender; 641 import std.format : format; 642 import std.string : indexOf; 643 644 ptrdiff_t startpos; 645 646 if (caseSensitive) 647 { 648 startpos = line.indexOf(toInvert); 649 } 650 else 651 { 652 import std.algorithm.searching : countUntil; 653 import std.uni : asLowerCase; 654 startpos = line.asLowerCase.countUntil(toInvert.asLowerCase); 655 } 656 657 //assert((startpos != -1), "Tried to invert nonexistent text"); 658 if (startpos == -1) return line; 659 660 enum pattern = "%c[%dm%s%c[%dm"; 661 immutable inverted = pattern.format( 662 TerminalToken.format, 663 TerminalFormat.reverse, 664 toInvert, 665 TerminalToken.format, 666 TerminalReset.invert); 667 668 Appender!(char[]) sink; 669 sink.reserve(line.length + 16); 670 string slice = line; // mutable 671 672 uint i; 673 674 do 675 { 676 immutable endpos = startpos + toInvert.length; 677 678 if ((startpos == 0) && (i > 0)) 679 { 680 // Not the first run and begins with the nick --> run-on nicks 681 sink.put(slice[0..endpos]); 682 } 683 else if (endpos == slice.length) 684 { 685 // Line ends with the string; break 686 sink.put(slice[0..startpos]); 687 sink.put(inverted); 688 //break; 689 } 690 else if ((startpos > 1) && slice[startpos-1].isValidNicknameCharacter) 691 { 692 // string is in the middle of a string, like abcTHISdef; skip 693 sink.put(slice[0..endpos]); 694 } 695 else if (slice[endpos].isValidNicknameCharacter) 696 { 697 // string ends with a nick character --> different nick; skip 698 sink.put(slice[0..endpos]); 699 } 700 else 701 { 702 // Begins at string start, or trailed by non-nickname character 703 sink.put(slice[0..startpos]); 704 sink.put(inverted); 705 } 706 707 ++i; 708 slice = slice[endpos..$]; 709 startpos = slice.indexOf(toInvert); 710 } 711 while (startpos != -1); 712 713 // Add the remainder, from the last match to the end 714 sink.put(slice); 715 716 return sink.data; 717 } 718 719 /// 720 unittest 721 { 722 import kameloso.terminal.colours.defs : TerminalFormat, TerminalReset; 723 import std.format : format; 724 725 immutable pre = "%c[%dm".format(TerminalToken.format, TerminalFormat.reverse); 726 immutable post = "%c[%dm".format(TerminalToken.format, TerminalReset.invert); 727 728 { 729 immutable line = "abc".invert("abc"); 730 immutable expected = pre ~ "abc" ~ post; 731 assert((line == expected), line); 732 } 733 { 734 immutable line = "abc abc".invert("abc"); 735 immutable inverted = pre ~ "abc" ~ post; 736 immutable expected = inverted ~ ' ' ~ inverted; 737 assert((line == expected), line); 738 } 739 { 740 immutable line = "abca abc".invert("abc"); 741 immutable inverted = pre ~ "abc" ~ post; 742 immutable expected = "abca " ~ inverted; 743 assert((line == expected), line); 744 } 745 { 746 immutable line = "abcabc".invert("abc"); 747 immutable expected = "abcabc"; 748 assert((line == expected), line); 749 } 750 { 751 immutable line = "kameloso^^".invert("kameloso"); 752 immutable expected = "kameloso^^"; 753 assert((line == expected), line); 754 } 755 { 756 immutable line = "foo kameloso bar".invert("kameloso"); 757 immutable expected = "foo " ~ pre ~ "kameloso" ~ post ~ " bar"; 758 assert((line == expected), line); 759 } 760 { 761 immutable line = "fookameloso bar".invert("kameloso"); 762 immutable expected = "fookameloso bar"; 763 assert((line == expected), line); 764 } 765 { 766 immutable line = "foo kamelosobar".invert("kameloso"); 767 immutable expected = "foo kamelosobar"; 768 assert((line == expected), line); 769 } 770 { 771 immutable line = "foo(kameloso)bar".invert("kameloso"); 772 immutable expected = "foo(" ~ pre ~ "kameloso" ~ post ~ ")bar"; 773 assert((line == expected), line); 774 } 775 { 776 immutable line = "kameloso: 8ball".invert("kameloso"); 777 immutable expected = pre ~ "kameloso" ~ post ~ ": 8ball"; 778 assert((line == expected), line); 779 } 780 { 781 immutable line = "Welcome to the freenode Internet Relay Chat Network kameloso^" 782 .invert("kameloso^"); 783 immutable expected = "Welcome to the freenode Internet Relay Chat Network " ~ 784 pre ~ "kameloso^" ~ post; 785 assert((line == expected), line); 786 } 787 { 788 immutable line = "kameloso^: wfwef".invert("kameloso^"); 789 immutable expected = pre ~ "kameloso^" ~ post ~ ": wfwef"; 790 assert((line == expected), line); 791 } 792 { 793 immutable line = "[kameloso^]".invert("kameloso^"); 794 immutable expected = "[kameloso^]"; 795 assert((line == expected), line); 796 } 797 { 798 immutable line = `"kameloso^"`.invert("kameloso^"); 799 immutable expected = "\"" ~ pre ~ "kameloso^" ~ post ~ "\""; 800 assert((line == expected), line); 801 } 802 { 803 immutable line = "kameloso^".invert("kameloso"); 804 immutable expected = "kameloso^"; 805 assert((line == expected), line); 806 } 807 { 808 immutable line = "That guy kameloso? is a bot".invert("kameloso"); 809 immutable expected = "That guy " ~ pre ~ "kameloso" ~ post ~ "? is a bot"; 810 assert((line == expected), line); 811 } 812 { 813 immutable line = "kameloso`".invert("kameloso"); 814 immutable expected = "kameloso`"; 815 assert((line == expected), line); 816 } 817 { 818 immutable line = "kameloso9".invert("kameloso"); 819 immutable expected = "kameloso9"; 820 assert((line == expected), line); 821 } 822 { 823 immutable line = "kameloso-".invert("kameloso"); 824 immutable expected = "kameloso-"; 825 assert((line == expected), line); 826 } 827 { 828 immutable line = "kameloso_".invert("kameloso"); 829 immutable expected = "kameloso_"; 830 assert((line == expected), line); 831 } 832 { 833 immutable line = "kameloso_".invert("kameloso_"); 834 immutable expected = pre ~ "kameloso_" ~ post; 835 assert((line == expected), line); 836 } 837 { 838 immutable line = "kameloso kameloso kameloso kameloso kameloso".invert("kameloso"); 839 immutable expected = "%1$skameloso%2$s %1$skameloso%2$s %1$skameloso%2$s %1$skameloso%2$s %1$skameloso%2$s" 840 .format(pre, post); 841 assert((line == expected), line); 842 } 843 844 // Case-insensitive tests 845 846 { 847 immutable line = "KAMELOSO".invert("kameloso", No.caseSensitive); 848 immutable expected = pre ~ "kameloso" ~ post; 849 assert((line == expected), line); 850 } 851 { 852 immutable line = "KamelosoTV".invert("kameloso", No.caseSensitive); 853 immutable expected = "KamelosoTV"; 854 assert((line == expected), line); 855 } 856 { 857 immutable line = "Blah blah kAmElOsO Blah blah".invert("kameloso", No.caseSensitive); 858 immutable expected = "Blah blah " ~ pre ~ "kameloso" ~ post ~ " Blah blah"; 859 assert((line == expected), line); 860 } 861 { 862 immutable line = "Blah blah".invert("kameloso"); 863 immutable expected = "Blah blah"; 864 assert((line == expected), line); 865 } 866 { 867 immutable line = "".invert("kameloso"); 868 immutable expected = ""; 869 assert((line == expected), line); 870 } 871 { 872 immutable line = "KAMELOSO".invert("kameloso"); 873 immutable expected = "KAMELOSO"; 874 assert((line == expected), line); 875 } 876 } 877 878 879 // getColourByHash 880 /++ 881 Hashes the passed string and picks an ANSI colour for it by modulo. 882 883 Picks any colour, taking care not to pick black or white based on 884 the passed [kameloso.pods.CoreSettings|CoreSettings] struct (which has a 885 field that signifies a bright terminal background). 886 887 Example: 888 --- 889 immutable nickColour = "kameloso".getColourByHash(*kameloso.common.settings); 890 --- 891 892 Params: 893 word = String to hash and base colour on. 894 settings = A copy of the program-global [kameloso.pods.CoreSettings|CoreSettings]. 895 896 Returns: 897 A `uint` that can be used in an ANSI foreground colour sequence. 898 +/ 899 auto getColourByHash(const string word, const CoreSettings settings) pure @safe /*@nogc*/ nothrow 900 in (word.length, "Tried to get colour by hash but no word was given") 901 { 902 import kameloso.irccolours : ircANSIColourMap; 903 import kameloso.terminal.colours.defs : TerminalForeground; 904 import std.traits : EnumMembers; 905 906 static immutable basicForegroundMembers = [ EnumMembers!TerminalForeground ]; 907 908 static immutable uint[basicForegroundMembers.length+(-2)] brightTableBasic = 909 TerminalForeground.black ~ basicForegroundMembers[2..$-1]; 910 911 static immutable uint[basicForegroundMembers.length+(-2)] darkTableBasic = 912 TerminalForeground.white ~ basicForegroundMembers[2..$-1]; 913 914 static immutable brightTableExtended = () 915 { 916 uint[98] colourTable = ircANSIColourMap[1..$].dup; 917 918 // Tweak colours, darken some very bright ones 919 colourTable[0] = TerminalForeground.black; 920 colourTable[11] = TerminalForeground.yellow; 921 colourTable[53] = 224; 922 colourTable[65] = 222; 923 colourTable[77] = 223; 924 colourTable[78] = 190; 925 926 return colourTable; 927 }(); 928 929 static immutable darkTableExtended = () 930 { 931 uint[98] colourTable = ircANSIColourMap[1..$].dup; 932 933 // Tweak colours, brighten some very dark ones 934 colourTable[15] = 55; 935 colourTable[23] = 20; 936 colourTable[24] = 56; 937 colourTable[25] = 57; 938 colourTable[35] = 21; 939 colourTable[33] = 243; 940 colourTable[87] = 241; 941 colourTable[88] = 242; 942 colourTable[89] = 243; 943 colourTable[90] = 243; 944 colourTable[97] = 240; 945 return colourTable; 946 }(); 947 948 const table = settings.extendedColours ? 949 settings.brightTerminal ? 950 brightTableExtended : 951 darkTableExtended 952 : 953 settings.brightTerminal ? 954 brightTableBasic : 955 darkTableBasic; 956 957 immutable colourIndex = (hashOf(word) % table.length); 958 return table[colourIndex]; 959 } 960 961 /// 962 unittest 963 { 964 import std.conv : to; 965 966 CoreSettings brightSettings; 967 CoreSettings darkSettings; 968 brightSettings.brightTerminal = true; 969 970 { 971 immutable hash = getColourByHash("kameloso", darkSettings); 972 assert((hash == 227), hash.to!string); 973 } 974 { 975 immutable hash = getColourByHash("kameloso^", darkSettings); 976 assert((hash == 46), hash.to!string); 977 } 978 { 979 immutable hash = getColourByHash("zorael", brightSettings); 980 assert((hash == 35), hash.to!string); 981 } 982 { 983 immutable hash = getColourByHash("NO", brightSettings); 984 assert((hash == 90), hash.to!string); 985 } 986 } 987 988 989 // colourByHash 990 /++ 991 Shorthand function to colour a passed word by the hash of it. 992 993 Params: 994 word = String to colour. 995 settings = A copy of the program-global [kameloso.pods.CoreSettings|CoreSettings]. 996 997 Returns: 998 `word`, now in colour based on the hash of its contents. 999 +/ 1000 auto colourByHash(const string word, const CoreSettings settings) pure @safe nothrow 1001 { 1002 return word.withANSI(getColourByHash(word, settings)); 1003 } 1004 1005 /// 1006 unittest 1007 { 1008 import std.conv : to; 1009 1010 CoreSettings brightSettings; 1011 CoreSettings darkSettings; 1012 brightSettings.brightTerminal = true; 1013 1014 { 1015 immutable coloured = "kameloso".colourByHash(darkSettings); 1016 assert((coloured == "\033[38;5;227mkameloso\033[0m"), coloured); 1017 } 1018 { 1019 immutable coloured = "kameloso".colourByHash(brightSettings); 1020 assert((coloured == "\033[38;5;222mkameloso\033[0m"), coloured); 1021 } 1022 { 1023 immutable coloured = "zorael".colourByHash(darkSettings); 1024 assert((coloured == "\033[35mzorael\033[0m"), coloured); 1025 } 1026 { 1027 immutable coloured = "NO".colourByHash(brightSettings); 1028 assert((coloured == "\033[90mNO\033[0m"), coloured); 1029 } 1030 }