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