Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ Check out documentation and other usage examples in the [`docs` directory](./doc
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 or a string as supported by [Chalk](https://www.npmjs.com/package/chalk) and additional style `auto` for an automatically picked color.
Supports all Chalk color functions: `#RRGGBB`, `bg#RRGGBB`, `hex()`, `bgHex()`, `rgb()`, `bgRgb()`, `ansi256()`, `bgAnsi256()`.
Functions and modifiers can be chained (e.g., `rgb(255,136,0).bold`, `black.bgHex(#00FF00).dim`).
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`
Expand Down
4 changes: 2 additions & 2 deletions bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { hideBin } from 'yargs/helpers';
import { assertDeprecated } from '../lib/assert.js';
import * as defaults from '../lib/defaults.js';
import { concurrently } from '../lib/index.js';
import { castArray } from '../lib/utils.js';
import { castArray, splitOutsideParens } from '../lib/utils.js';
import { readPackageJson } from './read-package-json.js';

const version = String(readPackageJson().version);
Expand Down Expand Up @@ -256,7 +256,7 @@ concurrently(
hide: args.hide.split(','),
group: args.group,
prefix: args.prefix,
prefixColors: args.prefixColors.split(','),
prefixColors: splitOutsideParens(args.prefixColors, ','),
prefixLength: args.prefixLength,
padPrefix: args.padPrefix,
restartDelay:
Expand Down
28 changes: 28 additions & 0 deletions docs/cli/prefixing.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,34 @@ $ concurrently -c bgGray,red.bgBlack 'echo Hello there' 'echo General Kenobi!'
- `bgYellow`
</details>

### Advanced Color Functions

concurrently supports all [Chalk color functions](https://github.com/chalk/chalk#256-and-truecolor-color-support):

| Function | Description |
| ---------------- | --------------------------- |
| `#RRGGBB` | Foreground hex (shorthand) |
| `bg#RRGGBB` | Background hex (shorthand) |
| `hex(#RRGGBB)` | Foreground hex |
| `bgHex(#RRGGBB)` | Background hex |
| `rgb(R,G,B)` | Foreground RGB (0-255) |
| `bgRgb(R,G,B)` | Background RGB (0-255) |
| `ansi256(N)` | Foreground ANSI 256 (0-255) |
| `bgAnsi256(N)` | Background ANSI 256 (0-255) |

All functions can be chained with colors and modifiers:

```bash
# Hex colors
$ concurrently -c 'bg#FF0000.bold,black.bgHex(#00FF00).dim' 'echo Red bg' 'echo Green bg'

# RGB colors
$ concurrently -c 'rgb(255,136,0).bold,black.bgRgb(100,100,255)' 'echo Orange' 'echo Blue bg'

# ANSI 256 colors
$ concurrently -c 'ansi256(199),ansi256(50).bgAnsi256(17)' 'echo Pink' 'echo Cyan on blue'
```

## Prefix Length

When using the `command` prefix style, it's possible that it'll be too long.<br/>
Expand Down
202 changes: 202 additions & 0 deletions lib/logger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,208 @@ describe('#logCommandText()', () => {
);
});

it('logs prefix using prefixColor from command if prefixColor is a bg hex value (short form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bg#32bd8a',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#32bd8a')('[1]')} `, 'foo', cmd);
});

it('logs prefix using prefixColor from command if prefixColor is a bg hex value with modifiers (short form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bg#32bd8a.bold',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.bgHex('#32bd8a').bold('[1]')} `,
'foo',
cmd,
);
});

it('handles 3-digit hex codes for bg hex (short form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bg#f00',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#f00')('[1]')} `, 'foo', cmd);
});

it('logs prefix using prefixColor from command if prefixColor is a bgHex() value (explicit form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(#ff5500)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#ff5500')('[1]')} `, 'foo', cmd);
});

it('logs prefix using prefixColor from command if prefixColor is a bgHex() value with modifiers (explicit form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(#ff5500).dim',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.bgHex('#ff5500').dim('[1]')} `,
'foo',
cmd,
);
});

it('handles 3-digit hex codes for bgHex() (explicit form)', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(#0f0)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#0f0')('[1]')} `, 'foo', cmd);
});

it('falls back to default color for malformed bgHex() syntax', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'bgHex(invalid)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
});

it('logs prefix with chained fgColor.bgHex().modifier pattern', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'black.bgHex(#533AFD).dim',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.black.bgHex('#533AFD').dim('[1]')} `,
'foo',
cmd,
);
});

it('logs prefix with chained fgColor.bg#HEXCODE.modifier pattern', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'black.bg#FF0000.bold',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.black.bgHex('#FF0000').bold('[1]')} `,
'foo',
cmd,
);
});

it('logs prefix with chained #HEXCODE.bgNamed.modifier pattern', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: '#FF0000.bgBlue.dim',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.hex('#FF0000').bgBlue.dim('[1]')} `,
'foo',
cmd,
);
});

it('logs prefix using rgb() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'rgb(255,136,0).bold',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.rgb(255, 136, 0).bold('[1]')} `,
'foo',
cmd,
);
});

it('logs prefix using bgRgb() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'black.bgRgb(100,100,255)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.black.bgRgb(100, 100, 255)('[1]')} `,
'foo',
cmd,
);
});

it('logs prefix using ansi256() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'ansi256(199)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.ansi256(199)('[1]')} `, 'foo', cmd);
});

it('logs prefix using bgAnsi256() color function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'ansi256(199).bgAnsi256(50)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(
`${chalk.ansi256(199).bgAnsi256(50)('[1]')} `,
'foo',
cmd,
);
});

it('logs prefix using hex() explicit function', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'hex(#ff5500)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.hex('#ff5500')('[1]')} `, 'foo', cmd);
});

it('falls back to default color for malformed hex() syntax', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'hex(invalid)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
});

it('falls back to default color for unknown function name', () => {
const { logger } = createLogger({});
const cmd = new FakeCommand('', undefined, 1, {
prefixColor: 'unknownFunc(123)',
});
logger.logCommandText('foo', cmd);

expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
});

it('does nothing if command is hidden by name', () => {
const { logger } = createLogger({ hide: ['abc'] });
const cmd = new FakeCommand('abc');
Expand Down
85 changes: 65 additions & 20 deletions lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,72 @@ import Rx from 'rxjs';
import { Command, CommandIdentifier } from './command.js';
import { DateFormatter } from './date-format.js';
import * as defaults from './defaults.js';
import { escapeRegExp } from './utils.js';
import { escapeRegExp, splitOutsideParens } from './utils.js';

const defaultChalk = chalk;
const noColorChalk = new Chalk({ level: 0 });

function getChalkPath(chalk: ChalkInstance, path: string): ChalkInstance | undefined {
return path
.split('.')
.reduce(
(prev, key) => prev && (prev as unknown as Record<string, ChalkInstance>)[key],
chalk,
);
const HEX_PATTERN = /^#[0-9A-Fa-f]{3,6}$/;

/**
* Applies a single color segment to a chalk instance.
* Handles: function calls (hex, bgHex, rgb, bgRgb, ansi256, bgAnsi256, etc.),
* shorthands (#HEX, bg#HEX), and named colors/modifiers.
*/
function applySegment(color: ChalkInstance, segment: string): ChalkInstance | undefined {
// Function call: name(args) - handles chalk color functions
const fnMatch = segment.match(/^(\w+)\((.+)\)$/);
if (fnMatch) {
const [, fnName, argsStr] = fnMatch;
const args = argsStr.split(',').map((a) => {
const t = a.trim();
return /^\d+$/.test(t) ? parseInt(t, 10) : t;
});

// Explicit function calls for known chalk color functions
switch (fnName) {
case 'rgb':
return color.rgb(args[0] as number, args[1] as number, args[2] as number);
case 'bgRgb':
return color.bgRgb(args[0] as number, args[1] as number, args[2] as number);
case 'hex':
if (!HEX_PATTERN.test(args[0] as string)) return undefined;
return color.hex(args[0] as string);
case 'bgHex':
if (!HEX_PATTERN.test(args[0] as string)) return undefined;
return color.bgHex(args[0] as string);
case 'ansi256':
return color.ansi256(args[0] as number);
case 'bgAnsi256':
return color.bgAnsi256(args[0] as number);
default:
return undefined;
}
}

// Shorthands
if (segment.startsWith('bg#')) return color.bgHex(segment.slice(2));
if (segment.startsWith('#')) return color.hex(segment);

// Property: black, bold, dim, etc.
return (color as unknown as Record<string, ChalkInstance>)[segment] ?? undefined;
}

/**
* Applies a color string to chalk, supporting chained colors and modifiers.
* Returns undefined if any segment is invalid (triggers fallback to default).
*/
function applyColor(chalkInstance: ChalkInstance, colorString: string): ChalkInstance | undefined {
const segments = splitOutsideParens(colorString, '.');
if (segments.length === 0) return undefined;

let color: ChalkInstance = chalkInstance;
for (const segment of segments) {
const next = applySegment(color, segment);
if (!next) return undefined;
color = next;
}
return color;
}

export class Logger {
Expand Down Expand Up @@ -157,18 +211,9 @@ export class Logger {
}

colorText(command: Command, text: string) {
let color: ChalkInstance;
if (command.prefixColor?.startsWith('#')) {
const [hexColor, ...modifiers] = command.prefixColor.split('.');
color = this.chalk.hex(hexColor);
const modifiedColor = getChalkPath(color, modifiers.join('.'));
if (modifiedColor) {
color = modifiedColor;
}
} else {
const defaultColor = getChalkPath(this.chalk, defaults.prefixColors) as ChalkInstance;
color = getChalkPath(this.chalk, command.prefixColor ?? '') ?? defaultColor;
}
const prefixColor = command.prefixColor ?? '';
const defaultColor = applyColor(this.chalk, defaults.prefixColors) as ChalkInstance;
const color = applyColor(this.chalk, prefixColor) ?? defaultColor;
return color(text);
}

Expand Down
Loading
Loading