1 module commandr.help;
2 
3 import commandr.program;
4 import commandr.option;
5 import std.algorithm : filter, map, any, chunkBy;
6 import std.array : join, array;
7 import std.conv : to;
8 import std.stdio : writefln, writeln, write;
9 import std..string : format;
10 import std.range : chain, empty;
11 
12 
13 ///
14 struct HelpOutput {
15     ///
16     bool colors = true;
17     // bool compact = false;
18 
19     ///
20     // int maxWidth = 80;
21 
22     ///
23     int indent = 24;
24     ///
25     int optionsLimit = 6;
26     ///
27     int commandLimit = 6;
28 }
29 
30 ///
31 void printHelp(Command program, HelpOutput output = HelpOutput.init) {
32     HelpPrinter(output).printHelp(program);
33 }
34 
35 ///
36 void printUsage(Command program, HelpOutput output = HelpOutput.init) {
37     HelpPrinter(output).printUsage(program);
38 }
39 
40 
41 struct HelpPrinter {
42     HelpOutput config;
43 
44     public this(HelpOutput config) nothrow pure @safe {
45         this.config = config;
46     }
47 
48     void printHelp(Command program) {
49         if (cast(Program)program) {
50             writefln("%s: %s %s(%s)%s", program.name, program.summary, ansi("2"), program.version_, ansi("0"));
51             writeln();
52         }
53         else {
54             writefln("%s: %s", program.chain.join(" "), program.summary);
55             writeln();
56         }
57 
58         writefln("%sUSAGE%s", ansi("1"), ansi("0"));
59         write("  $ "); // prefix for usage
60         printUsage(program);
61         writeln();
62 
63         if (!program.flags.empty) {
64             writefln("%sFLAGS%s", ansi("1"), ansi("0"));
65             foreach(flag; program.flags) {
66                 printHelp(flag);
67             }
68             writeln();
69         }
70 
71         if (!program.options.empty) {
72             writefln("%sOPTIONS%s", ansi("1"), ansi("0"));
73             foreach(option; program.options) {
74                 printHelp(option);
75             }
76             writeln();
77         }
78 
79         if (!program.arguments.empty) {
80             writefln("%sARGUMENTS%s", ansi("1"), ansi("0"));
81             foreach(arg; program.arguments) {
82                 printHelp(arg);
83             }
84             writeln();
85         }
86 
87         if (program.commands.length > 0) {
88             writefln("%sSUBCOMMANDS%s", ansi("1"), ansi("0"));
89             printSubcommands(program.commands);
90             writeln();
91         }
92     }
93 
94     void printUsage(Program program) {
95         string optionsUsage = "[options]";
96 
97         // if there are not too many options
98         if (program.options.length + program.flags.length <= config.optionsLimit) {
99             optionsUsage = chain(
100                 program.flags.map!(f => optionUsage(f)),
101                 program.options.map!(o => optionUsage(o))
102             ).join(" ");
103         } else {
104             optionsUsage ~= " " ~ program.options.filter!(o => o.isRequired).map!(o => optionUsage(o)).join(" ");
105         }
106 
107         string commands = program.commands.length == 0 ? "" : (
108             program.commands.length > config.commandLimit ? "COMMAND" : program.commands.keys.join("|")
109         );
110         string args = program.arguments.map!(a => argUsage(a)).join(" ");
111 
112         writefln("%s %s %s%s",
113             program.binaryName,
114             optionsUsage,
115             args.empty ? "" : args ~ " ",
116             commands
117         );
118     }
119 
120     void printUsage(Command command) {
121         string optionsUsage = "[options]";
122         if (command.options.length + command.flags.length <= config.optionsLimit) {
123             optionsUsage = chain(
124                 command.flags.map!(f => optionUsage(f)),
125                 command.options.map!(o => optionUsage(o))
126             ).join(" ");
127         } else {
128             optionsUsage ~= " " ~ command.options.filter!(o => o.isRequired).map!(o => optionUsage(o)).join(" ");
129         }
130 
131         string commands = command.commands.length == 0 ? "" : (
132             command.commands.length > config.commandLimit ? "command" : command.commands.keys.join("|")
133         );
134         string args = command.arguments.map!(a => argUsage(a)).join(" ");
135 
136         writefln("%s %s %s%s",
137             usageChain(command),
138             optionsUsage,
139             args.empty ? "" : args ~ " ",
140             commands
141         );
142     }
143 
144     private void printHelp(Flag flag) {
145         string left = optionNames(flag);
146         writefln("  %-"~config.indent.to!string~"s  %s%s%s", left, ansi("2"), flag.description, ansi("0"));
147     }
148 
149     private void printHelp(Option option) {
150         string left = optionNames(option);
151         size_t length = left.length + option.tag.length + 1;
152         string formatted = "%s %s%s%s".format(left, ansi("4"), option.tag, ansi("0"));
153         size_t padLength = config.indent + (formatted.length - length);
154 
155         writefln("  %-"~padLength.to!string~"s  %s%s%s", formatted, ansi("2"), option.description, ansi("0"));
156     }
157 
158     private void printHelp(Argument arg) {
159         writefln("  %-"~config.indent.to!string~"s  %s%s%s", arg.tag, ansi("2"), arg.description, ansi("0"));
160     }
161 
162     private void printSubcommands(Command[string] commands) {
163         auto grouped = commands.values.chunkBy!(a => a.topic).array;
164 
165         if (grouped.length == 1 && grouped[0][0] is null) {
166             foreach(key, command; commands) {
167                 writefln("  %-"~config.indent.to!string~"s  %s%s%s", key, ansi("2"), command.summary, ansi("0"));
168             }
169         }
170         else {
171             foreach (entry; grouped) {
172                 writefln("  %s%s%s:", ansi("4"), entry[0], ansi("0"));
173                 foreach(command; entry[1]) {
174                     writefln(
175                         "    %-"~(config.indent - 2).to!string~"s  %s%s%s",
176                         command.name, ansi("2"), command.summary, ansi("0")
177                     );
178                 }
179                 writeln();
180             }
181         }
182     }
183 
184     private string usageChain(Command target) {
185         Command[] commands = [];
186         Command dest = target.parent;
187         while (dest !is null) {
188             commands ~= dest;
189             dest = dest.parent;
190         }
191 
192         string[] elements;
193 
194         foreach_reverse(command; commands) {
195             elements ~= ansi("0") ~ command.name ~ ansi("2");
196 
197             foreach (opt; command.options.filter!(o => o.isRequired)) {
198                 elements ~= optionUsage(opt) ~ ansi("2");
199             }
200 
201             foreach (arg; command.arguments.filter!(o => o.isRequired)) {
202                 elements ~= argUsage(arg);
203             }
204         }
205 
206         elements ~= ansi("0") ~ target.name;
207 
208         return elements.join(" ");
209     }
210 
211     private string optionNames(T)(T o) {
212         string names = "";
213 
214         if (o.abbrev) {
215             names ~= "-" ~ o.abbrev;
216         }
217         else {
218             names ~= "    ";
219         }
220 
221         if (o.full) {
222             if (o.abbrev) {
223                 names ~= ", ";
224             }
225             names ~= "--%s".format(o.full);
226         }
227 
228         return names;
229     }
230 
231     private string optionUsage(IOption o) {
232         string result = o.displayName;
233 
234         if (cast(Option)o) {
235             result = "%s %s%s%s".format(result, ansi("4"), (cast(Option)o).tag, ansi("0"));
236         }
237 
238         if (!o.isRequired) {
239             result = "[%s]".format(result);
240         }
241 
242         return result;
243     }
244 
245     private string argUsage(Argument arg) {
246         return (arg.isRequired ? "%s" : "[%s]").format(arg.tag);
247     }
248 
249 
250     private string ansi(string code) {
251         version(Windows) {
252             return "";
253         }
254         version(Posix) {
255             import core.sys.posix.unistd : isatty, STDOUT_FILENO;
256 
257             if (config.colors && isatty(STDOUT_FILENO)) {
258                 return "\033[%sm".format(code);
259             }
260 
261             return "";
262         }
263 
264         assert(0);
265     }
266 }