1 module jcli.helptext.helptext;
2 
3 import jcli.core, jcli.text, jcli.introspect;
4 
5 // TODO: Maybe make it take the processed command info instead of the command type itself.
6 struct CommandHelpText(alias CommandT_)
7 {
8     alias CommandType  = CommandT_;
9     alias CommandInfo  = jcli.introspect.CommandInfo!CommandType;
10     alias ArgumentInfo = CommandInfo.Arguments;
11 
12     private string _cached;
13 
14     import std.file : thisExePath;
15     import std.path : baseName;
16     
17     string generate(string appName = thisExePath().baseName, uint width = 180)
18     {
19         import std.range;
20         import std.algorithm;
21 
22         if (this._cached)
23             return this._cached;
24 
25         HelpText help = HelpText.make(width);
26 
27         static struct Arg
28         {
29             string name;
30             string description;
31             ArgGroup group;
32             bool optional;
33         }
34 
35         static string patternToNamedArgList(Pattern pattern)
36         {
37             return pattern.map!(p => p.length == 1 ? "-" ~ p : "--" ~ p).join(" ");
38         }
39 
40         Arg[] positionals;
41         Arg[] named;
42 
43         static foreach(i, pos; ArgumentInfo.positional)
44             positionals ~= Arg(pos.uda.name, pos.uda.description);
45         static foreach(i, nam; ArgumentInfo.named)
46         {
47             named ~= Arg(
48                 patternToNamedArgList(cast()nam.pattern),
49                 nam.description,
50                 nam.group,
51                 nam.flags.has(ArgFlags._optionalBit)
52             );
53         }
54 
55         named.multiSort!("a.optional != b.optional", "a.name < b.name");
56         
57         static if (CommandInfo.flags.hasEither(CommandFlags.explicitlyDefault | CommandFlags.stringAttribute))
58             enum name = "Default";
59         else
60             enum name = CommandInfo.udaValue.name;
61 
62         // TODO:
63         // This allocates way too much memory for no reason, and is slower as a result.
64         // Just make that addLineWithPrefix take a range, and just chain these together.
65         // Or make it expose the appender and do a `formattedWrite`.
66         import std.format : format;
67         help.addLineWithPrefix("Usage: ", "%s %s%s %s".format(
68             appName,
69             name,
70             positionals
71                 .map!(p =>  "<" ~ p.name ~ ">")
72                 .join(" "),
73             named
74                 .map!(p => p.optional ? "[" ~ p.name ~ "]" : p.name)
75                 .join(" ")
76         ), AnsiStyleSet.init.style(AnsiStyle.init.bold));
77 
78         static if (CommandInfo.description)
79             help.addHeaderWithText("Description: ", CommandInfo.description);
80 
81         if (positionals.length > 0)
82         {
83             help.addHeader("Positional Arguments:");
84             foreach (pos; positionals)
85             {
86                 help.addArgument(
87                     pos.name,
88                     [HelpTextDescription(0, pos.description)]
89                 );
90             }
91         }
92 
93         Arg[][ArgGroup] argsByGroup;
94         foreach(nam; named)
95         {
96             scope ptr = (nam.group in argsByGroup);
97             if (!ptr)
98             {
99                 argsByGroup[nam.group] = Arg[].init;
100                 ptr = (nam.group in argsByGroup);
101             }
102 
103             (*ptr) ~= nam;
104         }
105 
106         foreach (group, args; argsByGroup)
107         {
108             help.addLine(null);
109 
110             if (group == ArgGroup.init)
111                 help.addHeader("Named Arguments: ");
112             else if (group.description == null)
113                 help.addHeader(group.name);
114             else
115                 help.addHeaderWithText(group.name, group.description);
116 
117             foreach (arg; args)
118             {
119                 auto descs = [HelpTextDescription(0, arg.description)];
120 
121                 help.addArgument(
122                     arg.name,
123                     descs
124                 );
125             }
126         }
127 
128         this._cached = help.finish();
129         return this._cached;
130     }
131 }
132 
133 unittest
134 {
135     @Command("command", "This is a command that is totally super complicated.")
136     static struct ComplexCommand
137     {
138         @ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere")
139         int a;
140         @ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere")
141         int b;
142         @ArgPositional("output", "Where to place the output.")
143         string output;
144 
145         @ArgNamed("test-flag", "Test flag, please ignore.")
146         @(ArgConfig.parseAsFlag)
147         bool flag;
148 
149         @ArgGroup("Debug", "Arguments related to debugging.")
150         {
151             @ArgNamed("verbose|v", "Enables verbose logging.")
152             Nullable!bool verbose;
153 
154             @ArgNamed("log|l", "Specifies a log file to direct output to.")
155             Nullable!string log;
156         }
157 
158         @ArgGroup("I/O", "Arguments related to I/O.")
159         @ArgNamed("config|c", "Specifies the config file to use.")
160         Nullable!string config;
161 
162         void onExecute(){}
163     }
164 
165     auto c = CommandHelpText!ComplexCommand();
166     // I've learned its next to pointless to fully unittest help text, since it can change so subtly and so often
167     // that manual validation is good enough.
168     //assert(false, c.generate());
169 }