Skip to content

Commit

Permalink
Add auto colors functionality (#296)
Browse files Browse the repository at this point in the history
Co-authored-by: Pascal Jufer <[email protected]>
Co-authored-by: Gustavo Henke <[email protected]>
  • Loading branch information
3 people authored Oct 10, 2022
1 parent c095c08 commit b56fbdd
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 14 deletions.
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,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 @@ -250,6 +251,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 @@ -324,12 +333,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);
});
});
Loading

0 comments on commit b56fbdd

Please sign in to comment.