1 /** 2 Copyright: Copyright (c) 2018, 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 Handles console logging in pretty colors. 7 8 The module disables colors when stdout and stderr isn't a TTY that support 9 colors. This is to avoid ASCII escape sequences in piped output. 10 */ 11 module colorlog; 12 13 import std.stdio : writefln, stderr, stdout; 14 import logger = std.experimental.logger; 15 import std.experimental.logger : LogLevel; 16 17 /// The verbosity level of the logging to use. 18 enum VerboseMode { 19 /// Warning+ 20 minimal, 21 /// Warnings+ 22 warning, 23 /// Info+ 24 info, 25 /// Trace+ 26 trace, 27 } 28 29 /** Configure `std.experimental.logger` with a colorlog instance. 30 */ 31 void confLogger(VerboseMode mode) @safe { 32 switch (mode) { 33 case VerboseMode.info: 34 logger.globalLogLevel = logger.LogLevel.info; 35 logger.sharedLog = new SimpleLogger(logger.LogLevel.info); 36 break; 37 case VerboseMode.trace: 38 logger.globalLogLevel = logger.LogLevel.all; 39 logger.sharedLog = new DebugLogger(logger.LogLevel.all); 40 break; 41 case VerboseMode.warning: 42 logger.globalLogLevel = logger.LogLevel.warning; 43 logger.sharedLog = new SimpleLogger(logger.LogLevel.info); 44 break; 45 default: 46 logger.globalLogLevel = logger.LogLevel.info; 47 logger.sharedLog = new SimpleLogger(logger.LogLevel.info); 48 } 49 } 50 51 @("shall be @safe to configure the logger") 52 @safe unittest { 53 auto old_level = logger.globalLogLevel; 54 auto old_log = logger.sharedLog; 55 scope (exit) { 56 logger.globalLogLevel = old_level; 57 logger.sharedLog = old_log; 58 } 59 60 confLogger(VerboseMode.info); 61 } 62 63 private template BaseColor(int n) { 64 enum BaseColor : int { 65 none = 39 + n, 66 67 black = 30 + n, 68 red = 31 + n, 69 green = 32 + n, 70 yellow = 33 + n, 71 blue = 34 + n, 72 magenta = 35 + n, 73 cyan = 36 + n, 74 white = 37 + n, 75 76 lightBlack = 90 + n, 77 lightRed = 91 + n, 78 lightGreen = 92 + n, 79 lightYellow = 93 + n, 80 lightBlue = 94 + n, 81 lightMagenta = 95 + n, 82 lightCyan = 96 + n, 83 lightWhite = 97 + n, 84 } 85 } 86 87 alias Color = BaseColor!0; 88 alias Background = BaseColor!10; 89 90 enum Mode { 91 none = 0, 92 bold = 1, 93 underline = 4, 94 blink = 5, 95 swap = 7, 96 hide = 8, 97 } 98 99 struct ColorImpl { 100 import std.format : FormatSpec; 101 102 private { 103 string text; 104 Color fg_; 105 Background bg_; 106 Mode mode_; 107 } 108 109 this(string txt) @safe pure nothrow @nogc { 110 text = txt; 111 } 112 113 this(string txt, Color c) @safe pure nothrow @nogc { 114 text = txt; 115 fg_ = c; 116 } 117 118 auto fg(Color c_) @safe pure nothrow @nogc { 119 this.fg_ = c_; 120 return this; 121 } 122 123 auto bg(Background c_) @safe pure nothrow @nogc { 124 this.bg_ = c_; 125 return this; 126 } 127 128 auto mode(Mode c_) @safe pure nothrow @nogc { 129 this.mode_ = c_; 130 return this; 131 } 132 133 string toString() @safe const { 134 import std.exception : assumeUnique; 135 import std.format : FormatSpec; 136 137 char[] buf; 138 buf.reserve(100); 139 auto fmt = FormatSpec!char("%s"); 140 toString((const(char)[] s) @safe const{ buf ~= s; }, fmt); 141 auto trustedUnique(T)(T t) @trusted { 142 return assumeUnique(t); 143 } 144 145 return trustedUnique(buf); 146 } 147 148 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 149 import std.format : formattedWrite; 150 import std.range.primitives : put; 151 152 if (!_printColors || (fg_ == Color.none && bg_ == Background.none && mode_ == Mode.none)) 153 put(w, text); 154 else 155 formattedWrite(w, "\033[%d;%d;%dm%s\033[0m", mode_, fg_, bg_, text); 156 } 157 } 158 159 auto color(string s, Color c = Color.none) @safe pure nothrow @nogc { 160 return ColorImpl(s, c); 161 } 162 163 @("shall be safe/pure/nothrow/nogc to color a string") 164 @safe pure nothrow @nogc unittest { 165 auto s = "foo".color(Color.red).bg(Background.black).mode(Mode.bold); 166 } 167 168 @("shall be safe to color a string") 169 @safe unittest { 170 auto s = "foo".color(Color.red).bg(Background.black).mode(Mode.bold).toString; 171 } 172 173 /** Whether to print text with colors or not 174 * 175 * Defaults to true but will be set to false in initColors() if stdout or 176 * stderr are not a TTY (which means the output is probably being piped and we 177 * don't want ASCII escape chars in it) 178 */ 179 private shared bool _printColors = true; 180 private shared bool _isColorsInitialized = false; 181 182 // The width of the prefix. 183 private immutable _prefixWidth = 8; 184 185 /** It will detect whether or not stdout/stderr are a console/TTY and will 186 * consequently disable colored output if needed. 187 * 188 * Forgetting to call the function will result in ASCII escape sequences in the 189 * piped output, probably an undesiderable thing. 190 */ 191 void initColors() @trusted { 192 if (_isColorsInitialized) 193 return; 194 scope (exit) 195 _isColorsInitialized = true; 196 197 // Initially enable colors, we'll disable them during this functions if we 198 // find any reason to 199 _printColors = true; 200 201 version (Windows) { 202 _printColors = false; 203 } else { 204 import core.stdc.stdio; 205 import core.sys.posix.unistd; 206 207 if (!isatty(STDERR_FILENO) || !isatty(STDOUT_FILENO)) 208 _printColors = false; 209 } 210 } 211 212 class SimpleLogger : logger.Logger { 213 this(const LogLevel lvl = LogLevel.warning) @safe { 214 super(lvl); 215 initColors; 216 } 217 218 override void writeLogMsg(ref LogEntry payload) @trusted { 219 auto out_ = stderr; 220 auto use_color = Color.red; 221 auto use_mode = Mode.bold; 222 const use_bg = Background.black; 223 224 switch (payload.logLevel) { 225 case LogLevel.trace: 226 out_ = stdout; 227 use_color = Color.white; 228 use_mode = Mode.init; 229 break; 230 case LogLevel.info: 231 out_ = stdout; 232 use_color = Color.white; 233 break; 234 default: 235 } 236 237 import std.conv : to; 238 239 out_.writefln("%s: %s", payload.logLevel.to!string.color(use_color) 240 .bg(use_bg).mode(use_mode), payload.msg); 241 } 242 } 243 244 class DebugLogger : logger.Logger { 245 this(const logger.LogLevel lvl = LogLevel.trace) @safe { 246 super(lvl); 247 initColors; 248 } 249 250 override void writeLogMsg(ref LogEntry payload) @trusted { 251 auto out_ = stderr; 252 auto use_color = Color.red; 253 auto use_mode = Mode.bold; 254 const use_bg = Background.black; 255 256 switch (payload.logLevel) { 257 case LogLevel.trace: 258 out_ = stdout; 259 use_color = Color.white; 260 use_mode = Mode.init; 261 break; 262 case LogLevel.info: 263 out_ = stdout; 264 use_color = Color.white; 265 break; 266 default: 267 } 268 269 import std.conv : to; 270 271 out_.writefln("%s: %s [%s:%d]", payload.logLevel.to!string.color(use_color) 272 .bg(use_bg).mode(use_mode), payload.msg, payload.funcName, payload.line); 273 } 274 }