1 /++
2  + Simple coloring module for strings
3  +
4  + Copyright: Copyright (c) 2017, Christian Koestlin
5  + Authors: Christian Koestlin, Christian Köstlin
6  + License: MIT
7  +/
8 module colored;
9 
10 @safe:
11 
12 import std.algorithm : map, filter, joiner;
13 import std.array : join, split;
14 import std.conv : to;
15 import std.format : format;
16 import std.functional : not;
17 import std.range : ElementType, empty, front, popFront;
18 import std.regex : ctRegex, Captures, replaceAll;
19 import std.string : toUpper;
20 import std.traits : EnumMembers;
21 
22 version (unittest)
23 {
24     import core.thread : Thread;
25     import core.time : msecs;
26     import std.conv : to;
27     import std.process : environment;
28     import std.stdio : writeln, write;
29     import unit_threaded;
30 }
31 /// Available Colors
32 enum AnsiColor
33 {
34     black = 30,
35     red = 31,
36     green = 32,
37     yellow = 33,
38     blue = 34,
39     magenta = 35,
40     cyan = 36,
41     lightGray = 37,
42     defaultColor = 39,
43     darkGray = 90,
44     lightRed = 91,
45     lightGreen = 92,
46     lightYellow = 93,
47     lightBlue = 94,
48     lightMagenta = 95,
49     lightCyan = 96,
50     white = 97
51 }
52 
53 /// Available Styles
54 enum Style
55 {
56     bold = 1,
57     dim = 2,
58     underlined = 4,
59     blink = 5,
60     reverse = 7,
61     hidden = 8
62 }
63 
64 /// Internal structure to style a string
65 struct StyledString
66 {
67     private string unformatted;
68     private int[] befores;
69     private int[] afters;
70     /// Create a styled string
71     public this(string unformatted)
72     {
73         this.unformatted = unformatted;
74     }
75 
76     private StyledString addPair(int before, int after)
77     {
78         befores ~= before;
79         afters ~= after;
80         return this;
81     }
82 
83     StyledString setForeground(int color)
84     {
85         return addPair(color, 0);
86     }
87 
88     StyledString setBackground(int color)
89     {
90         return addPair(color + 10, 0);
91     }
92 
93     /// Add styling to a string
94     StyledString addStyle(int style)
95     {
96         return addPair(style, 0);
97     }
98 
99     string toString() const @safe
100     {
101         auto prefix = befores.map!(a => "\033[%dm".format(a)).join("");
102         auto suffix = afters.map!(a => "\033[%dm".format(a)).join("");
103         return "%s%s%s".format(prefix, unformatted, suffix);
104     }
105 
106     /// Concatenate with another string
107     string opBinary(string op : "~")(string rhs) @safe
108     {
109         return toString ~ rhs;
110     }
111 }
112 
113 /// Truecolor string
114 struct RGBString
115 {
116     private string unformatted;
117     /// Colorinformation
118     struct RGB
119     {
120         /// Red component 0..256
121         ubyte r;
122         /// Green component 0..256
123         ubyte g;
124         /// Blue component 0..256
125         ubyte b;
126     }
127 
128     private RGB* foreground;
129     private RGB* background;
130     /// Create RGB String
131     this(string unformatted)
132     {
133         this.unformatted = unformatted;
134     }
135 
136     /// Set color
137     auto rgb(ubyte r, ubyte g, ubyte b)
138     {
139         this.foreground = new RGB(r, g, b);
140         return this;
141     }
142 
143     /// Set background color
144     auto onRgb(ubyte r, ubyte g, ubyte b)
145     {
146         this.background = new RGB(r, g, b);
147         return this;
148     }
149 
150     string toString() @safe
151     {
152         auto res = "";
153         if (foreground != null)
154         {
155             res = "\033[38;2;%s;%s;%sm".format(foreground.r, foreground.g, foreground.b) ~ res;
156         }
157         if (background != null)
158         {
159             res = "\033[48;2;%s;%s;%sm".format(background.r, background.g, background.b) ~ res;
160         }
161         res ~= unformatted;
162         if (foreground != null || background != null)
163         {
164             res ~= "\033[0m";
165         }
166         return res;
167     }
168 }
169 
170 /// Convinient helper function
171 string rgb(string s, ubyte r, ubyte g, ubyte b)
172 {
173     return RGBString(s).rgb(r, g, b).toString;
174 }
175 
176 /// Convinient helper function
177 string onRgb(string s, ubyte r, ubyte g, ubyte b)
178 {
179     return RGBString(s).onRgb(r, g, b).toString;
180 }
181 
182 @system @("rgb") unittest
183 {
184     import std.experimental.color : RGBA8, convertColor;
185     import std.experimental.color.hsx : HSV;
186 
187     writeln("red: ", "r".rgb(255, 0, 0).onRgb(0, 255, 0));
188     writeln("green: ", "g".rgb(0, 255, 0).onRgb(0, 0, 255));
189     writeln("blue: ", "b".rgb(0, 0, 255).onRgb(255, 0, 0));
190     writeln("mixed: ", ("withoutColor" ~ "red".red.to!string ~ "withoutColor").bold);
191     for (int r = 0; r <= 255; r += 10)
192     {
193         for (int g = 0; g <= 255; g += 3)
194         {
195             write(" ".onRgb(cast(ubyte) r, cast(ubyte) g, cast(ubyte)(255 - r)));
196         }
197         writeln;
198     }
199 
200     int delay = environment.get("DELAY", "0").to!int;
201     for (int j = 0; j < 255; j += 1)
202     {
203         for (int i = 0; i < 255; i += 3)
204         {
205             auto c = HSV!ubyte(cast(ubyte)(i - j), 0xff, 0xff);
206             auto rgb = convertColor!RGBA8(c).tristimulus;
207             write(" ".onRgb(rgb[0].value, rgb[1].value, rgb[2].value));
208         }
209         Thread.sleep(delay.msecs);
210         write("\r");
211     }
212     writeln;
213 }
214 
215 @system @("styledstring") unittest
216 {
217     foreach (immutable color; [EnumMembers!AnsiColor])
218     {
219         auto colorName = "%s".format(color);
220         writeln(StyledString(colorName).setForeground(color));
221     }
222     foreach (immutable color; [EnumMembers!AnsiColor])
223     {
224         auto colorName = "bg%s".format(color);
225         writeln(StyledString(colorName).setBackground(color));
226     }
227     foreach (immutable style; [EnumMembers!Style])
228     {
229         auto styleName = "%s".format(style);
230         writeln(StyledString(styleName).addStyle(style));
231     }
232 
233     writeln("boldUnderlined".bold.underlined);
234     writeln("redOnGreenReverse".red.onGreen.reverse);
235 }
236 
237 @system @("styledstring ~") unittest
238 {
239     ("test".red ~ "blub").should == "\033[31mtest\033[0mblub";
240 }
241 
242 /// Create `color` and `onColor` functions for all enum members. e.g. "abc".green.onRed
243 auto colorMixin(T)()
244 {
245     string res = "";
246     foreach (immutable color; [EnumMembers!T])
247     {
248         auto t = typeof(T.init).stringof;
249         auto c = "%s".format(color);
250         res ~= "auto %1$s(string s) { return StyledString(s).setForeground(%2$s.%1$s); }\n".format(c,
251                 t);
252         res ~= "auto %1$s(StyledString s) { return s.setForeground(%2$s.%1$s); }\n".format(c, t);
253         string name = c[0 .. 1].toUpper ~ c[1 .. $];
254         res ~= "auto on%3$s(string s) { return StyledString(s).setBackground(%2$s.%1$s); }\n".format(c,
255                 t, name);
256         res ~= "auto on%3$s(StyledString s) { return s.setBackground(%2$s.%1$s); }\n".format(c,
257                 t, name);
258     }
259     return res;
260 }
261 
262 /// Create `style` functions for all enum mebers, e.g. "abc".bold
263 auto styleMixin(T)()
264 {
265     string res = "";
266     foreach (immutable style; [EnumMembers!T])
267     {
268         auto t = typeof(T.init).stringof;
269         auto s = "%s".format(style);
270         res ~= "auto %1$s(string s) { return StyledString(s).addStyle(%2$s.%1$s); }\n".format(s, t);
271         res ~= "auto %1$s(StyledString s) { return s.addStyle(%2$s.%1$s); }\n".format(s, t);
272     }
273     return res;
274 }
275 
276 mixin(colorMixin!AnsiColor);
277 mixin(styleMixin!Style);
278 
279 @system @("api") unittest
280 {
281     "redOnGreen".red.onGreen.writeln;
282     "redOnYellowBoldUnderlined".red.onYellow.bold.underlined.writeln;
283     "bold".bold.writeln;
284     "test".writeln;
285 }
286 
287 /// Calculate length of string excluding all formatting escapes
288 ulong unformattedLength(string s)
289 {
290     enum State
291     {
292         NORMAL,
293         ESCAPED,
294     }
295 
296     auto state = State.NORMAL;
297     ulong count = 0;
298     foreach (c; s)
299     {
300         switch (state)
301         {
302         case State.NORMAL:
303             if (c == 0x1b)
304             {
305                 state = State.ESCAPED;
306             }
307             else
308             {
309                 count++;
310             }
311             break;
312         case State.ESCAPED:
313             if (c == 'm')
314             {
315                 state = State.NORMAL;
316             }
317             break;
318         default:
319             throw new Exception("Illegal state");
320         }
321     }
322     return count;
323 }
324 
325 /++ Range to work with ansi escapes. The ESC[ parts and m must be
326  + already removed and the numbers need to be converted to uints.
327  + See https://en.wikipedia.org/wiki/ANSI_escape_code
328  +/
329 auto tokenize(Range)(Range parts)
330 {
331     struct TokenizeResult(Range)
332     {
333         Range parts;
334         ElementType!(Range)[] next;
335         this(Range parts)
336         {
337             this.parts = parts;
338             tokenizeNext();
339         }
340 
341         private void tokenizeNext()
342         {
343             next = [];
344             if (parts.empty)
345             {
346                 return;
347             }
348             switch (parts.front)
349             {
350             case 38:
351             case 48:
352                 next ~= 38;
353                 parts.popFront;
354                 switch (parts.front)
355                 {
356                 case 2:
357                     next ~= 2;
358                     parts.popFront;
359                     next ~= parts.front;
360                     parts.popFront;
361                     next ~= parts.front;
362                     parts.popFront;
363                     next ~= parts.front;
364                     parts.popFront;
365                     break;
366                 case 5:
367                     next ~= 5;
368                     parts.popFront;
369                     next ~= parts.front;
370                     parts.popFront;
371                     break;
372                 default:
373                     throw new Exception("Only [38,48];[2,5] are supported but got %s;%s".format(next[0],
374                             parts.front));
375                 }
376                 break;
377             case 0: .. case 37:
378             case 39: .. case 47:
379             case 49:
380             case 51:
381                     .. case 55:
382             case 60: .. case 65:
383             case 90: .. case 97:
384             case 100: .. case 107:
385                 next ~= parts.front;
386                 parts.popFront;
387                 break;
388             default:
389                 throw new Exception("Only colors are supported");
390             }
391         }
392 
393         auto front()
394         {
395             return next;
396         }
397 
398         bool empty()
399         {
400             return next == null;
401         }
402 
403         void popFront()
404         {
405             tokenizeNext();
406         }
407     }
408 
409     return TokenizeResult!(Range)(parts);
410 }
411 
412 @system @("ansi tokenizer") unittest
413 {
414     [38, 5, 2, 38, 2, 1, 2, 3, 36, 1, 2, 3, 4].tokenize.should == ([
415         [38, 5, 2], [38, 2, 1, 2, 3], [36], [1], [2], [3], [4]
416     ]);
417 }
418 
419 /++ Remove classes of ansi escapes from a styled string.
420  +/
421 string filterAnsiEscapes(alias predicate)(string s)
422 {
423     string withFilters(Captures!string c)
424     {
425         auto parts = c[1].split(";").map!(a => a.to!uint)
426             .tokenize
427             .filter!(p => predicate(p));
428         if (parts.empty)
429         {
430             return "";
431         }
432         else
433         {
434             return "\033[" ~ parts.joiner.map!(a => "%d".format(a)).join(";") ~ "m";
435         }
436     }
437 
438     alias r = ctRegex!"\033\\[(.*?)m";
439     return s.replaceAll!(withFilters)(r);
440 }
441 
442 /// Predicate to select foreground color ansi escapes
443 bool foregroundColor(uint[] token)
444 {
445     return token[0] >= 30 && token[0] <= 38;
446 }
447 
448 /// Predicate to select background color ansi escapes
449 bool backgroundColor(uint[] token)
450 {
451     return token[0] >= 40 && token[0] <= 48;
452 }
453 
454 /// Predicate to select style ansi escapes
455 bool style(uint[] token)
456 {
457     return token[0] >= 1 && token[0] <= 29;
458 }
459 
460 /// Predicate select nothing
461 bool none(uint[])
462 {
463     return false;
464 }
465 
466 /// Predicate to select all
467 bool all(uint[])
468 {
469     return true;
470 }
471 
472 @system @("configurable strip") unittest
473 {
474     import unit_threaded;
475     import std.functional : not;
476 
477     "test".red.onGreen.bold.toString.filterAnsiEscapes!(foregroundColor).should == "\033[31mtest";
478     "test".red.onGreen.bold.toString.filterAnsiEscapes!(not!foregroundColor)
479         .should == "\033[42m\033[1mtest\033[0m\033[0m\033[0m";
480     "test".red.onGreen.bold.toString.filterAnsiEscapes!(style).should == "\033[1mtest";
481     "test".red.onGreen.bold.toString.filterAnsiEscapes!(none).should == "test";
482     "test".red.onGreen.bold.toString.filterAnsiEscapes!(all)
483         .should == "\033[31m\033[42m\033[1mtest\033[0m\033[0m\033[0m";
484     "test".red.onGreen.bold.toString.filterAnsiEscapes!(backgroundColor).should == "\033[42mtest";
485 }
486 
487 /// Add fillChar to the right of the string until width is reached
488 auto leftJustifyFormattedString(string s, ulong width, dchar fillChar = ' ')
489 {
490     auto res = s;
491     const currentWidth = s.unformattedLength;
492     for (long i = currentWidth; i < width; ++i)
493     {
494         res ~= fillChar;
495     }
496     return res;
497 }
498 
499 @system @("leftJustifyFormattedString") unittest
500 {
501     import unit_threaded;
502 
503     "test".red.toString.leftJustifyFormattedString(10).should == "\033[31mtest\033[0m      ";
504 }
505 
506 /// Add fillChar to the left of the string until width is reached
507 auto rightJustifyFormattedString(string s, ulong width, char fillChar = ' ')
508 {
509     auto res = s;
510     const currentWidth = s.unformattedLength;
511     for (long i = currentWidth; i < width; ++i)
512     {
513         res = fillChar ~ res;
514     }
515     return res;
516 }
517 
518 @system @("rightJustifyFormattedString") unittest
519 {
520     "test".red.toString.rightJustifyFormattedString(10).should == ("      \033[31mtest\033[0m");
521 }
522 
523 /// Force a style on possible preformatted text
524 auto forceStyle(string text, Style style) {
525     return "\033[%d".format(style.to!int) ~ "m" ~ text.split("\033[0m").join("\033[0;%d".format(style.to!int) ~"m") ~ "\033[0m";
526 }
527 
528 @("forceStyle") unittest
529 {
530     auto splitt = "1es2eses3".split("es").filter!(not!(empty));
531     splitt.should == ["1", "2", "3"];
532     string s = "noformatting%snoformatting".format("red".red).forceStyle(Style.reverse);
533     writeln(s);
534     s.should == "\033[7mnoformatting\033[31mred\033[0;7mnoformatting\033[0m";
535 }