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 }