1 /** 2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 Terminal colors. initColors must be called for the colors to be activated. 7 Colors are automatically toggled off if the output is not an interactive tty. 8 This can be bypassed by calling initColors with `true`. 9 */ 10 module my.term_color; 11 12 import std.stdio : writefln, stderr, stdout; 13 import logger = std.experimental.logger; 14 15 @("shall print colors, backgrounds and modes") 16 unittest { 17 import std.stdio; 18 import std.traits; 19 import std.conv; 20 import std.string; 21 22 initColors(true); 23 24 foreach (c; EnumMembers!Color) { 25 write(c.to!string.color(c).toString.rightJustify(30)); 26 foreach (m; EnumMembers!Mode) { 27 writef(" %s", m.to!string.color(c).mode(m)); 28 } 29 writeln; 30 } 31 foreach (c; EnumMembers!Background) { 32 write(c.to!string.color.bg(c).toString.rightJustify(30)); 33 foreach (m; EnumMembers!Mode) { 34 writef(" %s", m.to!string.color.bg(c).mode(m)); 35 } 36 writeln; 37 } 38 } 39 40 private template BaseColor(int n) { 41 enum BaseColor : int { 42 none = 39 + n, 43 44 black = 30 + n, 45 red = 31 + n, 46 green = 32 + n, 47 yellow = 33 + n, 48 blue = 34 + n, 49 magenta = 35 + n, 50 cyan = 36 + n, 51 white = 37 + n, 52 53 lightBlack = 90 + n, 54 lightRed = 91 + n, 55 lightGreen = 92 + n, 56 lightYellow = 93 + n, 57 lightBlue = 94 + n, 58 lightMagenta = 95 + n, 59 lightCyan = 96 + n, 60 lightWhite = 97 + n, 61 } 62 } 63 64 alias Color = BaseColor!0; 65 alias Background = BaseColor!10; 66 67 enum Mode { 68 none = 0, 69 bold = 1, 70 underline = 4, 71 blink = 5, 72 swap = 7, 73 hide = 8, 74 } 75 76 struct ColorImpl { 77 import std.format : FormatSpec; 78 79 private { 80 string text; 81 Color fg_; 82 Background bg_; 83 Mode mode_; 84 } 85 86 this(string txt) @safe pure nothrow @nogc { 87 text = txt; 88 } 89 90 this(string txt, Color c) @safe pure nothrow @nogc { 91 text = txt; 92 fg_ = c; 93 } 94 95 ColorImpl opDispatch(string fn)() { 96 import std.conv : to; 97 import std.string : toLower; 98 99 static if (fn.length >= 2 && (fn[0 .. 2] == "fg" || fn[0 .. 2] == "bg")) { 100 static if (fn[0 .. 2] == "fg") { 101 fg_ = fn[2 .. $].toLower.to!Color; 102 } else static if (fn[0 .. 2] == "bg") { 103 bg_ = fn[2 .. $].toLower.to!Background; 104 } else { 105 static assert("unable to handle " ~ fn); 106 } 107 } else { 108 mode_ = fn.to!Mode; 109 } 110 return this; 111 } 112 113 ColorImpl fg(Color c_) @safe pure nothrow @nogc { 114 this.fg_ = c_; 115 return this; 116 } 117 118 ColorImpl bg(Background c_) @safe pure nothrow @nogc { 119 this.bg_ = c_; 120 return this; 121 } 122 123 ColorImpl mode(Mode c_) @safe pure nothrow @nogc { 124 this.mode_ = c_; 125 return this; 126 } 127 128 string toString() @safe const { 129 import std.exception : assumeUnique; 130 import std.format : FormatSpec; 131 132 char[] buf; 133 buf.reserve(100); 134 auto fmt = FormatSpec!char("%s"); 135 toString((const(char)[] s) @safe const{ buf ~= s; }, fmt); 136 auto trustedUnique(T)(T t) @trusted { 137 return assumeUnique(t); 138 } 139 140 return trustedUnique(buf); 141 } 142 143 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 144 import std.format : formattedWrite; 145 import std.range.primitives : put; 146 147 if (!_printColors || (fg_ == Color.none && bg_ == Background.none && mode_ == Mode.none)) 148 put(w, text); 149 else 150 formattedWrite(w, "\033[%d;%d;%dm%s\033[0m", mode_, fg_, bg_, text); 151 } 152 } 153 154 auto color(string s, Color c = Color.none) @safe pure nothrow @nogc { 155 return ColorImpl(s, c); 156 } 157 158 @("shall be safe/pure/nothrow/nogc to color a string") 159 @safe pure nothrow @nogc unittest { 160 auto s = "foo".color(Color.red).bg(Background.black).mode(Mode.bold); 161 } 162 163 @("shall be safe to color a string") 164 @safe unittest { 165 initColors(true); 166 auto s = "foo".color(Color.red).bg(Background.black).mode(Mode.bold).toString; 167 } 168 169 @("shall use opDispatch for config") 170 @safe unittest { 171 import std.stdio; 172 173 initColors(true); 174 writeln("opDispatch".color.fgred); 175 writeln("opDispatch".color.bgGreen); 176 writeln("opDispatch".color.bold); 177 writeln("opDispatch".color.fgred.bggreen.bold); 178 } 179 180 /** It will detect whether or not stdout/stderr are a console/TTY and will 181 * consequently disable colored output if needed. 182 * 183 * Forgetting to call the function will result in ASCII escape sequences in the 184 * piped output, probably an undesiderable thing. 185 */ 186 void initColors(bool forceOn = false) @trusted { 187 if (forceOn) { 188 _printColors = true; 189 return; 190 } 191 192 if (_isColorsInitialized) 193 return; 194 scope (exit) 195 _isColorsInitialized = true; 196 197 version (Windows) { 198 _printColors = false; 199 } else { 200 import my.tty; 201 202 _printColors = isStdoutInteractive && isStderrInteractive; 203 } 204 } 205 206 private: 207 208 /** Whether to print text with colors or not 209 * 210 * Defaults to true but will be set to false in initColors() if stdout or 211 * stderr are not a TTY (which means the output is probably being piped and we 212 * don't want ASCII escape chars in it) 213 */ 214 private shared bool _printColors = false; 215 private shared bool _isColorsInitialized = false;