Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues/21 #26

Merged
merged 3 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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));
});
});