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