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 }