Skip to content

Commit

Permalink
add auto colors functionality to resolve #279
Browse files Browse the repository at this point in the history
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
  • Loading branch information
eliasm307 and paescuj committed Jun 21, 2022
1 parent 0980e5b commit f57afd7
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ concurrently can be used programmatically by using the API documented below:
- `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.
Prefix colors specified per-command take precedence over this list.
- `colors`: let colours be selected to vary automatically where not explicitly defined
- `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.
Expand Down
7 changes: 7 additions & 0 deletions bin/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ const args = yargs(argsBeforeSep)
default: defaults.prefixColors,
type: 'string',
},
color: {
describe:
'Automatically adds varying prefix colors where commands do not have a prefix color defined',
default: defaults.color,
type: 'boolean',
},
'prefix-length': {
alias: 'l',
describe:
Expand Down Expand Up @@ -212,6 +218,7 @@ concurrently(
group: args.group,
prefix: args.prefix,
prefixColors: args.prefixColors.split(','),
color: args.color,
prefixLength: args.prefixLength,
restartDelay: args.restartAfter,
restartTries: args.restartTries,
Expand Down
13 changes: 9 additions & 4 deletions src/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ExpandArguments } from './command-parser/expand-arguments';
import { ExpandNpmShortcut } from './command-parser/expand-npm-shortcut';
import { ExpandNpmWildcard } from './command-parser/expand-npm-wildcard';
import { StripQuotes } from './command-parser/strip-quotes';
import { PrefixColorSelector } from './prefix-color-selector';
import { CompletionListener, SuccessCondition } from './completion-listener';
import { FlowController } from './flow-control/flow-controller';
import { getSpawnOpts } from './get-spawn-opts';
Expand Down Expand Up @@ -63,6 +64,11 @@ export type ConcurrentlyOptions = {
*/
prefixColors?: string[];

/**
* Automatically adds varying prefix colors where commands do not have a prefix color defined.
*/
color?: boolean;

/**
* Maximum number of commands to run at once.
*
Expand Down Expand Up @@ -130,6 +136,8 @@ export function concurrently(

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

const prefixColorSelector = new PrefixColorSelector(options.prefixColors, options.color);

const commandParsers: CommandParser[] = [
new StripQuotes(),
new ExpandNpmShortcut(),
Expand All @@ -140,18 +148,15 @@ 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(
Object.assign(
{
index,
prefixColor: lastColor,
prefixColor: prefixColorSelector.getNextColor(index),
},
command
),
Expand Down
5 changes: 5 additions & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export const prefix = '';
*/
export const prefixColors = 'reset';

/**
* Adding prefix colors automatically.
*/
export const color = false;

/**
* How many bytes we'll show on the command prefix.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export type ConcurrentlyOptions = BaseConcurrentlyOptions & {
*/
prefix?: string;

/**
* Automatically adds varying prefix colors where commands do not have a prefix color defined.
*/
color?: boolean;

/**
* How many characters should a prefix have at most, used when the prefix format is `command`.
*/
Expand Down Expand Up @@ -136,6 +141,7 @@ export default (
}),
],
prefixColors: options.prefixColors || [],
color: options.color || false,
additionalArguments: options.additionalArguments,
});
};
Expand Down
71 changes: 71 additions & 0 deletions src/prefix-color-selector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { PrefixColorSelector } from './prefix-color-selector';

it('does not produce a color if it should not', () => {
const prefixColorSelector = new PrefixColorSelector([], false);

let selectedColor = prefixColorSelector.getNextColor(0);
expect(selectedColor).toBe('');
selectedColor = prefixColorSelector.getNextColor(1);
expect(selectedColor).toBe('');
selectedColor = prefixColorSelector.getNextColor(2);
expect(selectedColor).toBe('');
});

it('uses user defined prefix colors only if not allowed to use auto colors', () => {
const prefixColorSelector = new PrefixColorSelector(['red', 'green', 'blue'], false);

let selectedColor = prefixColorSelector.getNextColor(0);
expect(selectedColor).toBe('red');
selectedColor = prefixColorSelector.getNextColor(1);
expect(selectedColor).toBe('green');
selectedColor = prefixColorSelector.getNextColor(2);
expect(selectedColor).toBe('blue');

// uses last color if no more user defined colors
selectedColor = prefixColorSelector.getNextColor(3);
expect(selectedColor).toBe('blue');
selectedColor = prefixColorSelector.getNextColor(4);
expect(selectedColor).toBe('blue');
});

it('uses user defined colors then recurring auto colors without repeating consecutive colors', () => {
const prefixColorSelector = new PrefixColorSelector(['red', 'green'], true);

jest.spyOn(prefixColorSelector, 'ACCEPTABLE_CONSOLE_COLORS', 'get').mockReturnValue([
'green',
'blue',
]);

let selectedColor = prefixColorSelector.getNextColor(0);
expect(selectedColor).toBe('red');
selectedColor = prefixColorSelector.getNextColor(1);
expect(selectedColor).toBe('green');

// auto colors now, does not repeat last user color of green
selectedColor = prefixColorSelector.getNextColor(2);
expect(selectedColor).toBe('blue');

selectedColor = prefixColorSelector.getNextColor(3);
expect(selectedColor).toBe('green');

selectedColor = prefixColorSelector.getNextColor(4);
expect(selectedColor).toBe('blue');
});

it('has more than 1 auto color defined', () => {
const prefixColorSelector = new PrefixColorSelector([], true);
// ! code assumes this always has more than one entry, so make sure
expect(prefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.length).toBeGreaterThan(1);
});

it('can use only auto colors and does not repeat consecutive colors', () => {
const prefixColorSelector = new PrefixColorSelector([], true);

let previousColor;
let selectedColor: string;
Array(prefixColorSelector.ACCEPTABLE_CONSOLE_COLORS.length * 2).forEach((_, index) => {
previousColor = selectedColor;
selectedColor = prefixColorSelector.getNextColor(index);
expect(selectedColor).not.toBe(previousColor);
});
});
77 changes: 77 additions & 0 deletions src/prefix-color-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import chalk from 'chalk';

export class PrefixColorSelector {
lastColor: string;
autoColors: string[];

get ACCEPTABLE_CONSOLE_COLORS() {
// Colors picked randomly, can be amended if required
return (
[
chalk.cyan,
chalk.yellow,
chalk.magenta,
chalk.grey,
chalk.bgBlueBright,
chalk.bgMagenta,
chalk.magentaBright,
chalk.bgBlack,
chalk.bgWhite,
chalk.bgCyan,
chalk.bgGreen,
chalk.bgYellow,
chalk.bgRed,
chalk.bgGreenBright,
chalk.bgGrey,
chalk.blueBright,
]
// Filter out duplicates
.filter((chalkColor, index, arr) => {
return arr.indexOf(chalkColor) === index;
})
.map(chalkColor => chalkColor.bold.toString())
);
}

constructor(private readonly prefixColors?: string[], private readonly color?: boolean) {}

getNextColor(index?: number) {
const cannotSelectColor = !this.prefixColors?.length && !this.color;
if (cannotSelectColor) {
return '';
}

const userDefinedColorForCurrentCommand =
this.prefixColors && typeof index === 'number' && this.prefixColors[index];

if (!this.color) {
// Use documented behaviour of repeating last color
// when specifying more commands than colors
this.lastColor = userDefinedColorForCurrentCommand || this.lastColor;
return this.lastColor;
}

// User preference takes priority if defined
if (userDefinedColorForCurrentCommand) {
this.lastColor = userDefinedColorForCurrentCommand;
return userDefinedColorForCurrentCommand;
}

// Auto selection requested and no user preference defined, select next auto color
if (!this.autoColors || !this.autoColors.length) {
this.refillAutoColors();
}

// Prevent consecutive colors from being the same
// (i.e. when transitioning from user colours to auto colours)
const nextColor = this.autoColors.shift();

this.lastColor = nextColor !== this.lastColor ? nextColor : this.getNextColor();
return this.lastColor;
}

refillAutoColors() {
// Make sure auto colors are not empty after refill
this.autoColors = [...this.ACCEPTABLE_CONSOLE_COLORS];
}
}

0 comments on commit f57afd7

Please sign in to comment.