1 /++ 2 Functions related to (formatting and) printing structs and classes to the 3 local terminal, listing each member variable and their contents in an 4 easy-to-visually-parse way. 5 6 Example: 7 8 `printObjects(client, bot, settings);` 9 --- 10 /* Output to screen: 11 12 -- IRCClient 13 string nickname "kameloso"(8) 14 string user "kameloso"(8) 15 string ident "NaN"(3) 16 string realName "kameloso IRC bot"(16) 17 18 -- IRCBot 19 string account "kameloso"(8) 20 string[] admins ["zorael"](1) 21 string[] homeChannels ["#flerrp"](1) 22 string[] guestChannels ["#d"](1) 23 24 -- IRCServer 25 string address "irc.libera.chat"(16) 26 ushort port 6667 27 */ 28 --- 29 30 Distance between types, member names and member values are deduced automatically 31 based on how long they are (in terms of characters). If it doesn't line up, 32 its a bug. 33 34 See_Also: 35 [kameloso.terminal.colours] 36 37 Copyright: [JR](https://github.com/zorael) 38 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 39 40 Authors: 41 [JR](https://github.com/zorael) 42 +/ 43 module kameloso.printing; 44 45 private: 46 47 import std.range.primitives : isOutputRange; 48 import std.meta : allSatisfy; 49 import std.traits : isAggregateType; 50 import std.typecons : Flag, No, Yes; 51 52 public: 53 54 55 // Widths 56 /++ 57 Calculates the minimum padding needed to accommodate the strings of all the 58 types and names of the members of the passed struct and/or classes, for 59 formatting into neat columns. 60 61 Params: 62 all = Whether or not to also include [lu.uda.Unserialisable|Unserialisable] members. 63 Things = Variadic list of aggregates to introspect. 64 +/ 65 private template Widths(Flag!"all" all, Things...) 66 { 67 private: 68 import std.algorithm.comparison : max; 69 70 enum minimumTypeWidth = 8; // Current sweet spot, accommodates well for `string[]` 71 enum minimumNameWidth = 24; // Current minimum 22, TwitchSettings' "caseSensitiveTriggers" 72 73 static if (all) 74 { 75 import kameloso.traits : longestUnserialisableMemberNames; 76 77 alias names = longestUnserialisableMemberNames!Things; 78 public enum type = max(minimumTypeWidth, names.type.length); 79 enum initialWidth = names.member.length; 80 } 81 else 82 { 83 import kameloso.traits : longestMemberNames; 84 85 alias names = longestMemberNames!Things; 86 public enum type = max(minimumTypeWidth, names.type.length); 87 enum initialWidth = names.member.length; 88 } 89 90 enum ptrdiff_t compensatedWidth = (type > minimumTypeWidth) ? 91 (initialWidth - type + minimumTypeWidth) : initialWidth; 92 public enum ptrdiff_t name = max(minimumNameWidth, compensatedWidth); 93 } 94 95 /// 96 unittest 97 { 98 import std.algorithm.comparison : max; 99 100 enum minimumTypeWidth = 8; // Current sweet spot, accommodates well for `string[]` 101 enum minimumNameWidth = 24; // Current minimum 22, TwitchSettings' "caseSensitiveTriggers" 102 103 struct S1 104 { 105 string someString; 106 int someInt; 107 string[] aaa; 108 } 109 110 struct S2 111 { 112 string longerString; 113 int i; 114 } 115 116 alias widths = Widths!(No.all, S1, S2); 117 118 static assert(widths.type == max(minimumTypeWidth, "string[]".length)); 119 static assert(widths.name == max(minimumNameWidth, "longerString".length)); 120 } 121 122 123 // printObjects 124 /++ 125 Prints out aggregate objects, with all their printable members with all their 126 printable values. 127 128 This is not only convenient for debugging but also usable to print out 129 current settings and state, where such is kept in structs. 130 131 Example: 132 --- 133 struct Foo 134 { 135 int foo; 136 string bar; 137 float f; 138 double d; 139 } 140 141 Foo foo, bar; 142 printObjects(foo, bar); 143 --- 144 145 Params: 146 all = Whether or not to also display members marked as 147 [lu.uda.Unserialisable|Unserialisable]; usually transitive 148 information that doesn't carry between program runs. 149 Also those annotated [lu.uda.Hidden|Hidden]. 150 things = Variadic list of aggregate objects to enumerate. 151 +/ 152 void printObjects(Flag!"all" all = No.all, Things...)(auto ref Things things) @trusted // for stdout.flush() 153 if ((Things.length > 0) && allSatisfy!(isAggregateType, Things)) 154 { 155 static import kameloso.common; 156 import kameloso.constants : BufferSize; 157 import std.array : Appender; 158 import std.stdio : stdout, writeln; 159 160 alias widths = Widths!(all, Things); 161 162 static Appender!(char[]) outbuffer; 163 scope(exit) outbuffer.clear(); 164 outbuffer.reserve(BufferSize.printObjectBufferPerObject * Things.length); 165 166 foreach (immutable i, ref thing; things) 167 { 168 bool put; 169 170 version(Colours) 171 { 172 if (!kameloso.common.settings) 173 { 174 // Threading and/or otherwise forgot to assign pointer `kameloso.common.settings` 175 // It will be wrong but initialise it here so we at least don't crash 176 kameloso.common.settings = new typeof(*kameloso.common.settings); 177 } 178 179 if (!kameloso.common.settings.monochrome) 180 { 181 formatObjectImpl!(all, Yes.coloured) 182 (outbuffer, 183 cast(Flag!"brightTerminal")kameloso.common.settings.brightTerminal, 184 thing, 185 widths.type+1, 186 widths.name); 187 put = true; 188 } 189 } 190 191 if (!put) 192 { 193 // Brightness setting is irrelevant; pass false 194 formatObjectImpl!(all, No.coloured) 195 (outbuffer, 196 No.brightTerminal, 197 thing, 198 widths.type+1, 199 widths.name); 200 } 201 202 static if (i+1 < things.length) 203 { 204 // Pad between things 205 outbuffer.put('\n'); 206 } 207 } 208 209 writeln(outbuffer.data); 210 if (kameloso.common.settings.flush) stdout.flush(); 211 } 212 213 214 /// Ditto 215 alias printObject = printObjects; 216 217 218 // formatObjects 219 /++ 220 Formats an aggregate object, with all its printable members with all their 221 printable values. Overload that writes to a passed output range sink. 222 223 Example: 224 --- 225 struct Foo 226 { 227 int foo = 42; 228 string bar = "arr matey"; 229 float f = 3.14f; 230 double d = 9.99; 231 } 232 233 Foo foo, bar; 234 Appender!(char[]) sink; 235 236 sink.formatObjects!(Yes.all, Yes.coloured)(foo); 237 sink.formatObjects!(No.all, No.coloured)(bar); 238 writeln(sink.data); 239 --- 240 241 Params: 242 all = Whether or not to also display members marked as 243 [lu.uda.Unserialisable|Unserialisable]; usually transitive 244 information that doesn't carry between program runs. 245 Also those annotated [lu.uda.Hidden|Hidden]. 246 coloured = Whether to display in colours or not. 247 sink = Output range to write to. 248 bright = Whether or not to format for a bright terminal background. 249 things = Variadic list of aggregate objects to enumerate and format. 250 +/ 251 void formatObjects(Flag!"all" all = No.all, 252 Flag!"coloured" coloured = Yes.coloured, Sink, Things...) 253 (auto ref Sink sink, 254 const Flag!"brightTerminal" bright, 255 auto ref Things things) 256 if ((Things.length > 0) && allSatisfy!(isAggregateType, Things) && isOutputRange!(Sink, char[])) 257 { 258 alias widths = Widths!(all, Things); 259 260 foreach (immutable i, ref thing; things) 261 { 262 formatObjectImpl!(all, coloured) 263 (sink, 264 bright, 265 thing, 266 widths.type+1, 267 widths.name); 268 269 static if ((i+1 < things.length) || !__traits(hasMember, Sink, "data")) 270 { 271 // Not an Appender, make sure it has a final linebreak to be consistent 272 // with Appender writeln 273 sink.put('\n'); 274 } 275 } 276 } 277 278 /// Ditto 279 alias formatObject = formatObjects; 280 281 282 // FormatStringMemberArguments 283 /++ 284 Argument aggregate for invocations of [formatStringMemberImpl]. 285 +/ 286 private struct FormatStringMemberArguments 287 { 288 /// Type name. 289 string typestring; 290 291 /// Member name. 292 string memberstring; 293 294 /// Width (length) of longest type name. 295 uint typewidth; 296 297 /// Width (length) of longest member name. 298 uint namewidth; 299 300 /// Whether or not we should compensate for a bright terminal background. 301 bool bright; 302 303 /// Whether or not to truncate long lines. 304 bool truncate = true; 305 } 306 307 308 // formatStringMemberImpl 309 /++ 310 Formats the description of a string for insertion into a [formatObjects] listing. 311 312 Broken out of [formatObjects] to reduce template bloat. 313 314 Params: 315 coloured = Whether or no to display terminal colours. 316 sink = Output range to store output in. 317 args = Argument aggregate for easier passing. 318 content = The contents of the string member we're describing. 319 +/ 320 private void formatStringMemberImpl(Flag!"coloured" coloured, T, Sink) 321 (auto ref Sink sink, const FormatStringMemberArguments args, const auto ref T content) 322 { 323 import std.format : formattedWrite; 324 325 enum truncateAfter = 128; 326 327 static if (coloured) 328 { 329 import kameloso.terminal.colours.defs : F = TerminalForeground; 330 import kameloso.terminal.colours : asANSI; 331 332 if (args.truncate && (content.length > truncateAfter)) 333 { 334 enum stringPattern = `%s%*s %s%-*s %s"%s"%s ... (%d)` ~ '\n'; 335 immutable memberCode = args.bright ? F.black : F.white; 336 immutable valueCode = args.bright ? F.green : F.lightgreen; 337 immutable lengthCode = args.bright ? F.default_ : F.darkgrey; 338 immutable typeCode = args.bright ? F.lightcyan : F.cyan; 339 340 sink.formattedWrite( 341 stringPattern, 342 typeCode.asANSI, 343 args.typewidth, 344 args.typestring, 345 memberCode.asANSI, 346 args.namewidth, 347 args.memberstring, 348 //(content.length ? string.init : " "), 349 valueCode.asANSI, 350 content[0..truncateAfter], 351 lengthCode.asANSI, 352 content.length); 353 } 354 else 355 { 356 enum stringPattern = `%s%*s %s%-*s %s%s"%s"%s(%d)` ~ '\n'; 357 immutable memberCode = args.bright ? F.black : F.white; 358 immutable valueCode = args.bright ? F.green : F.lightgreen; 359 immutable lengthCode = args.bright ? F.default_ : F.darkgrey; 360 immutable typeCode = args.bright ? F.lightcyan : F.cyan; 361 362 sink.formattedWrite( 363 stringPattern, 364 typeCode.asANSI, 365 args.typewidth, 366 args.typestring, 367 memberCode.asANSI, 368 args.namewidth, 369 args.memberstring, 370 (content.length ? string.init : " "), 371 valueCode.asANSI, 372 content, 373 lengthCode.asANSI, 374 content.length); 375 } 376 } 377 else 378 { 379 if (args.truncate && (content.length > truncateAfter)) 380 { 381 enum stringPattern = `%*s %-*s "%s" ... (%d)` ~ '\n'; 382 sink.formattedWrite( 383 stringPattern, 384 args.typewidth, 385 args.typestring, 386 args.namewidth, 387 args.memberstring, 388 //(content.length ? string.init : " "), 389 content[0..truncateAfter], 390 content.length); 391 } 392 else 393 { 394 enum stringPattern = `%*s %-*s %s"%s"(%d)` ~ '\n'; 395 sink.formattedWrite( 396 stringPattern, 397 args.typewidth, 398 args.typestring, 399 args.namewidth, 400 args.memberstring, 401 (content.length ? string.init : " "), 402 content, 403 content.length); 404 } 405 } 406 } 407 408 409 // FormatArrayMemberArguments 410 /++ 411 Argument aggregate for invocations of [formatArrayMemberImpl]. 412 +/ 413 private struct FormatArrayMemberArguments 414 { 415 /// Type name. 416 string typestring; 417 418 /// Member name. 419 string memberstring; 420 421 /// Element type name. 422 string elemstring; 423 424 /// Whether or not the element is a `char`. 425 bool elemIsCharacter; 426 427 /// Width (length) of longest type name. 428 uint typewidth; 429 430 /// Width (length) of longest member name. 431 uint namewidth; 432 433 /// Whether or not we should compensate for a bright terminal background. 434 bool bright; 435 436 /// Whether or not to truncate big arrays. 437 bool truncate = true; 438 } 439 440 441 // formatArrayMemberImpl 442 /++ 443 Formats the description of an array for insertion into a [formatObjects] listing. 444 445 Broken out of [formatObjects] to reduce template bloat. 446 447 Params: 448 coloured = Whether or no to display terminal colours. 449 sink = Output range to store output in. 450 args = Argument aggregate for easier passing. 451 rawContent = The array we're describing. 452 +/ 453 private void formatArrayMemberImpl(Flag!"coloured" coloured, T, Sink) 454 (auto ref Sink sink, const FormatArrayMemberArguments args, const auto ref T rawContent) 455 { 456 import std.format : formattedWrite; 457 import std.range.primitives : ElementEncodingType; 458 import std.traits : TemplateOf; 459 import std.typecons : Nullable; 460 461 enum truncateAfter = 5; 462 463 static if (__traits(isSame, TemplateOf!(ElementEncodingType!T), Nullable)) 464 { 465 import std.array : replace; 466 import std.conv : to; 467 import std.traits : TemplateArgsOf; 468 469 immutable typestring = "N!" ~ TemplateArgsOf!(ElementEncodingType!T).stringof; 470 immutable endIndex = (args.truncate && (rawContent.length > truncateAfter)) ? 471 truncateAfter : 472 rawContent.length; 473 immutable content = rawContent[0..endIndex] 474 .to!string 475 .replace("Nullable.null", "N.null"); 476 immutable length = rawContent.length; 477 enum alreadyTruncated = true; 478 } 479 else 480 { 481 import lu.traits : UnqualArray; 482 483 immutable typestring = UnqualArray!T.stringof; 484 alias content = rawContent; 485 immutable length = content.length; 486 enum alreadyTruncated = false; 487 } 488 489 static if (coloured) 490 { 491 import kameloso.terminal.colours.defs : F = TerminalForeground; 492 import kameloso.terminal.colours : asANSI; 493 494 immutable memberCode = args.bright ? F.black : F.white; 495 immutable valueCode = args.bright ? F.green : F.lightgreen; 496 immutable lengthCode = args.bright ? F.default_ : F.darkgrey; 497 immutable typeCode = args.bright ? F.lightcyan : F.cyan; 498 499 if (!alreadyTruncated && args.truncate && (content.length > truncateAfter)) 500 { 501 immutable rtArrayPattern = args.elemIsCharacter ? 502 "%s%*s %s%-*s %s[%(%s, %)]%s ... (%d)\n" : 503 "%s%*s %s%-*s %s%s%s ... (%d)\n"; 504 505 sink.formattedWrite( 506 rtArrayPattern, 507 typeCode.asANSI, 508 args.typewidth, 509 typestring, 510 memberCode.asANSI, 511 args.namewidth, 512 args.memberstring, 513 valueCode.asANSI, 514 content[0..truncateAfter], 515 lengthCode.asANSI, 516 length); 517 } 518 else 519 { 520 immutable rtArrayPattern = args.elemIsCharacter ? 521 "%s%*s %s%-*s %s%s[%(%s, %)]%s(%d)\n" : 522 "%s%*s %s%-*s %s%s%s%s(%d)\n"; 523 524 sink.formattedWrite( 525 rtArrayPattern, 526 typeCode.asANSI, 527 args.typewidth, 528 typestring, 529 memberCode.asANSI, 530 args.namewidth, 531 args.memberstring, 532 (content.length ? string.init : " "), 533 valueCode.asANSI, 534 content, 535 lengthCode.asANSI, 536 length); 537 } 538 } 539 else 540 { 541 if (!alreadyTruncated && args.truncate && (content.length > truncateAfter)) 542 { 543 immutable rtArrayPattern = args.elemIsCharacter ? 544 "%*s %-*s [%(%s, %)] ... (%d)\n" : 545 "%*s %-*s %s ... (%d)\n"; 546 547 sink.formattedWrite( 548 rtArrayPattern, 549 args.typewidth, 550 typestring, 551 args.namewidth, 552 args.memberstring, 553 content[0..truncateAfter], 554 length); 555 } 556 else 557 { 558 immutable rtArrayPattern = args.elemIsCharacter ? 559 "%*s %-*s %s[%(%s, %)](%d)\n" : 560 "%*s %-*s %s%s(%d)\n"; 561 562 sink.formattedWrite( 563 rtArrayPattern, 564 args.typewidth, 565 typestring, 566 args.namewidth, 567 args.memberstring, 568 (content.length ? string.init : " "), 569 content, 570 length); 571 } 572 } 573 } 574 575 576 // formatAssociativeArrayMemberImpl 577 /++ 578 Formats the description of an associative array for insertion into a 579 [formatObjects] listing. 580 581 Broken out of [formatObjects] to reduce template bloat. 582 583 Params: 584 coloured = Whether or no to display terminal colours. 585 sink = Output range to store output in. 586 args = Argument aggregate for easier passing. 587 content = The associative array we're describing. 588 +/ 589 private void formatAssociativeArrayMemberImpl(Flag!"coloured" coloured, T, Sink) 590 (auto ref Sink sink, const FormatArrayMemberArguments args, const auto ref T content) 591 { 592 import std.format : formattedWrite; 593 594 enum truncateAfter = 5; 595 596 static if (coloured) 597 { 598 import kameloso.terminal.colours.defs : F = TerminalForeground; 599 import kameloso.terminal.colours : asANSI; 600 601 immutable memberCode = args.bright ? F.black : F.white; 602 immutable valueCode = args.bright ? F.green : F.lightgreen; 603 immutable lengthCode = args.bright ? F.default_ : F.darkgrey; 604 immutable typeCode = args.bright ? F.lightcyan : F.cyan; 605 606 if (args.truncate && (content.length > truncateAfter)) 607 { 608 enum aaPattern = "%s%*s %s%-*s %s%s%s ... (%d)\n"; 609 610 sink.formattedWrite( 611 aaPattern, 612 typeCode.asANSI, 613 args.typewidth, 614 args.typestring, 615 memberCode.asANSI, 616 args.namewidth, 617 args.memberstring, 618 valueCode.asANSI, 619 content.keys[0..truncateAfter], 620 lengthCode.asANSI, 621 content.length); 622 } 623 else 624 { 625 enum aaPattern = "%s%*s %s%-*s %s%s%s%s(%d)\n"; 626 627 sink.formattedWrite( 628 aaPattern, 629 typeCode.asANSI, 630 args.typewidth, 631 args.typestring, 632 memberCode.asANSI, 633 args.namewidth, 634 args.memberstring, 635 (content.length ? string.init : " "), 636 valueCode.asANSI, 637 content.keys, 638 lengthCode.asANSI, 639 content.length); 640 } 641 } 642 else 643 { 644 if (args.truncate && (content.length > truncateAfter)) 645 { 646 enum aaPattern = "%*s %-*s %s ... (%d)\n"; 647 648 sink.formattedWrite( 649 aaPattern, 650 args.typewidth, 651 args.typestring, 652 args.namewidth, 653 args.memberstring, 654 content.keys[0..truncateAfter], 655 content.length); 656 } 657 else 658 { 659 enum aaPattern = "%*s %-*s %s%s(%d)\n"; 660 661 sink.formattedWrite( 662 aaPattern, 663 args.typewidth, 664 args.typestring, 665 args.namewidth, 666 args.memberstring, 667 (content.length ? string.init : " "), 668 content, 669 content.length); 670 } 671 } 672 } 673 674 675 // FormatAggregateMemberArguments 676 /++ 677 Argument aggregate for invocations of [formatAggregateMemberImpl]. 678 +/ 679 private struct FormatAggregateMemberArguments 680 { 681 /// Type name. 682 string typestring; 683 684 /// Member name. 685 string memberstring; 686 687 /// Type of member aggregate; one of "struct", "class", "interface" and "union". 688 string aggregateType; 689 690 /// Text snippet indicating whether or not the aggregate is in an initial state. 691 string initText; 692 693 /// Width (length) of longest type name. 694 uint typewidth; 695 696 /// Width (length) of longest member name. 697 uint namewidth; 698 699 /// Whether or not we should compensate for a bright terminal background. 700 bool bright; 701 } 702 703 704 // formatAggregateMemberImpl 705 /++ 706 Formats the description of an aggregate for insertion into a [formatObjects] listing. 707 708 Broken out of [formatObjects] to reduce template bloat. 709 710 Params: 711 coloured = Whether or no to display terminal colours. 712 sink = Output range to store output in. 713 args = Argument aggregate for easier passing. 714 +/ 715 private void formatAggregateMemberImpl(Flag!"coloured" coloured, Sink) 716 (auto ref Sink sink, const FormatAggregateMemberArguments args) 717 { 718 import std.format : formattedWrite; 719 720 static if (coloured) 721 { 722 import kameloso.terminal.colours.defs : F = TerminalForeground; 723 import kameloso.terminal.colours : asANSI; 724 725 enum normalPattern = "%s%*s %s%-*s %s<%s>%s\n"; 726 immutable memberCode = args.bright ? F.black : F.white; 727 immutable valueCode = args.bright ? F.green : F.lightgreen; 728 immutable typeCode = args.bright ? F.lightcyan : F.cyan; 729 730 sink.formattedWrite( 731 normalPattern, 732 typeCode.asANSI, 733 args.typewidth, 734 args.typestring, 735 memberCode.asANSI, 736 args.namewidth, 737 args.memberstring, 738 valueCode.asANSI, 739 args.aggregateType, 740 args.initText); 741 } 742 else 743 { 744 enum normalPattern = "%*s %-*s <%s>%s\n"; 745 sink.formattedWrite( 746 normalPattern, 747 args.typewidth, 748 args.typestring, 749 args.namewidth, 750 args.memberstring, 751 args.aggregateType,args.initText); 752 } 753 } 754 755 756 // FormatOtherMemberArguments 757 /++ 758 Argument aggregate for invocations of [formatOtherMemberImpl]. 759 +/ 760 private struct FormatOtherMemberArguments 761 { 762 /// Type name. 763 string typestring; 764 765 /// Member name. 766 string memberstring; 767 768 /// Width (length) of longest type name. 769 uint typewidth; 770 771 /// Width (length) of longest member name. 772 uint namewidth; 773 774 /// Whether or not we should compensate for a bright terminal background. 775 bool bright; 776 } 777 778 779 // formatOtherMemberImpl 780 /++ 781 Formats the description of a non-string, non-array, non-aggregate value 782 for insertion into a [formatObjects] listing. 783 784 Broken out of [formatObjects] to reduce template bloat. 785 786 Params: 787 coloured = Whether or no to display terminal colours. 788 sink = Output range to store output in. 789 args = Argument aggregate for easier passing. 790 content = The value we're describing. 791 +/ 792 private void formatOtherMemberImpl(Flag!"coloured" coloured, T, Sink) 793 (auto ref Sink sink, const FormatOtherMemberArguments args, const auto ref T content) 794 { 795 import std.format : formattedWrite; 796 797 static if (coloured) 798 { 799 import kameloso.terminal.colours.defs : F = TerminalForeground; 800 import kameloso.terminal.colours : asANSI; 801 802 enum normalPattern = "%s%*s %s%-*s %s%s\n"; 803 immutable memberCode = args.bright ? F.black : F.white; 804 immutable valueCode = args.bright ? F.green : F.lightgreen; 805 immutable typeCode = args.bright ? F.lightcyan : F.cyan; 806 807 sink.formattedWrite( 808 normalPattern, 809 typeCode.asANSI, 810 args.typewidth, 811 args.typestring, 812 memberCode.asANSI, 813 args.namewidth, 814 args.memberstring, 815 valueCode.asANSI, 816 content); 817 } 818 else 819 { 820 enum normalPattern = "%*s %-*s %s\n"; 821 sink.formattedWrite( 822 normalPattern, 823 args.typewidth, 824 args.typestring, 825 args.namewidth, 826 args.memberstring, 827 content); 828 } 829 } 830 831 832 // formatObjectImpl 833 /++ 834 Formats an aggregate object, with all its printable members with all their 835 printable values. This is an implementation template and should not be 836 called directly; instead use [printObjects] or [formatObjects]. 837 838 Params: 839 all = Whether or not to also display members marked as 840 [lu.uda.Unserialisable|Unserialisable]; usually transitive 841 information that doesn't carry between program runs. 842 Also those annotated [lu.uda.Hidden|Hidden]. 843 coloured = Whether to display in colours or not. 844 sink = Output range to write to. 845 bright = Whether or not to format for a bright terminal background. 846 thing = Aggregate object to enumerate and format. 847 typewidth = The width with which to pad type names, to align properly. 848 namewidth = The width with which to pad variable names, to align properly. 849 +/ 850 private void formatObjectImpl(Flag!"all" all = No.all, 851 Flag!"coloured" coloured = Yes.coloured, Sink, Thing) 852 (auto ref Sink sink, 853 const Flag!"brightTerminal" bright, 854 auto ref Thing thing, 855 const uint typewidth, 856 const uint namewidth) 857 if (isOutputRange!(Sink, char[]) && isAggregateType!Thing) 858 { 859 static if (coloured) 860 { 861 import kameloso.terminal.colours.defs : F = TerminalForeground; 862 import kameloso.terminal.colours : applyANSI; 863 } 864 865 import lu.string : stripSuffix; 866 import std.format : formattedWrite; 867 import std.traits : Unqual; 868 869 alias Thing = Unqual!(typeof(thing)); 870 871 static if (coloured) 872 { 873 immutable titleCode = bright ? F.black : F.white; 874 sink.applyANSI(titleCode); 875 scope(exit) sink.applyANSI(F.default_); 876 } 877 878 sink.formattedWrite("-- %s\n", Thing.stringof.stripSuffix("Settings")); 879 880 foreach (immutable memberstring; __traits(derivedMembers, Thing)) 881 { 882 import kameloso.traits : memberIsMutable, memberIsValue, 883 memberIsVisibleAndNotDeprecated, memberstringIsThisCtorOrDtor; 884 import lu.traits : isSerialisable; 885 import lu.uda : Hidden, Unserialisable; 886 import std.traits : hasUDA; 887 888 enum namePadding = 2; 889 890 static if ( 891 !memberstringIsThisCtorOrDtor(memberstring) && 892 memberIsVisibleAndNotDeprecated!(Thing, memberstring) && 893 memberIsValue!(Thing, memberstring) && 894 memberIsMutable!(Thing, memberstring) && 895 (all || 896 (isSerialisable!(__traits(getMember, Thing, memberstring)) && 897 !hasUDA!(__traits(getMember, Thing, memberstring), Hidden) && 898 !hasUDA!(__traits(getMember, Thing, memberstring), Unserialisable)))) 899 { 900 import lu.traits : isTrulyString; 901 import std.traits : isAggregateType, isArray, isAssociativeArray; 902 903 alias T = Unqual!(typeof(__traits(getMember, Thing, memberstring))); 904 905 static if (isTrulyString!T) 906 { 907 FormatStringMemberArguments args; 908 args.typestring = T.stringof; 909 args.memberstring = memberstring; 910 args.typewidth = typewidth; 911 args.namewidth = namewidth + namePadding; 912 args.bright = bright; 913 args.truncate = !all; 914 formatStringMemberImpl!(coloured, T)(sink, args, __traits(getMember, thing, memberstring)); 915 } 916 else static if (isArray!T || isAssociativeArray!T) 917 { 918 import lu.traits : UnqualArray; 919 import std.range.primitives : ElementEncodingType; 920 921 alias ElemType = Unqual!(ElementEncodingType!T); 922 923 FormatArrayMemberArguments args; 924 args.typestring = UnqualArray!T.stringof; 925 args.memberstring = memberstring; 926 args.elemstring = ElemType.stringof; 927 args.typewidth = typewidth; 928 args.namewidth = namewidth + namePadding; 929 args.truncate = !all; 930 args.bright = bright; 931 932 static if (isArray!T) 933 { 934 enum elemIsCharacter = 935 is(ElemType == char) || 936 is(ElemType == dchar) || 937 is(ElemType == wchar); 938 939 args.elemIsCharacter = elemIsCharacter; 940 formatArrayMemberImpl!(coloured, T) 941 (sink, 942 args, 943 __traits(getMember, thing, memberstring)); 944 } 945 else /*static if (isAssociativeArray!T)*/ 946 { 947 // Can't pass T for some reason, nor UnqualArray 948 formatAssociativeArrayMemberImpl!(coloured, T) 949 (sink, 950 args, 951 __traits(getMember, thing, memberstring)); 952 } 953 } 954 else static if (isAggregateType!T) 955 { 956 enum aggregateType = 957 is(T == struct) ? "struct" : 958 is(T == class) ? "class" : 959 is(T == interface) ? "interface" : 960 /*is(T == union) ?*/ "union"; //: "<error>"; 961 962 static if (is(Thing == struct) && is(T == struct)) 963 { 964 immutable initText = (__traits(getMember, thing, memberstring) == 965 __traits(getMember, Thing.init, memberstring)) ? 966 " (init)" : 967 string.init; 968 } 969 else static if (is(T == class) || is(T == interface)) 970 { 971 immutable initText = (__traits(getMember, thing, memberstring) is null) ? 972 " (null)" : 973 string.init; 974 } 975 else 976 { 977 enum initText = string.init; 978 } 979 980 FormatAggregateMemberArguments args; 981 args.typestring = T.stringof; 982 args.memberstring = memberstring; 983 args.aggregateType = aggregateType; 984 args.initText = initText; 985 args.typewidth = typewidth; 986 args.namewidth = namewidth + namePadding; 987 args.bright = bright; 988 formatAggregateMemberImpl!coloured(sink, args); 989 } 990 else 991 { 992 FormatOtherMemberArguments args; 993 args.typestring = T.stringof; 994 args.memberstring = memberstring; 995 args.typewidth = typewidth; 996 args.namewidth = namewidth + namePadding; 997 args.bright = bright; 998 formatOtherMemberImpl!(coloured, T)(sink, args, __traits(getMember, thing, memberstring)); 999 } 1000 } 1001 } 1002 } 1003 1004 /// 1005 @system unittest 1006 { 1007 import lu.string : contains; 1008 import std.array : Appender; 1009 1010 Appender!(char[]) sink; 1011 sink.reserve(512); // ~323 1012 1013 struct Struct 1014 { 1015 string members; 1016 int asdf; 1017 } 1018 1019 // Monochrome 1020 1021 struct StructName 1022 { 1023 Struct struct_; 1024 int i = 12_345; 1025 string s = "the moon; the sign of hope! it appeared when we left the pain " ~ 1026 "of the ice desert behind. we faced up to the curse and endured " ~ 1027 "misery. condemned we are! we brought hope but also lies, and treachery..."; 1028 string p = "!"; 1029 string p2; 1030 bool b = true; 1031 float f = 3.14f; 1032 double d = 99.9; 1033 const(char)[] c = [ 'a', 'b', 'c' ]; 1034 const(char)[] emptyC; 1035 string[] dynA = [ "foo", "bar", "baz" ]; 1036 int[] iA = [ 1, 2, 3, 4 ]; 1037 const(char)[char] cC; 1038 } 1039 1040 StructName s; 1041 s.cC = [ 'a':'a', 'b':'b' ]; 1042 assert('a' in s.cC); 1043 assert('b' in s.cC); 1044 1045 sink.formatObjects!(No.all, No.coloured)(No.brightTerminal, s); 1046 1047 enum theMoon = `"the moon; the sign of hope! it appeared when we left the ` ~ 1048 `pain of the ice desert behind. we faced up to the curse and endured mis"`; 1049 1050 enum structNameSerialised = 1051 `-- StructName 1052 Struct struct_ <struct> (init) 1053 int i 12345 1054 string s ` ~ theMoon ~ ` ... (198) 1055 string p "!"(1) 1056 string p2 ""(0) 1057 bool b true 1058 float f 3.14 1059 double d 99.9 1060 char[] c ['a', 'b', 'c'](3) 1061 char[] emptyC [](0) 1062 string[] dynA ["foo", "bar", "baz"](3) 1063 int[] iA [1, 2, 3, 4](4) 1064 char[char] cC ['b':'b', 'a':'a'](2) 1065 `; 1066 assert((sink.data == structNameSerialised), "\n" ~ sink.data); 1067 1068 // Adding Settings does nothing 1069 alias StructNameSettings = StructName; 1070 StructNameSettings so = s; 1071 sink.clear(); 1072 sink.formatObjects!(No.all, No.coloured)(No.brightTerminal, so); 1073 1074 assert((sink.data == structNameSerialised), "\n" ~ sink.data); 1075 1076 // Class copy 1077 class ClassName 1078 { 1079 Struct struct_; 1080 int i = 12_345; 1081 string s = "foo"; 1082 string p = "!"; 1083 string p2; 1084 bool b = true; 1085 float f = 3.14f; 1086 double d = 99.9; 1087 const(char)[] c = [ 'a', 'b', 'c' ]; 1088 const(char)[] emptyC; 1089 string[] dynA = [ "foo", "bar", "baz" ]; 1090 int[] iA = [ 1, 2, 3, 4 ]; 1091 const(char)[char] cC; 1092 } 1093 1094 auto c1 = new ClassName; 1095 sink.clear(); 1096 sink.formatObjects!(No.all, No.coloured)(No.brightTerminal, c1); 1097 1098 enum classNameSerialised = 1099 `-- ClassName 1100 Struct struct_ <struct> 1101 int i 12345 1102 string s "foo"(3) 1103 string p "!"(1) 1104 string p2 ""(0) 1105 bool b true 1106 float f 3.14 1107 double d 99.9 1108 char[] c ['a', 'b', 'c'](3) 1109 char[] emptyC [](0) 1110 string[] dynA ["foo", "bar", "baz"](3) 1111 int[] iA [1, 2, 3, 4](4) 1112 char[char] cC [](0) 1113 `; 1114 1115 assert((sink.data == classNameSerialised), '\n' ~ sink.data); 1116 1117 // Two at a time 1118 struct Struct1 1119 { 1120 string members; 1121 int asdf; 1122 } 1123 1124 struct Struct2 1125 { 1126 string mumburs; 1127 int fdsa; 1128 } 1129 1130 Struct1 st1; 1131 Struct2 st2; 1132 1133 st1.members = "harbl"; 1134 st1.asdf = 42; 1135 st2.mumburs = "hirrs"; 1136 st2.fdsa = -1; 1137 1138 sink.clear(); 1139 sink.formatObjects!(No.all, No.coloured)(No.brightTerminal, st1, st2); 1140 enum st1st2Formatted = 1141 `-- Struct1 1142 string members "harbl"(5) 1143 int asdf 42 1144 1145 -- Struct2 1146 string mumburs "hirrs"(5) 1147 int fdsa -1 1148 `; 1149 assert((sink.data == st1st2Formatted), '\n' ~ sink.data); 1150 1151 // Colour 1152 struct StructName2 1153 { 1154 int int_ = 12_345; 1155 string string_ = "foo"; 1156 bool bool_ = true; 1157 float float_ = 3.14f; 1158 double double_ = 99.9; 1159 } 1160 1161 version(Colours) 1162 { 1163 StructName2 s2; 1164 1165 sink.clear(); 1166 sink.reserve(256); // ~239 1167 sink.formatObjects!(No.all, Yes.coloured)(No.brightTerminal, s2); 1168 1169 assert((sink.data.length > 12), "Empty sink after coloured fill"); 1170 1171 assert(sink.data.contains("-- StructName")); 1172 assert(sink.data.contains("int_")); 1173 assert(sink.data.contains("12345")); 1174 1175 assert(sink.data.contains("string_")); 1176 assert(sink.data.contains(`"foo"`)); 1177 1178 assert(sink.data.contains("bool_")); 1179 assert(sink.data.contains("true")); 1180 1181 assert(sink.data.contains("float_")); 1182 assert(sink.data.contains("3.14")); 1183 1184 assert(sink.data.contains("double_")); 1185 assert(sink.data.contains("99.9")); 1186 1187 // Adding Settings does nothing 1188 alias StructName2Settings = StructName2; 1189 immutable sinkCopy = sink.data.idup; 1190 StructName2Settings s2o; 1191 1192 sink.clear(); 1193 sink.formatObjects!(No.all, Yes.coloured)(No.brightTerminal, s2o); 1194 assert((sink.data == sinkCopy), sink.data); 1195 } 1196 1197 class C 1198 { 1199 string a = "abc"; 1200 bool b = true; 1201 int i = 42; 1202 } 1203 1204 C c2 = new C; 1205 1206 sink.clear(); 1207 sink.formatObjects!(No.all, No.coloured)(No.brightTerminal, c2); 1208 enum cFormatted = 1209 `-- C 1210 string a "abc"(3) 1211 bool b true 1212 int i 42 1213 `; 1214 assert((sink.data == cFormatted), '\n' ~ sink.data); 1215 1216 sink.clear(); 1217 1218 interface I3 1219 { 1220 void foo(); 1221 } 1222 1223 class C3 : I3 1224 { 1225 void foo() {} 1226 int i; 1227 } 1228 1229 class C4 1230 { 1231 I3 i3; 1232 C3 c3; 1233 int i = 42; 1234 } 1235 1236 C4 c4 = new C4; 1237 //c4.i3 = new C3; 1238 c4.c3 = new C3; 1239 c4.c3.i = -1; 1240 1241 sink.formatObjects!(No.all, No.coloured)(No.brightTerminal, c4, c4.i3, c4.c3); 1242 enum c4Formatted = 1243 `-- C4 1244 I3 i3 <interface> (null) 1245 C3 c3 <class> 1246 int i 42 1247 1248 -- I3 1249 1250 -- C3 1251 int i -1 1252 `; 1253 assert((sink.data == c4Formatted), '\n' ~ sink.data); 1254 } 1255 1256 1257 // formatObjects 1258 /++ 1259 Formats a struct object, with all its printable members with all their 1260 printable values. A `string`-returning overload that doesn't take an input range. 1261 1262 This is useful when you just want the object(s) formatted without having to 1263 pass it a sink. 1264 1265 Example: 1266 --- 1267 struct Foo 1268 { 1269 int foo = 42; 1270 string bar = "arr matey"; 1271 float f = 3.14f; 1272 double d = 9.99; 1273 } 1274 1275 Foo foo, bar; 1276 1277 writeln(formatObjects!(No.all, Yes.coloured)(foo)); 1278 writeln(formatObjects!(Yes.all, No.coloured)(bar)); 1279 --- 1280 1281 Params: 1282 all = Whether or not to also display members marked as 1283 [lu.uda.Unserialisable|Unserialisable]; usually transitive 1284 information that doesn't carry between program runs. 1285 Also those annotated [lu.uda.Hidden|Hidden]. 1286 coloured = Whether to display in colours or not. 1287 bright = Whether or not to format for a bright terminal background. 1288 things = Variadic list of structs to enumerate and format. 1289 1290 Returns: 1291 String with the object formatted, as per the passed arguments. 1292 +/ 1293 string formatObjects(Flag!"all" all = No.all, 1294 Flag!"coloured" coloured = Yes.coloured, Things...) 1295 (const Flag!"brightTerminal" bright, auto ref Things things) 1296 if ((Things.length > 0) && !isOutputRange!(Things[0], char[])) 1297 { 1298 import kameloso.constants : BufferSize; 1299 import std.array : Appender; 1300 1301 Appender!(char[]) sink; 1302 sink.reserve(BufferSize.printObjectBufferPerObject * Things.length); 1303 1304 formatObjects!(all, coloured)(sink, bright, things); 1305 return sink.data; 1306 } 1307 1308 /// 1309 unittest 1310 { 1311 // Rely on the main unit tests of the output range version of formatObjects 1312 1313 struct Struct 1314 { 1315 string members; 1316 int asdf; 1317 } 1318 1319 Struct s; 1320 s.members = "foo"; 1321 s.asdf = 42; 1322 1323 immutable formatted = formatObjects!(No.all, No.coloured)(No.brightTerminal, s); 1324 assert((formatted == 1325 `-- Struct 1326 string members "foo"(3) 1327 int asdf 42 1328 `), '\n' ~ formatted); 1329 1330 class Nested 1331 { 1332 int harbl; 1333 string snarbl; 1334 } 1335 1336 class ClassSettings 1337 { 1338 string s = "arb"; 1339 int i; 1340 string someLongConfiguration = "acdc adcadcad acacdadc"; 1341 int[] arrMatey = [ 1, 2, 3, 42 ]; 1342 Nested nest; 1343 } 1344 1345 auto c = new ClassSettings; 1346 c.i = 2; 1347 1348 immutable formattedClass = formatObjects!(No.all, No.coloured)(No.brightTerminal, c); 1349 assert((formattedClass == 1350 `-- Class 1351 string s "arb"(3) 1352 int i 2 1353 string someLongConfiguration "acdc adcadcad acacdadc"(22) 1354 int[] arrMatey [1, 2, 3, 42](4) 1355 Nested nest <class> (null) 1356 `), '\n' ~ formattedClass); 1357 1358 c.nest = new Nested; 1359 immutable formattedClass2 = formatObjects!(No.all, No.coloured)(No.brightTerminal, c); 1360 assert((formattedClass2 == 1361 `-- Class 1362 string s "arb"(3) 1363 int i 2 1364 string someLongConfiguration "acdc adcadcad acacdadc"(22) 1365 int[] arrMatey [1, 2, 3, 42](4) 1366 Nested nest <class> 1367 `), '\n' ~ formattedClass2); 1368 1369 struct Reparse {} 1370 struct Client {} 1371 struct Server {} 1372 1373 struct State 1374 { 1375 Client client; 1376 Server server; 1377 Reparse[] reparses; 1378 bool hasReplays; 1379 } 1380 1381 State state; 1382 1383 immutable formattedState = formatObjects!(No.all, No.coloured)(No.brightTerminal, state); 1384 assert((formattedState == 1385 `-- State 1386 Client client <struct> (init) 1387 Server server <struct> (init) 1388 Reparse[] reparses [](0) 1389 bool hasReplays false 1390 `), '\n' ~ formattedState); 1391 }