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 }