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 }