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 @("rgb") unittest 152 { 153 import std.stdio; 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 160 @("styledstring") unittest 161 { 162 import unit_threaded; 163 import std.stdio; 164 import std.traits; 165 166 foreach (immutable color; [EnumMembers!AnsiColor]) 167 { 168 auto colorName = "%s".format(color); 169 writeln(StyledString(colorName).setForeground(color)); 170 } 171 foreach (immutable color; [EnumMembers!AnsiColor]) 172 { 173 auto colorName = "bg%s".format(color); 174 writeln(StyledString(colorName).setBackground(color)); 175 } 176 foreach (immutable style; [EnumMembers!Style]) 177 { 178 auto styleName = "%s".format(style); 179 writeln(StyledString(styleName).addStyle(style)); 180 } 181 } 182 183 auto colorMixin(T)() 184 { 185 import std.traits; 186 187 string res = ""; 188 foreach (immutable color; [EnumMembers!T]) 189 { 190 auto t = typeof(T.init).stringof; 191 auto c = "%s".format(color); 192 res ~= "auto %1$s(string s) { return StyledString(s).setForeground(%2$s.%1$s); }\n".format(c, 193 t); 194 res ~= "auto %1$s(StyledString s) { return s.setForeground(%2$s.%1$s); }\n".format(c, t); 195 string name = c[0 .. 1].toUpper ~ c[1 .. $]; 196 res ~= "auto on%3$s(string s) { return StyledString(s).setBackground(%2$s.%1$s); }\n".format(c, 197 t, name); 198 res ~= "auto on%3$s(StyledString s) { return s.setBackground(%2$s.%1$s); }\n".format(c, 199 t, name); 200 } 201 return res; 202 } 203 204 auto styleMixin(T)() 205 { 206 import std.traits; 207 208 string res = ""; 209 foreach (immutable style; [EnumMembers!T]) 210 { 211 auto t = typeof(T.init).stringof; 212 auto s = "%s".format(style); 213 res ~= "auto %1$s(string s) { return StyledString(s).addStyle(%2$s.%1$s); }\n".format(s, t); 214 res ~= "auto %1$s(StyledString s) { return s.addStyle(%2$s.%1$s); }\n".format(s, t); 215 } 216 return res; 217 } 218 219 mixin(colorMixin!AnsiColor); 220 mixin(styleMixin!Style); 221 222 @("api") unittest 223 { 224 import std.stdio; 225 226 "redOnGreen".red.onGreen.writeln; 227 "redOnYellowBoldUnderlined".red.onYellow.bold.underlined.writeln; 228 "bold".bold.writeln; 229 "test".writeln; 230 } 231 232 /// Calculate length of string excluding all formatting escapes 233 ulong unformattedLength(string s) 234 { 235 enum State 236 { 237 NORMAL, 238 ESCAPED, 239 } 240 241 auto state = State.NORMAL; 242 ulong count = 0; 243 foreach (c; s) 244 { 245 switch (state) 246 { 247 case State.NORMAL: 248 if (c == 0x1b) 249 { 250 state = State.ESCAPED; 251 } 252 else 253 { 254 count++; 255 } 256 break; 257 case State.ESCAPED: 258 if (c == 'm') 259 { 260 state = State.NORMAL; 261 } 262 break; 263 default: 264 throw new Exception("Illegal state"); 265 } 266 } 267 return count; 268 } 269 270 // https://en.wikipedia.org/wiki/ANSI_escape_code 271 auto tokenize(Range)(Range parts) 272 { 273 import std.range; 274 275 struct TokenizeResult(Range) 276 { 277 Range parts; 278 ElementType!(Range)[] next; 279 this(Range parts) 280 { 281 this.parts = parts; 282 tokenizeNext(); 283 } 284 285 private void tokenizeNext() 286 { 287 next = []; 288 if (parts.empty) 289 { 290 return; 291 } 292 switch (parts.front) 293 { 294 case 38: 295 case 48: 296 next ~= 38; 297 parts.popFront; 298 switch (parts.front) 299 { 300 case 2: 301 next ~= 2; 302 parts.popFront; 303 next ~= parts.front; 304 parts.popFront; 305 next ~= parts.front; 306 parts.popFront; 307 next ~= parts.front; 308 parts.popFront; 309 break; 310 case 5: 311 next ~= 5; 312 parts.popFront; 313 next ~= parts.front; 314 parts.popFront; 315 break; 316 default: 317 throw new Exception("Only [38,48];[2,5] are supported but got %s;%s".format(next[0], 318 parts.front)); 319 } 320 break; 321 case 0: .. case 37: 322 case 39: .. case 47: 323 case 49: 324 case 51: .. case 55: 325 case 60: .. case 65: 326 case 90: .. case 97: 327 case 100: .. case 107: 328 next ~= parts.front; 329 parts.popFront; 330 break; 331 default: 332 throw new Exception("Only colors are supported"); 333 } 334 } 335 336 auto front() 337 { 338 return next; 339 } 340 341 bool empty() 342 { 343 return next == null; 344 } 345 346 void popFront() 347 { 348 tokenizeNext(); 349 } 350 } 351 352 return TokenizeResult!(Range)(parts); 353 } 354 355 @("ansi tokenizer") unittest 356 { 357 import unit_threaded; 358 359 [38, 5, 2, 38, 2, 1, 2, 3, 36, 1, 2, 3, 4].tokenize.shouldEqual([[38, 5, 360 2], [38, 2, 1, 2, 3], [36], [1], [2], [3], [4]]); 361 } 362 363 string filterAnsiEscapes(alias predicate)(string s) 364 { 365 import std.regex; 366 367 string withFilters(Captures!string c) 368 { 369 import std..string; 370 import std.algorithm; 371 import std.conv; 372 import std.array; 373 374 auto parts = c[1].split(";").map!(a => a.to!uint).tokenize.filter!(p => predicate(p)); 375 if (parts.empty) 376 { 377 return ""; 378 } 379 else 380 { 381 return "\033[" ~ parts.joiner.map!(a => "%d".format(a)).join(";") ~ "m"; 382 } 383 } 384 385 alias r = ctRegex!"\033\\[(.*?)m"; 386 return s.replaceAll!(withFilters)(r); 387 } 388 389 bool foregroundColor(uint[] token) 390 { 391 return token[0] >= 30 && token[0] <= 38; 392 } 393 394 bool backgroundColor(uint[] token) 395 { 396 return token[0] >= 40 && token[0] <= 48; 397 } 398 399 bool style(uint[] token) 400 { 401 return token[0] >= 1 && token[0] <= 29; 402 } 403 404 bool none(uint[] token) 405 { 406 return false; 407 } 408 409 bool all(uint[] token) 410 { 411 return true; 412 } 413 414 @("configurable strip") unittest 415 { 416 import unit_threaded; 417 import std.functional; 418 419 "\033[1;31mtest".filterAnsiEscapes!(foregroundColor).shouldEqual("\033[31mtest"); 420 "\033[1;31mtest".filterAnsiEscapes!(not!foregroundColor).shouldEqual("\033[1mtest"); 421 "\033[1;31mtest".filterAnsiEscapes!(style).shouldEqual("\033[1mtest"); 422 "\033[1;31mtest".filterAnsiEscapes!(none).shouldEqual("test"); 423 } 424 425 auto leftJustifyFormattedString(string s, ulong width, dchar fillChar = ' ') 426 { 427 auto res = s; 428 auto currentWidth = s.unformattedLength; 429 for (auto i = currentWidth; i < width; ++i) 430 { 431 res ~= fillChar; 432 } 433 return res; 434 }