1 /// Terminal logging utilities
2 module sily.logger;
3 
4 import std.datetime: DateTime, Clock;
5 import std.array: replace, split, join;
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, stdout, File;
11 import std.math: round;
12 import std.path: dirSeparator;
13 import std.file: exists;
14 
15 import sily.terminal: terminalWidth;
16 import sily.string: splitStringWidth;
17 import sily.path: fixPath;
18 
19 static import sily.conv;
20 
21 struct Log {
22     /// Log level (recommended to be set to LogLevel.warning on production)
23     ubyte logLevel = LogLevel.all;
24     /// Is formatting (colors) enabled
25     bool formattingEnabled = true;
26     /// Is TTY (terminal) allowed to be printed into
27     bool allowTTY = true;
28     /// Is File (logFile) allowed to be printed into
29     bool allowFile = false;
30     /++
31     Always flushes file after logging.
32     might be slow, but will prevent data loss on crashes
33     +/
34     bool alwaysFlush = false;
35 
36     /// Should output in format: File(Line): Type: Message
37     bool simpleOutput = false;
38 
39     private File _logFile;
40 
41     this(bool p_formattingEnabled) {
42         formattingEnabled = p_formattingEnabled;
43     }
44 
45     this(ubyte p_logLevel) {
46         logLevel = p_logLevel;
47     }
48 
49     this(File p_logFile, bool p_allowTTY = true, ubyte p_logLevel = LogLevel.all) {
50         logFile = p_logFile;
51         allowTTY = p_allowTTY;
52         logLevel = p_logLevel;
53     }
54 
55     this(string p_logFile, bool p_allowTTY = true, ubyte p_logLevel = LogLevel.all) {
56         logFile = p_logFile;
57         allowTTY = p_allowTTY;
58         logLevel = p_logLevel;
59     }
60 
61     // ~this() {
62         // For some reason it closes on it's own right
63         // when I create SDL window
64         // _logFile.close();
65     // }
66 
67     /++
68     Sets or resets file for logging, supply unopened file or empty filepath to reset.
69     If string path is supplies Log opens file in "W" mode (overwrites file contents)
70     +/
71     void logFile(File f) @property {
72         if (f.isOpen) {
73             _logFile = f;
74             allowFile = true;
75         } else {
76             if (_logFile.isOpen) _logFile.close();
77             allowFile = false;
78         }
79     }
80 
81     /// Ditto
82     void logFile(string filepath) @property {
83         if (filepath.length) {
84             _logFile = File(filepath.fixPath, "w");
85             import std.datetime: Clock;
86             import std.array: split;
87             import std.system;
88             import core.cpuid;
89             string ver = __VERSION__.to!string;
90             ver = (ver.length > 1 ? ver[0] ~ "." ~ ver[1..$] : ver);
91 
92             _logFile.writeln("Logging started at ", (Clock.currTime.toLocalTime().to!string).split(".")[0], " (Local)");
93             _logFile.writeln("    File            : ", filepath.fixPath);
94             _logFile.writeln("    OS              : ", os, " ", isX86_64 ? "x64" : "x32");
95             _logFile.writeln("    Compiler        : ", __VENDOR__ ~ " version " ~ ver);
96             _logFile.writeln("    Compiled at     : ", __DATE__ ~ ", " ~ __TIME__);
97             _logFile.writeln("-----------------------------------------------");
98             allowFile = true;
99         } else {
100             if (_logFile.isOpen) _logFile.close();
101             allowFile = false;
102         }
103     }
104 
105     /// Returns log file
106     const(File) logFile() @property const {
107         return _logFile;
108     }
109 
110     /// Flushes log file
111     void flush() {
112         if (isCustomFile()) _logFile.flush();
113     }
114 
115     private bool isCustomFile() {
116         return _logFile.isOpen && allowFile;
117     }
118 
119     /**
120     This function logs `args` to stdout
121     In order for the resulting log message to appear
122     LogLevel must be greater or equal then globalLogLevel
123     When using `log!LogLevel.off` or `message` it'll be
124     displayed no matter the level of globalLogLevel
125     Params:
126       args = Data that should be logged
127     Example:
128     ---
129     trace(true, " is true bool");
130     info(true, " is true bool");
131     warning(true, " is true bool");
132     error(true, " is true bool");
133     critical(true, " is true bool");
134     fatal(true, " is true bool");
135     log(true, " is true bool");
136     log!(LogLevel.error)(true, " is true bool");
137     log!(LogLevel.warning)(true, " is true bool");
138     ---
139     */
140     void message(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.off, line, file)(args); }
141     /// Ditto
142     void trace(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.trace, line, file)(args); }
143     /// Ditto
144     void info(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.info, line, file)(args); }
145     /// Ditto
146     void warning(int line = __LINE__, string file = __FILE__, S...)(S args)
147         { log!(LogLevel.warning, line, file)(args); }
148     /// Ditto
149     void error(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.error, line, file)(args); }
150     /// Ditto
151     void critical(int line = __LINE__, string file = __FILE__, S...)(S args)
152         { log!(LogLevel.critical, line, file)(args); }
153     /// Ditto
154     void fatal(int line = __LINE__, string file = __FILE__, S...)(S args) { log!(LogLevel.fatal, line, file)(args); }
155     /// Ditto
156     void log(LogLevel ll = LogLevel.trace, int line = __LINE__, string file = __FILE__, S...)(S args) {
157         if (!logLevel.hasFlag(ll.highestOneBit)) return;
158         string lstring = "";
159 
160         if (simpleOutput) {
161             // File(Line): Type: Message
162             if (ll.hasFlag(LogLevel.traceOnly)) {
163                 lstring = "Note";
164             } else
165             if (ll.hasFlag(LogLevel.infoOnly)) { // set to 92 for green
166                 lstring = "Info";
167             } else
168             if (ll.hasFlag(LogLevel.warningOnly)) {
169                 lstring = "Warning";
170             } else
171             if (ll.hasFlag(LogLevel.errorOnly)) {
172                 lstring = "Error";
173             } else
174             if (ll.hasFlag(LogLevel.criticalOnly)) {
175                 lstring = "Error";
176             } else
177             if (ll.hasFlag(LogLevel.fatalOnly)) {
178                 lstring = "Error";
179             } else {
180                 lstring = "Note";
181             }
182 
183             dstring messages = sily.conv.format!dstring(args);
184 
185             writefln("%s(%d): %s: %s",
186                 file.split(dirSeparator)[$-1],
187                 line,
188                 lstring,
189                 messages
190             );
191 
192             return;
193         }
194 
195         if (formattingEnabled) {
196             if (ll.hasFlag(LogLevel.traceOnly)) {
197                 lstring = "\033[90m%*-s\033[m".format(8, "Trace");
198             } else
199             if (ll.hasFlag(LogLevel.infoOnly)) { // set to 92 for green
200                 lstring = "\033[94m%*-s\033[m".format(8, "Info");
201             } else
202             if (ll.hasFlag(LogLevel.warningOnly)) {
203                 lstring = "\033[33m%*-s\033[m".format(8, "Warning");
204             } else
205             if (ll.hasFlag(LogLevel.errorOnly)) {
206                 lstring = "\033[1;91m%*-s\033[m".format(8, "Error");
207             } else
208             if (ll.hasFlag(LogLevel.criticalOnly)) {
209                 lstring = "\033[1;101;30m%*-s\033[m".format(8, "Critical");
210             } else
211             if (ll.hasFlag(LogLevel.fatalOnly)) {
212                 lstring = "\033[1;101;97m%*-s\033[m".format(8, "Fatal");
213             } else {
214                 lstring = "%*-s".format(8, "Message");
215             }
216         } else {
217             if (ll.hasFlag(LogLevel.traceOnly)) {
218                 lstring = "%*-s".format(8, "Trace");
219             } else
220             if (ll.hasFlag(LogLevel.infoOnly)) { // set to 92 for green
221                 lstring = "%*-s".format(8, "Info");
222             } else
223             if (ll.hasFlag(LogLevel.warningOnly)) {
224                 lstring = "%*-s".format(8, "Warning");
225             } else
226             if (ll.hasFlag(LogLevel.errorOnly)) {
227                 lstring = "%*-s".format(8, "Error");
228             } else
229             if (ll.hasFlag(LogLevel.criticalOnly)) {
230                 lstring = "%*-s".format(8, "Critical");
231             } else
232             if (ll.hasFlag(LogLevel.fatalOnly)) {
233                 lstring = "%*-s".format(8, "Fatal");
234             } else {
235                 lstring = "%*-s".format(8, "Message");
236             }
237         }
238 
239         dstring messages = sily.conv.format!dstring(args);
240 
241         int msgMaxWidth = terminalWidth -
242             ("[00:00:00] Critical  %s:%d".format(file.split(dirSeparator)[$-1], line).length).to!int;
243 
244         dstring[] msg = splitStringWidth(messages, msgMaxWidth);
245 
246         if (allowTTY) {
247             if (formattingEnabled) {
248                 writefln("\033[90m[%s]\033[m %s %*-s \033[m\033[90m%s:%d\033[m",
249                     to!DateTime(Clock.currTime).timeOfDay,
250                     lstring,
251                     msgMaxWidth, msg[0],
252                     file.split(dirSeparator)[$-1], line);
253             } else {
254                 writefln("[%s] %s %*-s %s:%d",
255                     to!DateTime(Clock.currTime).timeOfDay,
256                     lstring,
257                     msgMaxWidth, msg[0],
258                     file.split(dirSeparator)[$-1], line);
259             }
260         }
261         if (isCustomFile()) {
262             if (ll.hasFlag(LogLevel.traceOnly)) {
263                 lstring = "%*-s".format(8, "Trace");
264             } else
265             if (ll.hasFlag(LogLevel.infoOnly)) { // set to 92 for green
266                 lstring = "%*-s".format(8, "Info");
267             } else
268             if (ll.hasFlag(LogLevel.warningOnly)) {
269                 lstring = "%*-s".format(8, "Warning");
270             } else
271             if (ll.hasFlag(LogLevel.errorOnly)) {
272                 lstring = "%*-s".format(8, "Error");
273             } else
274             if (ll.hasFlag(LogLevel.criticalOnly)) {
275                 lstring = "%*-s".format(8, "Critical");
276             } else
277             if (ll.hasFlag(LogLevel.fatalOnly)) {
278                 lstring = "%*-s".format(8, "Fatal");
279             } else {
280                 lstring = "%*-s".format(8, "Message");
281             }
282             string _msg = format("%s:%d [%s] %s %*-s",
283                 file.split(dirSeparator)[$-1], line,
284                 to!DateTime(Clock.currTime).timeOfDay,
285                 lstring,
286                 msgMaxWidth, messages,
287             );
288             _logFile.writeln(_msg);
289             if (alwaysFlush) _logFile.flush();
290         }
291         for (int i = 1; i < msg.length; ++i) {
292             if (allowTTY) {
293                 if (formattingEnabled) {
294                     writefln("%*s%s\033[m", 20, " ", msg[i]);
295                 } else {
296                     writefln("%*s%s", 20, " ", msg[i]);
297                 }
298             }
299         }
300     }
301 
302     /// Creates new line (br)
303     void newline() {
304         if (allowTTY) writefln("");
305         if (isCustomFile()) {
306             _logFile.writeln("");
307             if (alwaysFlush) _logFile.flush();
308         }
309     }
310 
311     /// Writes raw message to log
312     void logRaw(S...)(S args) {
313         dstring messages = sily.conv.format!dstring(args);
314         if (allowTTY) writefln(messages);
315         if (isCustomFile()) {
316             _logFile.writeln(messages);
317             if (alwaysFlush) _logFile.flush();
318         }
319     }
320 }
321 
322 private Log defaultLogger;
323 
324 static this() {
325     defaultLogger = Log();
326 }
327 
328 /// Alias to same method in `private Log defaultLogger`, outputs only into stdout
329 void message(int line = __LINE__, string file = __FILE__, S...)(S args)
330     { defaultLogger.log!(LogLevel.off, line, file)(args); }
331 /// Ditto
332 void trace(int line = __LINE__, string file = __FILE__, S...)(S args)
333     { defaultLogger.log!(LogLevel.trace, line, file)(args); }
334 /// Ditto
335 void info(int line = __LINE__, string file = __FILE__, S...)(S args)
336     { defaultLogger.log!(LogLevel.info, line, file)(args); }
337 /// Ditto
338 void warning(int line = __LINE__, string file = __FILE__, S...)(S args)
339     { defaultLogger.log!(LogLevel.warning, line, file)(args); }
340 /// Ditto
341 void error(int line = __LINE__, string file = __FILE__, S...)(S args)
342     { defaultLogger.log!(LogLevel.error, line, file)(args); }
343 /// Ditto
344 void critical(int line = __LINE__, string file = __FILE__, S...)(S args)
345     { defaultLogger.log!(LogLevel.critical, line, file)(args); }
346 /// Ditto
347 void fatal(int line = __LINE__, string file = __FILE__, S...)(S args)
348     { defaultLogger.log!(LogLevel.fatal, line, file)(args); }
349 /// Ditto
350 void log(LogLevel ll = LogLevel.trace, int line = __LINE__, string file = __FILE__, S...)(S args)
351     { defaultLogger.log!(ll, line, file)(args); }
352 
353 /// Creates new line (br)
354 void newline() { defaultLogger.newline(); }
355 
356 /// Writes raw message to log
357 void logRaw(S...)(S args) { defaultLogger.logRaw(args); }
358 
359 private uint highestOneBit(uint i) {
360     i |= (i >>  1);
361     i |= (i >>  2);
362     i |= (i >>  4);
363     i |= (i >>  8);
364     i |= (i >> 16);
365     return i - (i >>> 1);
366 }
367 
368 private bool hasFlag(uint flags, uint flag) {
369     return (flags & flag) == flag;
370 }
371 
372 private bool hasFlags(uint flags, uint flag) {
373     return (flags & flag) != 0;
374 }
375 
376 /// LogLevel to use with `setGlobalLogLevel` and `log!LogLevel`
377 enum LogLevel: ubyte {
378     off          = 0,
379 
380     fatal        = 0b000001,
381     critical     = 0b000011,
382     error        = 0b000111,
383     warning      = 0b001111,
384     info         = 0b011111,
385     trace        = 0b111111,
386 
387     fatalOnly    = 0b000001,
388     criticalOnly = 0b000010,
389     errorOnly    = 0b000100,
390     warningOnly  = 0b001000,
391     infoOnly     = 0b010000,
392     traceOnly    = 0b100000,
393 
394     all = ubyte.max,
395 }
396 
397 /**
398 Prints horizontal ruler
399 Params:
400   pattern = Symbol to fill line with
401   message = Message in middle of line
402   lineFormat = Formatting string for line (!USE ONLY FOR FORMATTING)
403   msgFormat = Formatting string for message (!USE ONLY FOR FORMATTING)
404 Example:
405 ---
406 hr();
407 // ───────────────────────────────────────────
408 hr('~');
409 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
410 hr('-', "log trace");
411 // --------------- log trace -----------------
412 hr('=', "log", "\033[33m");
413 // prints = in yellow
414 // ================== log ====================
415 hr('#', "ERROR", "\033[91m", "\033[101m");
416 // prints # in red end ERROR in red background
417 // ################# ERROR ###################
418 ---
419 */
420 void hr(dchar pattern = '─', dstring message = "", string lineFormat = "", string msgFormat = "",
421         bool __logFormatEnabled = true) {
422     int tw = terminalWidth();
423     if (message != "") {
424         ulong llen = (tw - message.length - 2) / 2;
425         ulong mmod = message.length % 2 + tw % 2;
426         ulong rlen = llen + (mmod == 2 || mmod == 0 ? 0 : 1);
427         if (__logFormatEnabled) {
428             writef("%s%s%s %s%s%s %s%s%s",
429                 lineFormat, pattern.repeat(llen), "\033[m",
430                 msgFormat, message, "\033[m",
431                 lineFormat, pattern.repeat(rlen), "\033[m");
432         } else {
433             writef("%s %s %s", pattern.repeat(llen), message,pattern.repeat(rlen));
434         }
435     } else {
436         if (__logFormatEnabled) {
437             writef("%s%s%s", lineFormat, pattern.repeat(tw), "\033[m");
438         } else {
439             writef("%s", pattern.repeat(tw));
440         }
441     }
442     writef("\n");
443 }
444 
445 /**
446 Params:
447   title = Title of block
448   message = Message to print in block
449   width = Width of block. Set to -1 for auto
450   _align = Block align. -1 - left, 0 - center, 1 - right
451 */
452 void block(dstring title, dstring message, int width = -1, int _align = -1, bool __logFormatEnabled = true) {
453     ulong maxLen = title.length;
454 
455     if (width == -1) {
456         dstring[] lines = message.split('\n');
457         foreach (line; lines) {
458             if (line.length + 2 > maxLen) maxLen = line.length + 2;
459         }
460     } else {
461         maxLen = width;
462     }
463 
464     int tw = terminalWidth;
465     maxLen = maxLen > tw ? tw - 1 : maxLen;
466 
467     dstring[] titles = title.splitStringWidth(maxLen);
468     dstring[] lines = message.splitStringWidth(maxLen);
469 
470     ulong _alignSize = 0;
471     if (_align == 0) {
472         _alignSize = (tw - maxLen - 1) / 2;
473     } else
474     if (_align == 1) {
475         _alignSize = tw - maxLen - 1;
476     }
477 
478     if (__logFormatEnabled) {
479         foreach (line; titles) writef("%*s\033[1;7m %*-s\033[m\n", _alignSize, "", maxLen, line);
480         foreach (line; lines) writef("%*s\033[3;7;2m %*-s\033[m\n", _alignSize, "", maxLen, line);
481     } else {
482         foreach (line; titles) writef("%*s %*-s\n", _alignSize, "", maxLen, line);
483         foreach (line; lines) writef("%*s %*-s\n", _alignSize, "", maxLen, line);
484     }
485 }
486 
487 /**
488 Prints message centered in terminal
489 Params:
490   message = Message to print
491 */
492 void center(dstring message) {
493     int tw = terminalWidth;
494     dstring[] lines = message.split('\n');
495     foreach (line; lines) if (line.length > 0) {
496         if (line.length <= tw) {
497             writef("%*s%s\n", (tw - line.length) / 2, "", line);
498         } else {
499             dstring[] sublines = line.splitStringWidth(tw);
500             foreach (subline; sublines) if (subline.length > 0) {
501                 writef("%*s%s\n", (tw - subline.length) / 2, "", subline);
502             }
503         }
504     }
505 }
506 
507 /**
508 Prints compiler info in format:
509 Params:
510   _center = Should info be printed in center (default true)
511 */
512 void printCompilerInfo(bool _center = true) {
513     dstring ver = __VERSION__.to!dstring;
514     ver = (ver.length > 1 ? ver[0] ~ "."d ~ ver[1..$] : ver);
515     dstring compilerInfo = "[" ~ __VENDOR__ ~ ": v" ~ ver ~ "] Compiled at: " ~ __DATE__ ~ ", " ~ __TIME__;
516     if (_center) {
517         center(compilerInfo);
518     } else {
519         writefln(compilerInfo);
520     }
521 }
522 
523 /*
524 Returns compiler info in format: `[VENDOR: vVERSION] Compiled at: DATE, TIME`
525 */
526 string getCompilerInfo() {
527     string ver = __VERSION__.to!string;
528     ver = (ver.length > 1 ? ver[0] ~ "." ~ ver[1..$] : ver);
529     return "[" ~ __VENDOR__ ~ ": v" ~ ver ~ "] Compiled at: " ~ __DATE__ ~ ", " ~ __TIME__;
530 }
531 
532 /**
533 Params:
534   b = ProgressBar struct
535   width = Custom width. Set to `-1` for auto
536 */
537 void progress(ProgressBar b, int width = -1, bool __logFormatEnabled = true) {
538     int labelLen = b.label.length.to!int;
539 
540     if (labelLen > 0) {
541         if (__logFormatEnabled) {
542             writef("%s%s\033[m ", b.labelFormat, b.label);
543         } else {
544             writef("%s ", b.label);
545         }
546         labelLen += 1;
547     }
548 
549     if (width < 0) {
550         width = terminalWidth() - labelLen - " 100%".length.to!int;
551     }
552 
553     width -= (b.before != '\0' ? 1 : 0) + (b.after != '\0' ? 1 : 0);
554 
555     float percentComplete = b.percent / 100.0f;
556 
557     int completeLen = cast(int) (width * percentComplete);
558     int incompleteLen = width - completeLen - 1;
559 
560     string _col = b.colors[
561         cast(int) round(percentComplete * (b.colors.length.to!int - 1))
562     ];
563 
564     dstring completeBar = (b.complete == '\0' ? ' ' : b.complete).repeat(completeLen);
565     dstring incompleteBar = (b.incomplete == '\0' ? ' ' : b.incomplete).repeat(incompleteLen);
566     dchar breakChar = (b.break_ == '\0' ? ' ' : b.break_);
567 
568     if (__logFormatEnabled) {
569         writef("%s%s%s\033[m", b.before, _col, completeBar);
570         if (completeLen != width) {
571             writef("%s%s", _col, breakChar);
572         }
573         if (incompleteLen > 0) {
574             writef("\033[m\033[90m%s", incompleteBar);
575         }
576         writef("%s\033[m \033[90m%3d%%\033[m\n", b.after, b.percent);
577     } else {
578         writef("%s%s", b.before, completeBar);
579         if (completeLen != width) {
580             writef("%s", breakChar);
581         }
582         if (incompleteLen > 0) {
583             writef("%s", incompleteBar);
584         }
585         writef("%s %3d%%\n", b.after, b.percent);
586     }
587 }
588 
589 /// Structure containing progress bar info
590 struct ProgressBar {
591     int percent = 0;
592     dstring label = "";
593     string labelFormat = "";
594     dchar incomplete = '\u2501';
595     dchar break_ = '\u2578';
596     dchar complete = '\u2501';
597     dchar before = '\0';
598     dchar after = '\0';
599     string[] colors = ["\033[31m", "\033[91m", "\033[33m", "\033[93m", "\033[32m", "\033[92m"];
600 
601     /**
602     Creates default progress bar with label
603     Params:
604         _label = Bar label
605         _labelFormat = Bar label formatting
606     */
607     this(dstring _label, string _labelFormat = "") {
608         label = _label;
609         labelFormat = _labelFormat;
610     }
611 
612     /// Increases completion percent to `amount`
613     void advance(int amount) {
614         percent += amount;
615         if (percent > 100) percent = 100;
616     }
617 
618     // Sets completion percent to 0
619     void reset() {
620         percent = 0;
621     }
622 
623     /// Decreases completion percent to `amount`
624     void reduce(int amount) {
625         percent -= amount;
626         if (percent < 0) percent = 0;
627     }
628 }
629 
630 private dstring repeat(dchar val, long amount){
631     if (amount < 1) return "";
632     dstring s = "";
633     while (s.length < amount) s ~= val;
634     // writef(" %d, %d ", s[amount - 1], s[amount - 2]);
635     return s[0..amount];
636 }
637 
638 /// NOT READY YET
639 struct RichText {
640     private dstring _text;
641     private dstring _textRaw;
642     private dstring _textOnly;
643 
644     @disable this();
645 
646     this(dstring text_) {
647         set(text_);
648     }
649 
650     ulong length() {
651         return _textOnly.length;
652     }
653 
654     ulong lengthFormatted() {
655         return _text.length;
656     }
657 
658     ulong lengthRaw() {
659         return _textRaw.length;
660     }
661 
662     private void preprocess() {
663         // TODO
664     }
665 
666     void set(dstring text_) {
667         _textRaw = text_;
668         // TODO
669     }
670 
671     dstring text() {
672         return _text;
673     }
674 
675 
676 }