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