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