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