Skip to content

Commit

Permalink
Support color ansi code sequences in custom help (#2251)
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowspawn authored Nov 21, 2024
1 parent 5a79585 commit 5629947
Show file tree
Hide file tree
Showing 17 changed files with 1,405 additions and 334 deletions.
12 changes: 12 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,7 @@ You can configure the Help behaviour by modifying data properties and methods us
The data properties are:

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

There are _style_ methods to add color to the help, like `styleTitle` and `styleOptionText`. There is built-in support for respecting
environment variables for `NO_COLOR`, `FORCE_COLOR`, and `CLIFORCE_COLOR`.

Example file: [color-help.mjs](./examples/color-help.mjs)

Other help configuration examples:
- [color-help-replacement.mjs](./examples/color-help-replacement.mjs)
- [help-centered.mjs](./examples/help-centered.mjs)
- [help-subcommands-usage.js](./examples/help-subcommands-usage.js)
- [man-style-help.mjs](./examples/man-style-help.mjs)

## Custom event listeners

You can execute custom actions by listening to command and option events.
Expand Down
113 changes: 113 additions & 0 deletions examples/color-help-replacement.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import stripAnsi from 'strip-ansi';
import wrapAnsi from 'wrap-ansi';
import {
default as chalkStdOut,
chalkStderr as chalkStdErr,
supportsColor as supportsColorStdout,
supportsColorStderr,
} from 'chalk';
import { Command, Help } from 'commander';

// Replace default color and wrapping support with Chalk packages as an example of
// a deep replacement of format and style support.

// This example requires chalk and wrap-ansi and strip-ansi, and won't run
// from a clone of Commander repo without installing them first.
//
// For example using npm:
// npm install chalk wrap-ansi strip-ansi

class MyHelp extends Help {
constructor() {
super();
this.chalk = chalkStdOut;
}

prepareContext(contextOptions) {
super.prepareContext(contextOptions);
if (contextOptions?.error) {
this.chalk = chalkStdErr;
}
}

displayWidth(str) {
return stripAnsi(str).length; // use imported package
}

boxWrap(str, width) {
return wrapAnsi(str, width, { hard: true }); // use imported package
}

styleTitle(str) {
return this.chalk.bold(str);
}
styleCommandText(str) {
return this.chalk.cyan(str);
}
styleCommandDescription(str) {
return this.chalk.magenta(str);
}
styleItemDescription(str) {
return this.chalk.italic(str);
}
styleOptionText(str) {
return this.chalk.green(str);
}
styleArgumentText(str) {
return this.chalk.yellow(str);
}
styleSubcommandText(str) {
return this.chalk.blue(str);
}
}

class MyCommand extends Command {
createCommand(name) {
return new MyCommand(name);
}
createHelp() {
return Object.assign(new MyHelp(), this.configureHelp());
}
}

const program = new MyCommand();

// Override the color detection to use Chalk's detection.
// Chalk overrides color support based on the `FORCE_COLOR` environment variable,
// and looks for --color and --no-color command-line options.
// See https://github.com/chalk/chalk?tab=readme-ov-file#supportscolor
//
// In general we want stripColor() to be consistent with displayWidth().
program.configureOutput({
getOutHasColors: () => supportsColorStdout,
getErrHasColors: () => supportsColorStderr,
stripColor: (str) => stripAnsi(str),
});

program.description('program description '.repeat(10));
program
.option('-s', 'short description')
.option('--long <number>', 'long description '.repeat(10))
.option('--color', 'force color output') // implemented by chalk
.option('--no-color', 'disable color output'); // implemented by chalk

program.addHelpText('after', (context) => {
const chalk = context.error ? chalkStdErr : chalkStdOut;
return chalk.italic('\nThis is additional help text.');
});

program.command('esses').description('sssss '.repeat(33));

program
.command('print')
.description('print files')
.argument('<files...>', 'files to queue for printing')
.option('--double-sided', 'print on both sides');

program.parse();

// Try the following (after installing the required packages):
// node color-help-replacement.mjs --help
// node color-help-replacement.mjs --no-color help
// FORCE_COLOR=0 node color-help-replacement.mjs help
// node color-help-replacement.mjs help print
41 changes: 41 additions & 0 deletions examples/color-help.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { styleText } from 'node:util'; // from node v20.12.0
import { Command } from 'commander';

// Customise colours and styles for help output.

const program = new Command();

program.configureHelp({
styleTitle: (str) => styleText('bold', str),
styleCommandText: (str) => styleText('cyan', str),
styleCommandDescription: (str) => styleText('magenta', str),
styleItemDescription: (str) => styleText('italic', str),
styleOptionText: (str) => styleText('green', str),
styleArgumentText: (str) => styleText('yellow', str),
styleSubcommandText: (str) => styleText('blue', str),
});

program.description('program description '.repeat(10));
program
.option('-s', 'short description')
.option('--long <number>', 'long description '.repeat(10));

program.addHelpText(
'after',
styleText('italic', '\nThis is additional help text.'),
);

program.command('esses').description('sssss '.repeat(33));

program
.command('print')
.description('print files')
.argument('<files...>', 'files to queue for printing')
.option('--double-sided', 'print on both sides');

program.parse();

// Try the following:
// node color-help.mjs --help
// NO_COLOR=1 node color-help.mjs --help
// node color-help.mjs help print
42 changes: 42 additions & 0 deletions examples/help-centered.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Command, Help } from 'commander';

// Right-justify the terms in the help output.
// Setup a subclass so we can do simple tweak of formatItem.

class MyHelp extends Help {
formatItem(term, termWidth, description, helper) {
// Pre-pad the term at start instead of end.
const paddedTerm = term.padStart(
termWidth + term.length - helper.displayWidth(term),
);

return super.formatItem(paddedTerm, termWidth, description, helper);
}
}

class MyCommand extends Command {
createCommand(name) {
return new MyCommand(name);
}
createHelp() {
return Object.assign(new MyHelp(), this.configureHelp());
}
}

const program = new MyCommand();

program.configureHelp({ MyCommand });

program
.option('-s', 'short flag')
.option('-f, --flag', 'short and long flag')
.option('--long <number>', 'long flag');

program.command('compile').alias('c').description('compile something');

program.command('run', 'run something').command('print', 'print something');

program.parse();

// Try the following:
// node help-centered.mjs --help
41 changes: 41 additions & 0 deletions examples/man-style-help.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Command } from 'commander';

// Layout the help like a man page, with the description starting on the next line.

function formatItem(term, termWidth, description, helper) {
const termIndent = 2;
const descIndent = 6;
const helpWidth = this.helpWidth || 80;

// No need to pad term as on its own line.
const lines = [' '.repeat(termIndent) + term];

if (description) {
const boxText = helper.boxWrap(description, helpWidth - 6);
const descIndentText = ' '.repeat(descIndent);
lines.push(
descIndentText + boxText.split('\n').join('\n' + descIndentText),
);
}

lines.push('');
return lines.join('\n');
}

const program = new Command();

program.configureHelp({ formatItem });

program
.option('-s', 'short flag')
.option('-f, --flag', 'short and long flag')
.option('--long <number>', 'l '.repeat(100));

program
.command('sub1', 'sssss '.repeat(33))
.command('sub2', 'subcommand 2 description');

program.parse();

// Try the following:
// node man-style-help.mjs --help
Loading

0 comments on commit 5629947

Please sign in to comment.