Skip to content

Commit 5629947

Browse files
authored
Support color ansi code sequences in custom help (#2251)
1 parent 5a79585 commit 5629947

17 files changed

+1405
-334
lines changed

Readme.md

+12
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,7 @@ You can configure the Help behaviour by modifying data properties and methods us
926926
The data properties are:
927927

928928
- `helpWidth`: specify the wrap width, useful for unit tests
929+
- `minWidthToWrap`: specify required width to allow wrapping (default 40)
929930
- `sortSubcommands`: sort the subcommands alphabetically
930931
- `sortOptions`: sort the options alphabetically
931932
- `showGlobalOptions`: show a section with the global options from the parent command(s)
@@ -941,6 +942,17 @@ program.configureHelp({
941942
});
942943
```
943944

945+
There are _style_ methods to add color to the help, like `styleTitle` and `styleOptionText`. There is built-in support for respecting
946+
environment variables for `NO_COLOR`, `FORCE_COLOR`, and `CLIFORCE_COLOR`.
947+
948+
Example file: [color-help.mjs](./examples/color-help.mjs)
949+
950+
Other help configuration examples:
951+
- [color-help-replacement.mjs](./examples/color-help-replacement.mjs)
952+
- [help-centered.mjs](./examples/help-centered.mjs)
953+
- [help-subcommands-usage.js](./examples/help-subcommands-usage.js)
954+
- [man-style-help.mjs](./examples/man-style-help.mjs)
955+
944956
## Custom event listeners
945957

946958
You can execute custom actions by listening to command and option events.

examples/color-help-replacement.mjs

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import stripAnsi from 'strip-ansi';
2+
import wrapAnsi from 'wrap-ansi';
3+
import {
4+
default as chalkStdOut,
5+
chalkStderr as chalkStdErr,
6+
supportsColor as supportsColorStdout,
7+
supportsColorStderr,
8+
} from 'chalk';
9+
import { Command, Help } from 'commander';
10+
11+
// Replace default color and wrapping support with Chalk packages as an example of
12+
// a deep replacement of format and style support.
13+
14+
// This example requires chalk and wrap-ansi and strip-ansi, and won't run
15+
// from a clone of Commander repo without installing them first.
16+
//
17+
// For example using npm:
18+
// npm install chalk wrap-ansi strip-ansi
19+
20+
class MyHelp extends Help {
21+
constructor() {
22+
super();
23+
this.chalk = chalkStdOut;
24+
}
25+
26+
prepareContext(contextOptions) {
27+
super.prepareContext(contextOptions);
28+
if (contextOptions?.error) {
29+
this.chalk = chalkStdErr;
30+
}
31+
}
32+
33+
displayWidth(str) {
34+
return stripAnsi(str).length; // use imported package
35+
}
36+
37+
boxWrap(str, width) {
38+
return wrapAnsi(str, width, { hard: true }); // use imported package
39+
}
40+
41+
styleTitle(str) {
42+
return this.chalk.bold(str);
43+
}
44+
styleCommandText(str) {
45+
return this.chalk.cyan(str);
46+
}
47+
styleCommandDescription(str) {
48+
return this.chalk.magenta(str);
49+
}
50+
styleItemDescription(str) {
51+
return this.chalk.italic(str);
52+
}
53+
styleOptionText(str) {
54+
return this.chalk.green(str);
55+
}
56+
styleArgumentText(str) {
57+
return this.chalk.yellow(str);
58+
}
59+
styleSubcommandText(str) {
60+
return this.chalk.blue(str);
61+
}
62+
}
63+
64+
class MyCommand extends Command {
65+
createCommand(name) {
66+
return new MyCommand(name);
67+
}
68+
createHelp() {
69+
return Object.assign(new MyHelp(), this.configureHelp());
70+
}
71+
}
72+
73+
const program = new MyCommand();
74+
75+
// Override the color detection to use Chalk's detection.
76+
// Chalk overrides color support based on the `FORCE_COLOR` environment variable,
77+
// and looks for --color and --no-color command-line options.
78+
// See https://github.com/chalk/chalk?tab=readme-ov-file#supportscolor
79+
//
80+
// In general we want stripColor() to be consistent with displayWidth().
81+
program.configureOutput({
82+
getOutHasColors: () => supportsColorStdout,
83+
getErrHasColors: () => supportsColorStderr,
84+
stripColor: (str) => stripAnsi(str),
85+
});
86+
87+
program.description('program description '.repeat(10));
88+
program
89+
.option('-s', 'short description')
90+
.option('--long <number>', 'long description '.repeat(10))
91+
.option('--color', 'force color output') // implemented by chalk
92+
.option('--no-color', 'disable color output'); // implemented by chalk
93+
94+
program.addHelpText('after', (context) => {
95+
const chalk = context.error ? chalkStdErr : chalkStdOut;
96+
return chalk.italic('\nThis is additional help text.');
97+
});
98+
99+
program.command('esses').description('sssss '.repeat(33));
100+
101+
program
102+
.command('print')
103+
.description('print files')
104+
.argument('<files...>', 'files to queue for printing')
105+
.option('--double-sided', 'print on both sides');
106+
107+
program.parse();
108+
109+
// Try the following (after installing the required packages):
110+
// node color-help-replacement.mjs --help
111+
// node color-help-replacement.mjs --no-color help
112+
// FORCE_COLOR=0 node color-help-replacement.mjs help
113+
// node color-help-replacement.mjs help print

examples/color-help.mjs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { styleText } from 'node:util'; // from node v20.12.0
2+
import { Command } from 'commander';
3+
4+
// Customise colours and styles for help output.
5+
6+
const program = new Command();
7+
8+
program.configureHelp({
9+
styleTitle: (str) => styleText('bold', str),
10+
styleCommandText: (str) => styleText('cyan', str),
11+
styleCommandDescription: (str) => styleText('magenta', str),
12+
styleItemDescription: (str) => styleText('italic', str),
13+
styleOptionText: (str) => styleText('green', str),
14+
styleArgumentText: (str) => styleText('yellow', str),
15+
styleSubcommandText: (str) => styleText('blue', str),
16+
});
17+
18+
program.description('program description '.repeat(10));
19+
program
20+
.option('-s', 'short description')
21+
.option('--long <number>', 'long description '.repeat(10));
22+
23+
program.addHelpText(
24+
'after',
25+
styleText('italic', '\nThis is additional help text.'),
26+
);
27+
28+
program.command('esses').description('sssss '.repeat(33));
29+
30+
program
31+
.command('print')
32+
.description('print files')
33+
.argument('<files...>', 'files to queue for printing')
34+
.option('--double-sided', 'print on both sides');
35+
36+
program.parse();
37+
38+
// Try the following:
39+
// node color-help.mjs --help
40+
// NO_COLOR=1 node color-help.mjs --help
41+
// node color-help.mjs help print

examples/help-centered.mjs

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Command, Help } from 'commander';
2+
3+
// Right-justify the terms in the help output.
4+
// Setup a subclass so we can do simple tweak of formatItem.
5+
6+
class MyHelp extends Help {
7+
formatItem(term, termWidth, description, helper) {
8+
// Pre-pad the term at start instead of end.
9+
const paddedTerm = term.padStart(
10+
termWidth + term.length - helper.displayWidth(term),
11+
);
12+
13+
return super.formatItem(paddedTerm, termWidth, description, helper);
14+
}
15+
}
16+
17+
class MyCommand extends Command {
18+
createCommand(name) {
19+
return new MyCommand(name);
20+
}
21+
createHelp() {
22+
return Object.assign(new MyHelp(), this.configureHelp());
23+
}
24+
}
25+
26+
const program = new MyCommand();
27+
28+
program.configureHelp({ MyCommand });
29+
30+
program
31+
.option('-s', 'short flag')
32+
.option('-f, --flag', 'short and long flag')
33+
.option('--long <number>', 'long flag');
34+
35+
program.command('compile').alias('c').description('compile something');
36+
37+
program.command('run', 'run something').command('print', 'print something');
38+
39+
program.parse();
40+
41+
// Try the following:
42+
// node help-centered.mjs --help

examples/man-style-help.mjs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Command } from 'commander';
2+
3+
// Layout the help like a man page, with the description starting on the next line.
4+
5+
function formatItem(term, termWidth, description, helper) {
6+
const termIndent = 2;
7+
const descIndent = 6;
8+
const helpWidth = this.helpWidth || 80;
9+
10+
// No need to pad term as on its own line.
11+
const lines = [' '.repeat(termIndent) + term];
12+
13+
if (description) {
14+
const boxText = helper.boxWrap(description, helpWidth - 6);
15+
const descIndentText = ' '.repeat(descIndent);
16+
lines.push(
17+
descIndentText + boxText.split('\n').join('\n' + descIndentText),
18+
);
19+
}
20+
21+
lines.push('');
22+
return lines.join('\n');
23+
}
24+
25+
const program = new Command();
26+
27+
program.configureHelp({ formatItem });
28+
29+
program
30+
.option('-s', 'short flag')
31+
.option('-f, --flag', 'short and long flag')
32+
.option('--long <number>', 'l '.repeat(100));
33+
34+
program
35+
.command('sub1', 'sssss '.repeat(33))
36+
.command('sub2', 'subcommand 2 description');
37+
38+
program.parse();
39+
40+
// Try the following:
41+
// node man-style-help.mjs --help

0 commit comments

Comments
 (0)