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 }