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 }