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