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 }