1 /// Terminal logging utilities
2 module sily.logger;
3 
4 import std.datetime: DateTime, Clock;
5 import std.array: replace, split;
6 import std.string: capitalize;
7 import std.conv: to;
8 import std.format: format;
9 import std.traits: Unqual;
10 import std.stdio: writefln, writef;
11 import std.math: round;
12 
13 import sily.terminal: terminalWidth;
14 import sily.string: splitStringWidth;
15 static import sily.conv;
16 
17 private ubyte __loggingLevel = LogLevel.all;
18 private bool __logFormatEnabled = true;
19 
20 // TODO: LogConfig?
21 // LogConfig logConfig;
22 // private struct LogConfig {
23     // ubyte logLevel = LogLevel.all;
24     // bool formattingEnabled = true;
25     // File logFile = stdout;
26 // }
27 
28 /** 
29 Sets global log level
30 Params:
31   l = LogLevel
32 Example:
33 ---
34 globalLogLevel(LogLevel.error);
35 globalLogLevel = LogLevel.all;
36 globalLogLevel = LogLevel.
37 ---
38 */
39 void globalLogLevel(ubyte l) {
40     __loggingLevel = l;
41 }
42 
43 /// Returns current global log level
44 ubyte globalLogLevel() {
45     return __loggingLevel;
46 }
47 
48 /** 
49 Enables/Disables formatting (colors, bold, dim...) in log messages
50 Params:
51   state = bool
52 */
53 void globalLogFormattingEnabled(bool state) {
54     __logFormatEnabled = state;
55 }
56 
57 /// Returns: is log formatting enabled
58 bool globalLogFormattingEnabled() {
59     return __logFormatEnabled;
60 }
61 
62 /** 
63 This function logs `args` to stdout
64 In order for the resulting log message to appear
65 LogLevel must be greater or equal then globalLogLevel
66 When using `log!LogLevel.off` or `message` it'll be
67 displayed no matter the level of globalLogLevel
68 Params:
69   args = Data that should be logged
70 Example:
71 ---
72 trace(true, " is true bool");
73 info(true, " is true bool");
74 warning(true, " is true bool");
75 error(true, " is true bool");
76 critical(true, " is true bool");
77 fatal(true, " is true bool");
78 log(true, " is true bool");
79 log!(LogLevel.error)(true, " is true bool");
80 log!(LogLevel.warning)(true, " is true bool");
81 ---
82 */
83 void message(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.off, line, file)(args); }
84 /// Ditto
85 void trace(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.trace, line, file)(args); }
86 /// Ditto
87 void info(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.info, line, file)(args); }
88 /// Ditto
89 void warning(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.warning, line, file)(args); }
90 /// Ditto
91 void error(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.error, line, file)(args); }
92 /// Ditto
93 void critical(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.critical, line, file)(args); }
94 /// Ditto
95 void fatal(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.fatal, line, file)(args); }
96 /// Ditto
97 void log(LogLevel ll = LogLevel.trace, int line = __LINE__, string file = __FILE__, S...)(S args) {
98     if (!__loggingLevel.hasFlag(ll.highestOneBit)) return;
99     string lstring = "";
100     if (__logFormatEnabled) {
101         if (ll.hasFlag(LogLevel.traceOnly)) {
102             lstring = "\033[90m%*-s\033[m".format(8, "Trace"); 
103         } else
104         if (ll.hasFlag(LogLevel.infoOnly)) { // set to 92 for green
105             lstring = "\033[94m%*-s\033[m".format(8, "Info"); 
106         } else
107         if (ll.hasFlag(LogLevel.warningOnly)) {
108             lstring = "\033[33m%*-s\033[m".format(8, "Warning"); 
109         } else
110         if (ll.hasFlag(LogLevel.errorOnly)) {
111             lstring = "\033[1;91m%*-s\033[m".format(8, "Error"); 
112         } else
113         if (ll.hasFlag(LogLevel.criticalOnly)) {
114             lstring = "\033[1;101;30m%*-s\033[m".format(8, "Critical"); 
115         } else
116         if (ll.hasFlag(LogLevel.fatalOnly)) {
117             lstring = "\033[1;101;97m%*-s\033[m".format(8, "Fatal"); 
118         } else {
119             lstring = "%*-s".format(8, "Message"); 
120         }
121     } else {
122         if (ll.hasFlag(LogLevel.traceOnly)) {
123             lstring = "%*-s".format(8, "Trace"); 
124         } else
125         if (ll.hasFlag(LogLevel.infoOnly)) { // set to 92 for green
126             lstring = "%*-s".format(8, "Info"); 
127         } else
128         if (ll.hasFlag(LogLevel.warningOnly)) {
129             lstring = "%*-s".format(8, "Warning"); 
130         } else
131         if (ll.hasFlag(LogLevel.errorOnly)) {
132             lstring = "%*-s".format(8, "Error"); 
133         } else
134         if (ll.hasFlag(LogLevel.criticalOnly)) {
135             lstring = "%*-s".format(8, "Critical"); 
136         } else
137         if (ll.hasFlag(LogLevel.fatalOnly)) {
138             lstring = "%*-s".format(8, "Fatal"); 
139         } else {
140             lstring = "%*-s".format(8, "Message"); 
141         }
142     }
143 
144     dstring messages = sily.conv.format!dstring(args);
145     
146     int msgMaxWidth = terminalWidth - ("[00:00:00] Critical  %s:%d".format(file, line).length).to!int;
147     
148     dstring[] msg = splitStringWidth(messages, msgMaxWidth);
149 
150     if (__logFormatEnabled) {
151         writefln("\033[90m[%s]\033[m %s %*-s \033[m\033[90m%s:%d\033[m",
152             to!DateTime(Clock.currTime).timeOfDay, 
153             lstring, 
154             msgMaxWidth, msg[0],
155             file, line);
156     } else {
157         writefln("[%s] %s %*-s %s:%d",
158             to!DateTime(Clock.currTime).timeOfDay, 
159             lstring, 
160             msgMaxWidth, msg[0],
161             file, line);
162     }
163     for (int i = 1; i < msg.length; ++i) {
164         if (__logFormatEnabled) {
165             writefln("%*s%s\033[m", 20, " ", msg[i]); 
166         } else {
167             writefln("%*s%s", 20, " ", msg[i]); 
168         }
169     }
170 }
171 
172 private uint highestOneBit(uint i) {
173     i |= (i >>  1);
174     i |= (i >>  2);
175     i |= (i >>  4);
176     i |= (i >>  8);
177     i |= (i >> 16);
178     return i - (i >>> 1);
179 }
180 
181 private bool hasFlag(uint flags, uint flag) {
182     return (flags & flag) == flag;
183 }
184 
185 private bool hasFlags(uint flags, uint flag) {
186     return (flags & flag) != 0;
187 }
188 
189 /// LogLevel to use with `setGlobalLogLevel` and `log!LogLevel`
190 enum LogLevel: ubyte {
191     off          = 0,
192 
193     fatal        = 0b000001,
194     critical     = 0b000011,
195     error        = 0b000111,
196     warning      = 0b001111,
197     info         = 0b011111,
198     trace        = 0b111111,
199 
200     fatalOnly    = 0b000001,
201     criticalOnly = 0b000010,
202     errorOnly    = 0b000100,
203     warningOnly  = 0b001000,
204     infoOnly     = 0b010000,
205     traceOnly    = 0b100000,
206 
207     all = ubyte.max,
208 }
209 
210 /** 
211 Prints horizontal ruler
212 Params:
213   pattern = Symbol to fill line with
214   message = Message in middle of line
215   lineFormat = Formatting string for line (!USE ONLY FOR FORMATTING)
216   msgFormat = Formatting string for message (!USE ONLY FOR FORMATTING)
217 Example:
218 ---
219 hr();
220 // ───────────────────────────────────────────
221 hr('~');
222 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
223 hr('-', "log trace");
224 // --------------- log trace -----------------
225 hr('=', "log", "\033[33m");
226 // prints = in yellow
227 // ================== log ====================
228 hr('#', "ERROR", "\033[91m", "\033[101m");
229 // prints # in red end ERROR in red background
230 // ################# ERROR ###################
231 ---
232 */
233 void hr(dchar pattern = '─', dstring message = "", string lineFormat = "", string msgFormat = "") {
234     int tw = terminalWidth();
235     if (message != "") {
236         ulong llen = (tw - message.length - 2) / 2;
237         ulong mmod = message.length % 2 + tw % 2;
238         ulong rlen = llen + (mmod == 2 || mmod == 0 ? 0 : 1);
239         if (__logFormatEnabled) {
240             writef("%s%s%s %s%s%s %s%s%s", 
241                 lineFormat, pattern.repeat(llen), "\033[m", 
242                 msgFormat, message, "\033[m",
243                 lineFormat, pattern.repeat(rlen), "\033[m");
244         } else {
245             writef("%s %s %s", pattern.repeat(llen), message,pattern.repeat(rlen));
246         }
247     } else {
248         if (__logFormatEnabled) {
249             writef("%s%s%s", lineFormat, pattern.repeat(tw), "\033[m");
250         } else {
251             writef("%s", pattern.repeat(tw));
252         }
253     }
254     writef("\n");
255 }
256 
257 /** 
258 Params:
259   title = Title of block
260   message = Message to print in block
261   width = Width of block. Set to -1 for auto
262   _align = Block align. -1 - left, 0 - center, 1 - right 
263 */
264 void block(dstring title, dstring message, int width = -1, int _align = -1) {
265     ulong maxLen = title.length;
266 
267     if (width == -1) {
268         dstring[] lines = message.split('\n');
269         foreach (line; lines) {
270             if (line.length + 2 > maxLen) maxLen = line.length + 2;
271         }
272     } else {
273         maxLen = width;
274     }
275 
276     int tw = terminalWidth;
277     maxLen = maxLen > tw ? tw - 1 : maxLen;
278 
279     dstring[] titles = title.splitStringWidth(maxLen);
280     dstring[] lines = message.splitStringWidth(maxLen);
281 
282     ulong _alignSize = 0;
283     if (_align == 0) {
284         _alignSize = (tw - maxLen - 1) / 2;
285     } else
286     if (_align == 1) {
287         _alignSize = tw - maxLen - 1;
288     }
289 
290     if (__logFormatEnabled) {
291         foreach (line; titles) writef("%*s\033[1;7m %*-s\033[m\n", _alignSize, "", maxLen, line);
292         foreach (line; lines) writef("%*s\033[3;7;2m %*-s\033[m\n", _alignSize, "", maxLen, line);
293     } else {
294         foreach (line; titles) writef("%*s %*-s\n", _alignSize, "", maxLen, line);
295         foreach (line; lines) writef("%*s %*-s\n", _alignSize, "", maxLen, line);
296     }
297 }
298 
299 /** 
300 Prints message centered in terminal
301 Params:
302   message = Message to print
303 */
304 void center(dstring message) {
305     int tw = terminalWidth;
306     dstring[] lines = message.split('\n');
307     foreach (line; lines) if (line.length > 0) { 
308         if (line.length <= tw) {
309             writef("%*s%s\n", (tw - line.length) / 2, "", line);
310         } else {
311             dstring[] sublines = line.splitStringWidth(tw);
312             foreach (subline; sublines) if (subline.length > 0) {
313                 writef("%*s%s\n", (tw - subline.length) / 2, "", subline);
314             }
315         }
316     }
317 }
318 
319 /** 
320 Prints compiler info in format:
321 Params:
322   _center = Should info be printed in center (default true)
323 */
324 void printCompilerInfo(bool _center = true) {
325     dstring ver = __VERSION__.to!dstring;
326     ver = (ver.length > 1 ? ver[0] ~ "."d ~ ver[1..$] : ver);
327     dstring compilerInfo = "[" ~ __VENDOR__ ~ ": v" ~ ver ~ "] Compiled at: " ~ __DATE__ ~ ", " ~ __TIME__;
328     if (_center) {
329         center(compilerInfo);
330     } else {
331         writefln(compilerInfo);
332     }
333 }
334 
335 /** 
336 Params:
337   b = ProgressBar struct
338   width = Custom width. Set to `-1` for auto
339 */
340 void progress(ProgressBar b, int width = -1) {
341     int labelLen = b.label.length.to!int;
342 
343     if (labelLen > 0) {
344         if (__logFormatEnabled) {
345             writef("%s%s\033[m ", b.labelFormat, b.label);
346         } else {
347             writef("%s ", b.label);
348         }
349         labelLen += 1;
350     }
351 
352     if (width < 0) {
353         width = terminalWidth() - labelLen - " 100%".length.to!int;
354     }
355 
356     width -= (b.before != '\0' ? 1 : 0) + (b.after != '\0' ? 1 : 0);
357 
358     float percentComplete = b.percent / 100.0f;
359 
360     int completeLen = cast(int) (width * percentComplete);
361     int incompleteLen = width - completeLen - 1;
362 
363     string _col = b.colors[
364         cast(int) round(percentComplete * (b.colors.length.to!int - 1))
365     ];
366 
367     dstring completeBar = (b.complete == '\0' ? ' ' : b.complete).repeat(completeLen);
368     dstring incompleteBar = (b.incomplete == '\0' ? ' ' : b.incomplete).repeat(incompleteLen);
369     dchar breakChar = (b.break_ == '\0' ? ' ' : b.break_);
370 
371     if (__logFormatEnabled) {
372         writef("%s%s%s\033[m", b.before, _col, completeBar);
373         if (completeLen != width) {
374             writef("%s%s", _col, breakChar);
375         }
376         if (incompleteLen > 0) {
377             writef("\033[m\033[90m%s", incompleteBar);
378         }
379         writef("%s\033[m \033[90m%3d%%\033[m\n", b.after, b.percent);
380     } else {
381         writef("%s%s", b.before, completeBar);
382         if (completeLen != width) {
383             writef("%s", breakChar);
384         }
385         if (incompleteLen > 0) {
386             writef("%s", incompleteBar);
387         }
388         writef("%s %3d%%\n", b.after, b.percent);
389     }
390 }
391 
392 /// Structure containing progress bar info
393 struct ProgressBar {
394     int percent = 0;
395     dstring label = "";
396     string labelFormat = "";
397     dchar incomplete = '\u2501';
398     dchar break_ = '\u2578';
399     dchar complete = '\u2501';
400     dchar before = '\0';
401     dchar after = '\0';
402     string[] colors = ["\033[31m", "\033[91m", "\033[33m", "\033[93m", "\033[32m", "\033[92m"];
403     
404     /**
405     Creates default progress bar with label
406     Params:
407         _label = Bar label
408         _labelFormat = Bar label formatting
409     */
410     this(dstring _label, string _labelFormat = "") {
411         label = _label;
412         labelFormat = _labelFormat;
413     }
414 
415     /// Increases completion percent to `amount`
416     void advance(int amount) {
417         percent += amount;
418         if (percent > 100) percent = 100;
419     }
420 
421     // Sets completion percent to 0
422     void reset() {
423         percent = 0;
424     }
425 
426     /// Decreases completion percent to `amount`
427     void reduce(int amount) {
428         percent -= amount;
429         if (percent < 0) percent = 0;
430     }
431 }
432 
433 private dstring repeat(dchar val, long amount){
434     if (amount < 1) return "";
435     dstring s = "";
436     while (s.length < amount) s ~= val;
437     // writef(" %d, %d ", s[amount - 1], s[amount - 2]);
438     return s[0..amount];
439 }
440 
441 /// NOT READY YET
442 struct RichText {
443     private dstring _text;
444     private dstring _textRaw;
445     private dstring _textOnly;
446 
447     @disable this();
448 
449     this(dstring text_) {
450         set(text_);
451     }
452 
453     ulong length() {
454         return _textOnly.length;
455     }
456 
457     ulong lengthFormatted() {
458         return _text.length;
459     }
460 
461     ulong lengthRaw() {
462         return _textRaw.length;
463     }
464 
465     private void preprocess() {
466         // TODO
467     }
468 
469     void set(dstring text_) {
470         _textRaw = text_;
471         // TODO
472     }
473 
474     dstring text() {
475         return _text;
476     }
477 
478 
479 }