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 }