Skip to content

Commit f7ed109

Browse files
committed
improved help formatting
1 parent d05b74c commit f7ed109

File tree

6 files changed

+206
-48
lines changed

6 files changed

+206
-48
lines changed

src/cmd/ansi.d

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module cmd.ansi;
2+
3+
import std.regex;
4+
5+
public string stripAnsi(string input) @safe {
6+
return input.replaceAll(
7+
regex("\\x1B\\[[0-9;:?]*[A-Za-z]|\\x1B\\]8;;.*?\\x07(.*?)\\x1B\\]8;;\\x07|\\x1BO.|\\x1B.", "gs"),
8+
"$1"
9+
);
10+
}
11+
12+
public string bold(string text) nothrow @safe {
13+
return "\x1B[1m" ~ text ~ "\x1B[0m";
14+
}
15+
16+
public string dim(string text) nothrow @safe {
17+
return "\x1B[2m" ~ text ~ "\x1B[0m";
18+
}
19+
20+
public string brightBlack(string text) nothrow @safe {
21+
return "\x1B[90m" ~ text ~ "\x1B[0m";
22+
}
23+
24+
public string link(string text, string url) nothrow @safe {
25+
return "\x1B]8;;" ~ url ~ "\x07" ~ text ~ "\x1B]8;;\x07";
26+
}

src/cmd/argument.d

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module cmd.argument;
22

33
import std.string;
44

5+
import cmd.ansi;
6+
57
/** Represents a command-line argument. */
68
public class Argument {
79
/** Name of the argument. */
@@ -77,9 +79,18 @@ public class Argument {
7779
);
7880
}
7981

80-
/** Returns formatted name, e.g., "<file>" or "[files...]". */
81-
public string formattedName() const nothrow @safe {
82+
/**
83+
* Returns formatted name, e.g., "<file>" or "[files...]".
84+
*
85+
* Params:
86+
* colors = Whether to use colors.
87+
*/
88+
public string formattedName(bool colors = false) const nothrow @safe {
8289
const auto namePart = name ~ (variadic ? "..." : "");
90+
if (colors)
91+
return required
92+
? "<".brightBlack() ~ namePart.dim() ~ ">".brightBlack()
93+
: "[".brightBlack() ~ namePart.dim() ~ "]".brightBlack();
8394
return required ? "<" ~ namePart ~ ">" : "[" ~ namePart ~ "]";
8495
}
8596
}

src/cmd/command.d

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ module cmd.command;
33
import core.stdc.stdlib;
44
import std.algorithm;
55
import std.array;
6+
import std.range : repeat;
67
import std.stdio;
78
import std.string;
8-
import std.range : repeat;
99

10+
import cmd.ansi;
1011
import cmd.argument;
12+
import cmd.document;
1113
import cmd.flag;
1214
import cmd.option;
1315
import cmd.parsed_args;
@@ -214,10 +216,12 @@ public class Command {
214216
/**
215217
* Gets the usage string for this command.
216218
*
219+
* Params:
220+
* colors = Whether to use colors in the usage string.
217221
* Throws:
218222
* AssertionError if command chain is null or empty, or if first command is not a Program.
219223
*/
220-
public string usage() const {
224+
public string usage(bool colors = false) const {
221225
assert(chain !is null, "Command chain is not initialised");
222226
assert(!chain.empty(), "Command chain is empty. The command should have at least itself in its chain.");
223227
Program program = cast(Program) chain[0];
@@ -226,75 +230,62 @@ public class Command {
226230
Appender!string sb;
227231
sb.put(program.name());
228232
if (program.versionOption() || program.helpOption())
229-
sb.put(chain.length > 1 ? " [global options]" : " [options]");
233+
sb.put(" "
234+
~ "[".brightBlack() ~ ((chain.length > 1 ? "global " : "") ~ "options").dim() ~ "]".brightBlack());
230235
if (chain.length > 1)
231236
sb.put(" " ~ chain[1..$].map!(c => c.name()).array.join(" "));
232237
if (!subcommands.empty())
233-
sb.put(" <command> ...");
238+
sb.put(" " ~ "<".brightBlack() ~ "command".dim() ~ ">".brightBlack() ~ " " ~ "...".brightBlack());
234239
else {
235240
if (!options.empty() || !flags.empty())
236-
sb.put(" " ~ (options.any!(o => o.required) ? "<options>" : "[options]"));
241+
sb.put(" " ~ (options.any!(o => o.required)
242+
? "<".brightBlack() ~ "options".dim() ~ ">".brightBlack()
243+
: "[".brightBlack() ~ "options".dim() ~ "]".brightBlack()
244+
));
237245
foreach (arg; arguments)
238-
sb.put(" " ~ arg.formattedName());
246+
sb.put(" " ~ arg.formattedName(colors));
239247
}
240-
return sb.data;
248+
return colors ? sb.data : sb.data.stripAnsi();
241249
}
242250

243251
/** Prinths help for the command. */
244252
public int printHelp() const {
245-
writeln("\x1b[1mUsage:\x1b[0m");
246-
writeln(" " ~ usage());
247-
248-
if (description() !is null) {
249-
writeln();
250-
writeln("\x1b[1mDescription:\x1b[0m");
251-
writeln(" " ~ description());
252-
}
253-
254-
size_t longest = 0;
255-
foreach (cmd; subcommands)
256-
longest = max(longest, cmd.name().length);
257-
258-
foreach (arg; arguments)
259-
longest = max(longest, arg.name.length);
260-
261-
auto allOpts = cast(Flag[]) (flags ~ cast(Flag[]) options);
262-
foreach (opt; allOpts)
263-
longest = max(longest, opt.paddedName().length);
253+
auto doc = new Document();
254+
doc.add("Usage:".bold(), usage(true));
264255

265256
if (subcommands !is null) {
266-
writeln();
267-
writeln("\x1b[1mCommands:\x1b[0m");
257+
auto s = new Section("Commands:".bold());
258+
doc.add(s);
268259

269260
foreach (cmd; (cast(Command[]) subcommands).dup.sort!((a, b) {
270261
return a.name() < b.name();
271-
}))
272-
writeln(" " ~ cmd.name() ~ ' '.repeat(longest - cmd.name().length + 2).array ~ "\x1b[2m"
273-
~ cmd.description() ~ "\x1b[0m");
262+
})) s.add(cmd.name(), cmd.description());
274263
}
275264

276265
if (arguments !is null) {
277-
writeln();
278-
writeln("\x1b[1mArguments:\x1b[0m");
266+
auto s = new Section("Arguments:".bold());
267+
doc.add(s);
279268

280269
foreach (arg; arguments)
281-
writeln(" " ~ arg.name ~ ' '.repeat(longest - arg.name.length + 2).array ~ "\x1b[2m"
282-
~ arg.description ~ "\x1b[0m");
270+
s.add(arg.name, arg.description);
283271
}
284272

285273
if (!flags.empty() || !options.empty()) {
286-
writeln();
287-
writeln("\x1b[1mOptions:\x1b[0m");
274+
auto s = new Section("Options:".bold());
275+
doc.add(s);
288276

289-
foreach (opt; allOpts.sort!((a, b) {
277+
foreach (opt; (cast(Flag[]) (flags ~ cast(Flag[]) options)).sort!((a, b) {
290278
auto nameA = a.longName !is null ? a.longName : a.shortName;
291279
auto nameB = b.longName !is null ? b.longName : b.shortName;
292280
return nameA < nameB;
293-
}))
294-
writeln(" " ~ opt.paddedName() ~ ' '.repeat(longest - opt.paddedName().length + 2).array ~ "\x1b[2m"
295-
~ opt.description ~ "\x1b[0m");
281+
})) {
282+
if (Option o = cast(Option) opt)
283+
s.add(o.paddedName(true), opt.description);
284+
else s.add(opt.paddedName(), opt.description);
285+
}
296286
}
297287

288+
doc.print();
298289
return 0;
299290
}
300291

src/cmd/document.d

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
module cmd.document;
2+
3+
import std.algorithm;
4+
import std.array;
5+
import std.range;
6+
import std.stdio;
7+
import std.typecons;
8+
9+
import cmd.ansi;
10+
11+
public alias Term = Tuple!(string, string);
12+
13+
public class Section {
14+
public const string title;
15+
public const string body;
16+
protected Term[] _terms;
17+
protected size_t longest = 0;
18+
19+
public this(string title, string body = null, Term[] terms = []) @safe {
20+
assert(title !is null, "Section title cannot be null");
21+
this.title = title;
22+
this.body = body;
23+
_terms = terms;
24+
longest = !terms.empty() ?terms.map!(t => t[0].stripAnsi().length).maxElement() : 0;
25+
}
26+
27+
public const(Term[]) terms() const nothrow @safe {
28+
return _terms;
29+
}
30+
31+
public Section add(Term term) @safe {
32+
_terms ~= term;
33+
longest = max(longest, term[0].stripAnsi().length);
34+
return this;
35+
}
36+
37+
public Section add(string term, string definition) @safe {
38+
return this.add(Term(term, definition));
39+
}
40+
}
41+
42+
public class Document {
43+
protected Section[] _sections;
44+
45+
public this() nothrow @safe {
46+
_sections = [];
47+
}
48+
49+
public const(Section) section(string title) const @safe {
50+
auto sections = this.sections().find!(s => s.title.stripAnsi() == title.stripAnsi());
51+
if (sections.empty()) return null;
52+
return sections.front();
53+
}
54+
55+
public const(Section[]) sections() const nothrow @safe {
56+
return _sections;
57+
}
58+
59+
public Document add(Section section) nothrow @safe {
60+
_sections ~= section;
61+
return this;
62+
}
63+
64+
public Document add(string title, string body = null, Term[] terms = []) @safe {
65+
return this.add(new Section(title, body, terms));
66+
}
67+
68+
public Document add(string section, Term term) @safe {
69+
auto sections = _sections.find!(s => s.title.stripAnsi() == section.stripAnsi());
70+
assert(!sections.empty(), "Could not find section " ~ section);
71+
sections.front().add(term);
72+
return this;
73+
}
74+
75+
public Document add(string section, string term, string definition) @safe {
76+
return this.add(section, Term(term, definition));
77+
}
78+
79+
public void print() const @safe {
80+
const longest = sections().empty() ? 0 : sections().map!(s => s.longest).maxElement();
81+
82+
for (size_t i = 0; i < _sections.length; ++i) {
83+
const section = _sections[i];
84+
writeln(section.title);
85+
if (section.body !is null)
86+
writeln(" " ~ section.body);
87+
foreach (term; section.terms()) {
88+
writeln(
89+
" "
90+
~ term[0]
91+
~ ' '.repeat(max(2, longest - term[0].stripAnsi().length + 2)).array()
92+
~ term[1]
93+
);
94+
}
95+
if (i + 1 < _sections.length)
96+
writeln();
97+
}
98+
}
99+
}

src/cmd/option.d

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module cmd.option;
22

33
import std.string;
44

5+
import cmd.ansi;
56
import cmd.flag;
67

78
/** Represents a command-line option with a parameter. */
@@ -81,19 +82,47 @@ public final class Option : Flag {
8182
);
8283
}
8384

85+
/**
86+
* Returns formatted name with parameter placeholder.
87+
*
88+
* Params:
89+
* colors = Whether to use colors.
90+
*/
91+
public string formattedName(bool colors) const nothrow @safe {
92+
return super.formattedName() ~ " " ~
93+
(colors
94+
? required
95+
? "<".brightBlack() ~ paramName.dim() ~ ">".brightBlack()
96+
: "[".brightBlack() ~ paramName.dim() ~ "]".brightBlack()
97+
: required
98+
? "<" ~ paramName ~ ">"
99+
: "[" ~ paramName ~ "]"
100+
);
101+
}
102+
84103
/** Returns formatted name with parameter placeholder. */
85104
public override string formattedName() const nothrow @safe {
86-
return super.formattedName() ~ " " ~ (required ? "<" ~ paramName ~ ">" : "[" ~ paramName ~ "]");
105+
return formattedName(false);
87106
}
88107

89-
/** Returns formatted name padded with spaces if there is no short option. */
90-
public override string paddedName() const nothrow @safe {
91-
auto name = formattedName();
108+
/**
109+
* Returns formatted name padded with spaces if there is no short option.
110+
*
111+
* Params:
112+
* colors = Whether to use colors.
113+
*/
114+
public string paddedName(bool colors) const nothrow @safe {
115+
auto name = formattedName(colors);
92116
if (shortName !is null)
93117
return name;
94118
else
95119
return " " ~ name;
96120
}
121+
122+
/** Returns formatted name padded with spaces if there is no short option. */
123+
public override string paddedName() const nothrow @safe {
124+
return paddedName(false);
125+
}
97126

98127
public override hash_t toHash() const nothrow @safe {
99128
return formattedName().hashOf();

src/cmd/package.d

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
module cmd;
22

3+
public import cmd.ansi;
34
public import cmd.argument;
45
public import cmd.command;
6+
public import cmd.document;
57
public import cmd.flag;
68
public import cmd.help_command;
79
public import cmd.option;
810
public import cmd.parsed_args;
9-
public import cmd.program;
11+
public import cmd.program;

0 commit comments

Comments
 (0)