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 }