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 }