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

Add auto colors functionality #296

Merged
merged 12 commits into from
Oct 10, 2022
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ Prefix styling
- Available modifiers: reset, bold, dim, italic,
underline, inverse, hidden, strikethrough
- Available colors: black, red, green, yellow, blue,
magenta, cyan, white, gray
or any hex values for colors, eg #23de43
magenta, cyan, white, gray,
any hex values for colors (e.g. #23de43) or auto for
an automatically picked color
- Available background colors: bgBlack, bgRed,
bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite
See https://www.npmjs.com/package/chalk for more
Expand Down Expand Up @@ -247,6 +248,14 @@ Examples:
$ concurrently --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold"
"http-server" "npm run watch"

- Auto varying colored prefixes

$ concurrently -c "auto" "npm run watch" "http-server"

- Mixing auto and manual colored prefixes

$ concurrently -c "red,auto" "npm run watch" "http-server" "echo hello"

- Configuring via environment variables with CONCURRENTLY_ prefix

$ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true concurrently "echo
Expand Down Expand Up @@ -321,12 +330,12 @@ For more details, visit https://github.com/open-cli-tools/concurrently
- `prefix`: the prefix type to use when logging processes output.
Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
Default: the name of the process, or its index if no name is set.
- `prefixColors`: a list of colors as supported by [chalk](https://www.npmjs.com/package/chalk).
If concurrently would run more commands than there are colors, the last color is repeated.
- `prefixColors`: a list of colors as supported by [chalk](https://www.npmjs.com/package/chalk) or `auto` for an automatically picked color.
If concurrently would run more commands than there are colors, the last color is repeated, unless if the last color value is `auto` which means following colors are automatically picked to vary.
Prefix colors specified per-command take precedence over this list.
- `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`
- `raw`: whether raw mode should be used, meaning strictly process output will
be logged, without any prefixes, colouring or extra stuff.
be logged, without any prefixes, coloring or extra stuff.
- `successCondition`: the condition to consider the run was successful.
If `first`, only the first process to exit will make up the success of the run; if `last`, the last process that exits will determine whether the run succeeds.
Anything else means all processes should exit successfully.
Expand Down
6 changes: 3 additions & 3 deletions bin/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const args = yargs(argsBeforeSep)
'and concurrently coloring.',
type: 'boolean',
},
// This one is provided for free. Chalk reads this itself and removes colours.
// This one is provided for free. Chalk reads this itself and removes colors.
// https://www.npmjs.com/package/chalk#chalksupportscolor
'no-color': {
describe: 'Disables colors from logging',
Expand Down Expand Up @@ -126,8 +126,8 @@ const args = yargs(argsBeforeSep)
'Comma-separated list of chalk colors to use on prefixes. ' +
'If there are more commands than colors, the last color will be repeated.\n' +
'- Available modifiers: reset, bold, dim, italic, underline, inverse, hidden, strikethrough\n' +
'- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray \n' +
'or any hex values for colors, eg #23de43\n' +
'- Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray, \n' +
'any hex values for colors (e.g. #23de43) or auto for an automatically picked color\n' +
'- Available background colors: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite\n' +
'See https://www.npmjs.com/package/chalk for more information.',
default: defaults.prefixColors,
Expand Down
8 changes: 8 additions & 0 deletions bin/epilogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ const examples = [
example:
'$ $0 --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold" "http-server" "npm run watch"',
},
{
description: 'Auto varying colored prefixes',
example: '$ $0 -c "auto" "npm run watch" "http-server"',
},
{
description: 'Mixing auto and manual colored prefixes',
example: '$ $0 -c "red,auto" "npm run watch" "http-server" "echo hello"',
},
{
description: 'Configuring via environment variables with CONCURRENTLY_ prefix',
example:
Expand Down
2 changes: 1 addition & 1 deletion src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface CommandInfo {
cwd?: string;

/**
* Color to use on prefix of command.
* Color to use on prefix of the command.
*/
prefixColor?: string;
}
Expand Down
8 changes: 4 additions & 4 deletions src/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FlowController } from './flow-control/flow-controller';
import { getSpawnOpts } from './get-spawn-opts';
import { Logger } from './logger';
import { OutputWriter } from './output-writer';
import { PrefixColorSelector } from './prefix-color-selector';

const defaults: ConcurrentlyOptions = {
spawn,
Expand Down Expand Up @@ -131,6 +132,8 @@ export function concurrently(

const options = _.defaults(baseOptions, defaults);

const prefixColorSelector = new PrefixColorSelector(options.prefixColors);

const commandParsers: CommandParser[] = [
new StripQuotes(),
new ExpandNpmShortcut(),
Expand All @@ -141,17 +144,14 @@ export function concurrently(
commandParsers.push(new ExpandArguments(options.additionalArguments));
}

let lastColor = '';
let commands = _(baseCommands)
.map(mapToCommandInfo)
.flatMap((command) => parseCommand(command, commandParsers))
.map((command, index) => {
// Use documented behaviour of repeating last color when specifying more commands than colors
lastColor = (options.prefixColors && options.prefixColors[index]) || lastColor;
return new Command(
{
index,
prefixColor: lastColor,
prefixColor: prefixColorSelector.getNextColor(),
...command,
},
getSpawnOpts({
Expand Down
2 changes: 1 addition & 1 deletion src/logger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Logger } from './logger';
let emitSpy: jest.SpyInstance;

beforeEach(() => {
// Force chalk to use colours, otherwise tests may pass when they were supposed to be failing.
// Force chalk to use colors, otherwise tests may pass when they were supposed to be failing.
chalk.level = 3;
});

Expand Down
173 changes: 173 additions & 0 deletions src/prefix-color-selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import chalk from 'chalk';

import { PrefixColorSelector } from './prefix-color-selector';

afterEach(() => {
jest.restoreAllMocks();
});

describe('#getNextColor', function () {
const customTests: Record<
string,
{
acceptableConsoleColors?: Array<keyof typeof chalk>;
customColors?: string[];
expectedColors: string[];
}
> = {
'does not produce a color if prefixColors empty': {
customColors: [],
expectedColors: ['', '', ''],
},
'does not produce a color if prefixColors undefined': {
expectedColors: ['', '', ''],
},
'uses user defined prefix colors only, if no auto is used': {
customColors: ['red', 'green', 'blue'],
expectedColors: [
'red',
'green',
'blue',

// Uses last color if last color is not "auto"
'blue',
'blue',
'blue',
],
},
'picks varying colors when user defines an auto color': {
acceptableConsoleColors: ['green', 'blue'],
customColors: [
'red',
'green',
'auto',
'green',
'auto',
'green',
'auto',
'blue',
'auto',
'orange',
],
expectedColors: [
// Custom colors
'red',
'green',
'blue', // Picks auto color "blue", not repeating consecutive "green" color
'green', // Manual
'blue', // Auto picks "blue" not to repeat last
'green', // Manual
'blue', // Auto picks "blue" again not to repeat last
'blue', // Manual
'green', // Auto picks "green" again not to repeat last
'orange',

// Uses last color if last color is not "auto"
'orange',
'orange',
'orange',
],
},
'uses user defined colors then recurring auto colors without repeating consecutive colors':
{
acceptableConsoleColors: ['green', 'blue'],
customColors: ['red', 'green', 'auto'],
expectedColors: [
// Custom colors
'red',
'green',

// Picks auto colors, not repeating consecutive "green" color
'blue',
'green',
'blue',
'green',
],
},
'can sometimes produce consecutive colors': {
acceptableConsoleColors: ['green', 'blue'],
customColors: ['blue', 'auto'],
expectedColors: [
// Custom colors
'blue',

// Picks auto colors
'green',
// Does not repeat custom colors for initial auto colors, i.e. does not use "blue" again so soon
'green', // Consecutive color picked, however practically there would be a lot of colors that need to be set in a particular order for this to occur
'blue',
'green',
'blue',
'green',
'blue',
],
},
'considers the Bright variants of colors equal to the normal colors to avoid similar colors':
{
acceptableConsoleColors: ['greenBright', 'blueBright', 'green', 'blue', 'magenta'],
customColors: ['green', 'blue', 'auto'],
expectedColors: [
// Custom colors
'green',
'blue',

// Picks auto colors, not repeating green and blue colors and variants initially
'magenta',

// Picks auto colors
'greenBright',
'blueBright',
'green',
'blue',
'magenta',
],
},
};
it.each(Object.entries(customTests))(
'%s',
(_, { acceptableConsoleColors, customColors, expectedColors }) => {
if (acceptableConsoleColors) {
jest.spyOn(PrefixColorSelector, 'ACCEPTABLE_CONSOLE_COLORS', 'get').mockReturnValue(
acceptableConsoleColors
);
}
const prefixColorSelector = new PrefixColorSelector(customColors);
const prefixColorSelectorValues = expectedColors.map(() =>
prefixColorSelector.getNextColor()
);

expect(prefixColorSelectorValues).toEqual(expectedColors);
}
);

const autoTests = {
'does not repeat consecutive colors when last prefixColor is auto': false,
'handles when more individual auto prefixColors exist than acceptable console colors': true,
};
it.each(Object.entries(autoTests))('%s', (_, map) => {
// Pick auto colors over 2 sets
const expectedColors: string[] = [
...PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS,
...PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS,
];

const prefixColorSelector = new PrefixColorSelector(
map ? expectedColors.map(() => 'auto') : ['auto']
);

expectedColors.reduce((previousColor, currentExpectedColor) => {
const actualSelectedColor = prefixColorSelector.getNextColor();
expect(actualSelectedColor).not.toBe(previousColor); // No consecutive colors
expect(actualSelectedColor).toBe(currentExpectedColor); // Expected color
return actualSelectedColor;
}, '');
});
});

describe('PrefixColorSelector#ACCEPTABLE_CONSOLE_COLORS', () => {
it('has more than 1 auto color defined', () => {
// (!) The current implementation is based on the assumption that 'ACCEPTABLE_CONSOLE_COLORS'
// always has more than one entry, which is what we enforce via this test
expect(PrefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.length).toBeGreaterThan(1);
});
});