Skip to content

Commit

Permalink
Issues/21 (#26)
Browse files Browse the repository at this point in the history
A new `omitStyles` function was added in `packages/tsargp/lib/styles.ts`
to check the necessary environment variables:

- `FORCE_COLOR` to emit styles even when the output is being redirected
- `NO_COLOR` to omit styles if `FORCE_COLOR` is not set
- `TERM=dumb` to omit styles, if none of the previous variables was set

Note that, if the terminal width is zero (or undefined), and
`FORCE_COLOR` is not set, styles will be omitted regardless of the
values of the other two variables.

A new parameter was added to the `wrap` method of both `ErrorMessage`
and `HelpMessage` classes:

- `emitStyles` - `boolean` - true if styles should be emitted - defaults
to `!omitStyles(width)`

Likewise for the `wrapToWidth` method of the `TerminalString` class,
except that it is required in this case.

Closes #21
  • Loading branch information
disog committed Mar 13, 2024
1 parent 8dd762b commit e039ba2
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 47 deletions.
5 changes: 0 additions & 5 deletions .changeset/commit.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
function getAddMessage(changeset) {
return `[changeset] ${changeset.summary}`;
}

function getVersionMessage(releasePlan) {
const releases = releasePlan.releases.filter((release) => release.type !== 'none');
const lines = [`[release] Releasing ${releases.length} package(s)`];
Expand All @@ -12,6 +8,5 @@ function getVersionMessage(releasePlan) {
}

exports['default'] = {
getAddMessage,
getVersionMessage,
};
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "trulysimple/tsargp" }],
"commit": ["./commit.cjs", {}],
"commit": "./commit.cjs",
"fixed": [],
"linked": [],
"access": "public",
Expand Down
6 changes: 6 additions & 0 deletions .changeset/tiny-tables-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'tsargp': patch
---

You can now set the `NO_COLOR` environment variable to _omit_ styles from error and help messages.
You can also set `FORCE_COLOR` to _emit_ styles even when the output is being redirected.
2 changes: 1 addition & 1 deletion packages/tsargp/examples/demo.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default {
footer: `
MIT License.
Copyright (c) 2024
${style(tf.bold, tf.italic, fg.cyan)}TrulySimple${style(tf.clear)}
${style(tf.bold, tf.italic)}TrulySimple${style(tf.clear)}
Report a bug:
${style(tf.faint)}https://github.com/trulysimple/tsargp/issues${style(tf.clear)}`,
Expand Down
4 changes: 2 additions & 2 deletions packages/tsargp/lib/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ class ParserLoop {
} catch (err) {
// do not propagate errors during completion
if (!this.completing) {
if (suggestName(option)) {
handleUnknown(this.validator, value, err as ErrorMessage);
if (err instanceof ErrorMessage && suggestName(option)) {
handleUnknown(this.validator, value, err);
}
throw err;
}
Expand Down
37 changes: 26 additions & 11 deletions packages/tsargp/lib/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,10 @@ class TerminalString {
* @param result The resulting strings to append to
* @param column The current terminal column
* @param width The desired terminal width (or zero to avoid wrapping)
* @param emitStyles True if styles should be emitted
* @returns The updated terminal column
*/
wrapToWidth(result: Array<string>, column: number, width?: number): number {
wrapToWidth(result: Array<string>, column: number, width: number, emitStyles: boolean): number {
function shortenLine() {
while (result.length && column > start) {
const last = result[result.length - 1];
Expand Down Expand Up @@ -409,12 +410,12 @@ class TerminalString {
}
const len = this.lengths[i];
if (!len) {
if (width) {
if (emitStyles) {
result.push(str);
}
continue;
}
if (!width) {
if (!emitStyles) {
str = str.replace(regex.styles, '');
}
if (!column) {
Expand Down Expand Up @@ -456,13 +457,14 @@ class ErrorMessage extends Error {

/**
* Wraps the error message to a specified width.
* @param width The terminal width (in number of columns)
* @param width The terminal width (or zero to avoid wrapping)
* @param emitStyles True if styles should be emitted
* @returns The message to be printed on a terminal
*/
wrap(width = process.stderr.columns): string {
wrap(width = process.stderr.columns ?? 0, emitStyles = !omitStyles(width)): string {
const result = new Array<string>();
this.str.wrapToWidth(result, 0, width);
if (width) {
this.str.wrapToWidth(result, 0, width, emitStyles);
if (emitStyles) {
result.push(style(tf.clear));
}
return result.join('');
Expand All @@ -482,16 +484,17 @@ class HelpMessage extends Array<TerminalString> {

/**
* Wraps the help message to a specified width.
* @param width The terminal width (in number of columns)
* @param width The terminal width (or zero to avoid wrapping)
* @param emitStyles True if styles should be emitted
* @returns The message to be printed on a terminal
*/
wrap(width = process.stdout.columns): string {
wrap(width = process.stdout.columns ?? 0, emitStyles = !omitStyles(width)): string {
const result = new Array<string>();
let column = 0;
for (const str of this) {
column = str.wrapToWidth(result, column, width);
column = str.wrapToWidth(result, column, width, emitStyles);
}
if (width) {
if (emitStyles) {
result.push(style(tf.clear));
}
return result.join('');
Expand All @@ -501,6 +504,18 @@ class HelpMessage extends Array<TerminalString> {
//--------------------------------------------------------------------------------------------------
// Functions
//--------------------------------------------------------------------------------------------------
/**
* @param width The terminal width (in number of columns)
* @returns True if styles should be omitted from terminal strings
* @see https://clig.dev/#output
*/
function omitStyles(width: number): boolean {
return (
!process.env['FORCE_COLOR'] &&
(!width || !!process.env['NO_COLOR'] || process.env['TERM'] === 'dumb')
);
}

/**
* Creates a CSI sequence.
* @template P The type of the sequence parameter
Expand Down
87 changes: 60 additions & 27 deletions packages/tsargp/test/styles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,158 +160,187 @@ describe('TerminalString', () => {
it('should not wrap', () => {
const str = new TerminalString();
const result = new Array<string>();
str.splitText('abc def').wrapToWidth(result, 0);
str.splitText('abc def').wrapToWidth(result, 0, 0, false);
expect(result).toEqual(['abc', ' def']);
});

it('should preserve indentation', () => {
const str = new TerminalString(2);
const result = new Array<string>();
str.addWord('abc').wrapToWidth(result, 0);
str.addWord('abc').wrapToWidth(result, 0, 0, false);
expect(result).toEqual([' ', 'abc']);
});

it('should not preserve indentation if the string is empty', () => {
const str = new TerminalString(2);
const result = new Array<string>();
str.wrapToWidth(result, 0);
str.wrapToWidth(result, 0, 0, false);
expect(result).toEqual([]);
});

it('should not preserve indentation if the string starts with a line break', () => {
const str = new TerminalString(2);
const result = new Array<string>();
str.addBreaks(1).wrapToWidth(result, 0);
str.addBreaks(1).wrapToWidth(result, 0, 0, false);
expect(result).toEqual(['\n']);
});

it('should shorten the current line (1)', () => {
const str = new TerminalString(0);
const result = [' '];
str.splitText('abc def').wrapToWidth(result, 2);
str.splitText('abc def').wrapToWidth(result, 2, 0, false);
expect(result).toEqual(['abc', ' def']);
});

it('should shorten the current line (2)', () => {
const str = new TerminalString(0);
const result = [' '];
str.splitText('abc def').wrapToWidth(result, 2);
str.splitText('abc def').wrapToWidth(result, 2, 0, false);
expect(result).toEqual([' ', 'abc', ' def']);
});

it('should not shorten the current line if the string is empty', () => {
const str = new TerminalString(0);
const result = [' '];
str.wrapToWidth(result, 2);
str.wrapToWidth(result, 2, 0, false);
expect(result).toEqual([' ']);
});

it('should not shorten the current line if the string starts with a line break', () => {
const str = new TerminalString(0);
const result = [' '];
str.addBreaks(1).wrapToWidth(result, 2);
str.addBreaks(1).wrapToWidth(result, 2, 0, false);
expect(result).toEqual([' ', '\n']);
});

it('should preserve line breaks', () => {
const str = new TerminalString(0);
const result = new Array<string>();
str.splitText('abc\n\ndef').wrapToWidth(result, 0);
str.splitText('abc\n\ndef').wrapToWidth(result, 0, 0, false);
expect(result).toEqual(['abc', '\n\n', 'def']);
});

it('should remove styles', () => {
it('should omit styles', () => {
const str = new TerminalString(0);
const result = new Array<string>();
str.splitText(`abc${style(tf.clear)} ${style(tf.clear)} def`).wrapToWidth(result, 0);
str
.splitText(`abc${style(tf.clear)} ${style(tf.clear)} def`)
.wrapToWidth(result, 0, 0, false);
expect(result).toEqual(['abc', ' def']);
});

it('should emit styles', () => {
const str = new TerminalString();
const result = new Array<string>();
str
.splitText(`abc${style(tf.clear)} ${style(tf.clear)} def`)
.wrapToWidth(result, 0, 0, true);
expect(result).toEqual(['abc' + style(tf.clear), style(tf.clear), ' def']);
});
});

describe('when a width is provided', () => {
it('should wrap relative to the beginning when the largest word does not fit the width (1)', () => {
const str = new TerminalString(1);
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 0, 5);
str.splitText('abc largest').wrapToWidth(result, 0, 5, false);
expect(result).toEqual(['abc', '\nlargest']);
});

it('should wrap relative to the beginning when the largest word does not fit the width (2)', () => {
const str = new TerminalString(1);
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 1, 5);
str.splitText('abc largest').wrapToWidth(result, 1, 5, false);
expect(result).toEqual(['\n', 'abc', '\nlargest']);
});

it('should wrap relative to the beginning when the largest word does not fit the width (3)', () => {
const str = new TerminalString(1);
const result = new Array<string>();
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 1, 5);
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 1, 5, false);
expect(result).toEqual(['\n', 'abc', '\nlargest']);
});

it('should wrap with a move sequence when the largest word fits the width (1)', () => {
const str = new TerminalString(1);
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 1, 15);
str.splitText('abc largest').wrapToWidth(result, 1, 15, false);
expect(result).toEqual(['abc', ' largest']);
});

it('should wrap with a move sequence when the largest word fits the width (2)', () => {
const str = new TerminalString(1);
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 2, 15);
str.splitText('abc largest').wrapToWidth(result, 2, 15, false);
expect(result).toEqual([move(2, mv.cha), 'abc', ' largest']);
});

it('should wrap with a move sequence when the largest word fits the width (3)', () => {
const str = new TerminalString(2);
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 1, 15);
str.splitText('abc largest').wrapToWidth(result, 1, 15, false);
expect(result).toEqual([move(3, mv.cha), 'abc', ' largest']);
});

it('should wrap with a move sequence when the largest word fits the width (4)', () => {
const str = new TerminalString(1);
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 1, 10);
str.splitText('abc largest').wrapToWidth(result, 1, 10, false);
expect(result).toEqual(['abc', `\n${move(2, mv.cha)}largest`]);
});

it('should wrap with a move sequence when the largest word fits the width (5)', () => {
const str = new TerminalString();
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 0, 15);
str.splitText('abc largest').wrapToWidth(result, 0, 15, false);
expect(result).toEqual(['abc', ' largest']);
});

it('should wrap with a move sequence when the largest word fits the width (6)', () => {
const str = new TerminalString();
const result = new Array<string>();
str.splitText('abc largest').wrapToWidth(result, 1, 15);
str.splitText('abc largest').wrapToWidth(result, 1, 15, false);
expect(result).toEqual([move(1, mv.cha), 'abc', ' largest']);
});

it('should wrap with a move sequence when the largest word fits the width (7)', () => {
const str = new TerminalString();
const result = new Array<string>();
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 1, 15);
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 1, 15, false);
expect(result).toEqual(['\n', 'abc', ' largest']);
});

it('should wrap with a move sequence when the largest word fits the width (8)', () => {
const str = new TerminalString(1);
const result = new Array<string>();
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 2, 15);
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 2, 15, false);
expect(result).toEqual(['\n', `${move(2, mv.cha)}abc`, ' largest']);
});

it('should wrap with a move sequence when the largest word fits the width (9)', () => {
const str = new TerminalString(2);
const result = new Array<string>();
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 1, 15);
str.addBreaks(1).splitText('abc largest').wrapToWidth(result, 1, 15, false);
expect(result).toEqual(['\n', `${move(3, mv.cha)}abc`, ' largest']);
});

it('should omit styles', () => {
const str = new TerminalString(0);
const result = new Array<string>();
str
.splitText(`abc${style(tf.clear)} ${style(tf.clear)} def`)
.wrapToWidth(result, 0, 10, false);
expect(result).toEqual(['abc', ' def']);
});

it('should emit styles', () => {
const str = new TerminalString();
const result = new Array<string>();
str
.splitText(`abc${style(tf.clear)} ${style(tf.clear)} def`)
.wrapToWidth(result, 0, 10, true);
expect(result).toEqual(['abc' + style(tf.clear), style(tf.clear), ' def']);
});
});
});
});
Expand All @@ -322,8 +351,10 @@ describe('ErrorMessage', () => {
str.splitText('type script');
const err = new ErrorMessage(str);
expect(err.message).toMatch(/type script/);
expect(err.wrap(0)).toEqual('type script');
expect(err.wrap(11)).toEqual('type script' + style(tf.clear));
expect(err.wrap(0, false)).toEqual('type script');
expect(err.wrap(0, true)).toEqual('type script' + style(tf.clear));
expect(err.wrap(11, false)).toEqual('type script');
expect(err.wrap(11, true)).toEqual('type script' + style(tf.clear));
});

it('should be thrown and caught', () => {
Expand All @@ -343,7 +374,9 @@ describe('HelpMessage', () => {
const help = new HelpMessage();
help.push(str);
expect(help.toString()).toMatch(/type script/);
expect(help.wrap(0)).toEqual('type script');
expect(help.wrap(11)).toEqual('type script' + style(tf.clear));
expect(help.wrap(0, false)).toEqual('type script');
expect(help.wrap(0, true)).toEqual('type script' + style(tf.clear));
expect(help.wrap(11, false)).toEqual('type script');
expect(help.wrap(11, true)).toEqual('type script' + style(tf.clear));
});
});

0 comments on commit e039ba2

Please sign in to comment.