Skip to content

Commit 20aa945

Browse files
feat: add support for Chalk color functions in prefix colors
Add support for all Chalk color functions: hex(), bgHex(), rgb(), bgRgb(), ansi256(), bgAnsi256(), plus shorthand syntax #RRGGBB and bg#RRGGBB. Functions and modifiers can be chained (e.g., rgb(255,136,0).bold, black.bgHex(#00FF00).dim). The CLI now correctly parses color arguments containing commas inside function calls like rgb(255,255,0) by using a parenthesis-aware splitter.
1 parent 50beecb commit 20aa945

5 files changed

Lines changed: 342 additions & 20 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ Check out documentation and other usage examples in the [`docs` directory](./doc
112112
Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
113113
Default: the name of the process, or its index if no name is set.
114114
- `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.
115+
Supports all Chalk color functions: `#RRGGBB`, `bg#RRGGBB`, `hex()`, `bgHex()`, `rgb()`, `bgRgb()`, `ansi256()`, `bgAnsi256()`.
116+
Functions and modifiers can be chained (e.g., `rgb(255,136,0).bold`, `black.bgHex(#00FF00).dim`).
115117
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.
116118
Prefix colors specified per-command take precedence over this list.
117119
- `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`

bin/index.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@ import { concurrently } from '../lib/index.js';
1010
import { castArray } from '../lib/utils.js';
1111
import { readPackageJson } from './read-package-json.js';
1212

13+
/**
14+
* Splits a color arguments string on commas, but preserves commas inside parentheses.
15+
* e.g., "red,rgb(255,255,0),blue" → ["red", "rgb(255,255,0)", "blue"]
16+
*/
17+
function splitColorArgs(input: string): string[] {
18+
const colors: string[] = [];
19+
let current = '';
20+
let parenDepth = 0;
21+
for (const char of input) {
22+
if (char === '(') parenDepth++;
23+
if (char === ')') parenDepth--;
24+
if (char === ',' && parenDepth === 0) {
25+
if (current.trim()) colors.push(current.trim());
26+
current = '';
27+
} else {
28+
current += char;
29+
}
30+
}
31+
if (current.trim()) colors.push(current.trim());
32+
return colors;
33+
}
34+
1335
const version = String(readPackageJson().version);
1436
const epilogue = `For documentation and more examples, visit:\nhttps://github.com/open-cli-tools/concurrently/tree/v${version}/docs`;
1537

@@ -256,7 +278,7 @@ concurrently(
256278
hide: args.hide.split(','),
257279
group: args.group,
258280
prefix: args.prefix,
259-
prefixColors: args.prefixColors.split(','),
281+
prefixColors: splitColorArgs(args.prefixColors),
260282
prefixLength: args.prefixLength,
261283
padPrefix: args.padPrefix,
262284
restartDelay:

docs/cli/prefixing.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,34 @@ $ concurrently -c bgGray,red.bgBlack 'echo Hello there' 'echo General Kenobi!'
118118
- `bgYellow`
119119
</details>
120120

121+
### Advanced Color Functions
122+
123+
concurrently supports all [Chalk color functions](https://github.com/chalk/chalk#256-and-truecolor-color-support):
124+
125+
| Function | Description |
126+
| ---------------- | --------------------------- |
127+
| `#RRGGBB` | Foreground hex (shorthand) |
128+
| `bg#RRGGBB` | Background hex (shorthand) |
129+
| `hex(#RRGGBB)` | Foreground hex |
130+
| `bgHex(#RRGGBB)` | Background hex |
131+
| `rgb(R,G,B)` | Foreground RGB (0-255) |
132+
| `bgRgb(R,G,B)` | Background RGB (0-255) |
133+
| `ansi256(N)` | Foreground ANSI 256 (0-255) |
134+
| `bgAnsi256(N)` | Background ANSI 256 (0-255) |
135+
136+
All functions can be chained with colors and modifiers:
137+
138+
```bash
139+
# Hex colors
140+
$ concurrently -c 'bg#FF0000.bold,black.bgHex(#00FF00).dim' 'echo Red bg' 'echo Green bg'
141+
142+
# RGB colors
143+
$ concurrently -c 'rgb(255,136,0).bold,black.bgRgb(100,100,255)' 'echo Orange' 'echo Blue bg'
144+
145+
# ANSI 256 colors
146+
$ concurrently -c 'ansi256(199),ansi256(50).bgAnsi256(17)' 'echo Pink' 'echo Cyan on blue'
147+
```
148+
121149
## Prefix Length
122150

123151
When using the `command` prefix style, it's possible that it'll be too long.<br/>

lib/logger.spec.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,208 @@ describe('#logCommandText()', () => {
257257
);
258258
});
259259

260+
it('logs prefix using prefixColor from command if prefixColor is a bg hex value (short form)', () => {
261+
const { logger } = createLogger({});
262+
const cmd = new FakeCommand('', undefined, 1, {
263+
prefixColor: 'bg#32bd8a',
264+
});
265+
logger.logCommandText('foo', cmd);
266+
267+
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#32bd8a')('[1]')} `, 'foo', cmd);
268+
});
269+
270+
it('logs prefix using prefixColor from command if prefixColor is a bg hex value with modifiers (short form)', () => {
271+
const { logger } = createLogger({});
272+
const cmd = new FakeCommand('', undefined, 1, {
273+
prefixColor: 'bg#32bd8a.bold',
274+
});
275+
logger.logCommandText('foo', cmd);
276+
277+
expect(logger.log).toHaveBeenCalledWith(
278+
`${chalk.bgHex('#32bd8a').bold('[1]')} `,
279+
'foo',
280+
cmd,
281+
);
282+
});
283+
284+
it('handles 3-digit hex codes for bg hex (short form)', () => {
285+
const { logger } = createLogger({});
286+
const cmd = new FakeCommand('', undefined, 1, {
287+
prefixColor: 'bg#f00',
288+
});
289+
logger.logCommandText('foo', cmd);
290+
291+
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#f00')('[1]')} `, 'foo', cmd);
292+
});
293+
294+
it('logs prefix using prefixColor from command if prefixColor is a bgHex() value (explicit form)', () => {
295+
const { logger } = createLogger({});
296+
const cmd = new FakeCommand('', undefined, 1, {
297+
prefixColor: 'bgHex(#ff5500)',
298+
});
299+
logger.logCommandText('foo', cmd);
300+
301+
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#ff5500')('[1]')} `, 'foo', cmd);
302+
});
303+
304+
it('logs prefix using prefixColor from command if prefixColor is a bgHex() value with modifiers (explicit form)', () => {
305+
const { logger } = createLogger({});
306+
const cmd = new FakeCommand('', undefined, 1, {
307+
prefixColor: 'bgHex(#ff5500).dim',
308+
});
309+
logger.logCommandText('foo', cmd);
310+
311+
expect(logger.log).toHaveBeenCalledWith(
312+
`${chalk.bgHex('#ff5500').dim('[1]')} `,
313+
'foo',
314+
cmd,
315+
);
316+
});
317+
318+
it('handles 3-digit hex codes for bgHex() (explicit form)', () => {
319+
const { logger } = createLogger({});
320+
const cmd = new FakeCommand('', undefined, 1, {
321+
prefixColor: 'bgHex(#0f0)',
322+
});
323+
logger.logCommandText('foo', cmd);
324+
325+
expect(logger.log).toHaveBeenCalledWith(`${chalk.bgHex('#0f0')('[1]')} `, 'foo', cmd);
326+
});
327+
328+
it('falls back to default color for malformed bgHex() syntax', () => {
329+
const { logger } = createLogger({});
330+
const cmd = new FakeCommand('', undefined, 1, {
331+
prefixColor: 'bgHex(invalid)',
332+
});
333+
logger.logCommandText('foo', cmd);
334+
335+
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
336+
});
337+
338+
it('logs prefix with chained fgColor.bgHex().modifier pattern', () => {
339+
const { logger } = createLogger({});
340+
const cmd = new FakeCommand('', undefined, 1, {
341+
prefixColor: 'black.bgHex(#533AFD).dim',
342+
});
343+
logger.logCommandText('foo', cmd);
344+
345+
expect(logger.log).toHaveBeenCalledWith(
346+
`${chalk.black.bgHex('#533AFD').dim('[1]')} `,
347+
'foo',
348+
cmd,
349+
);
350+
});
351+
352+
it('logs prefix with chained fgColor.bg#HEXCODE.modifier pattern', () => {
353+
const { logger } = createLogger({});
354+
const cmd = new FakeCommand('', undefined, 1, {
355+
prefixColor: 'black.bg#FF0000.bold',
356+
});
357+
logger.logCommandText('foo', cmd);
358+
359+
expect(logger.log).toHaveBeenCalledWith(
360+
`${chalk.black.bgHex('#FF0000').bold('[1]')} `,
361+
'foo',
362+
cmd,
363+
);
364+
});
365+
366+
it('logs prefix with chained #HEXCODE.bgNamed.modifier pattern', () => {
367+
const { logger } = createLogger({});
368+
const cmd = new FakeCommand('', undefined, 1, {
369+
prefixColor: '#FF0000.bgBlue.dim',
370+
});
371+
logger.logCommandText('foo', cmd);
372+
373+
expect(logger.log).toHaveBeenCalledWith(
374+
`${chalk.hex('#FF0000').bgBlue.dim('[1]')} `,
375+
'foo',
376+
cmd,
377+
);
378+
});
379+
380+
it('logs prefix using rgb() color function', () => {
381+
const { logger } = createLogger({});
382+
const cmd = new FakeCommand('', undefined, 1, {
383+
prefixColor: 'rgb(255,136,0).bold',
384+
});
385+
logger.logCommandText('foo', cmd);
386+
387+
expect(logger.log).toHaveBeenCalledWith(
388+
`${chalk.rgb(255, 136, 0).bold('[1]')} `,
389+
'foo',
390+
cmd,
391+
);
392+
});
393+
394+
it('logs prefix using bgRgb() color function', () => {
395+
const { logger } = createLogger({});
396+
const cmd = new FakeCommand('', undefined, 1, {
397+
prefixColor: 'black.bgRgb(100,100,255)',
398+
});
399+
logger.logCommandText('foo', cmd);
400+
401+
expect(logger.log).toHaveBeenCalledWith(
402+
`${chalk.black.bgRgb(100, 100, 255)('[1]')} `,
403+
'foo',
404+
cmd,
405+
);
406+
});
407+
408+
it('logs prefix using ansi256() color function', () => {
409+
const { logger } = createLogger({});
410+
const cmd = new FakeCommand('', undefined, 1, {
411+
prefixColor: 'ansi256(199)',
412+
});
413+
logger.logCommandText('foo', cmd);
414+
415+
expect(logger.log).toHaveBeenCalledWith(`${chalk.ansi256(199)('[1]')} `, 'foo', cmd);
416+
});
417+
418+
it('logs prefix using bgAnsi256() color function', () => {
419+
const { logger } = createLogger({});
420+
const cmd = new FakeCommand('', undefined, 1, {
421+
prefixColor: 'ansi256(199).bgAnsi256(50)',
422+
});
423+
logger.logCommandText('foo', cmd);
424+
425+
expect(logger.log).toHaveBeenCalledWith(
426+
`${chalk.ansi256(199).bgAnsi256(50)('[1]')} `,
427+
'foo',
428+
cmd,
429+
);
430+
});
431+
432+
it('logs prefix using hex() explicit function', () => {
433+
const { logger } = createLogger({});
434+
const cmd = new FakeCommand('', undefined, 1, {
435+
prefixColor: 'hex(#ff5500)',
436+
});
437+
logger.logCommandText('foo', cmd);
438+
439+
expect(logger.log).toHaveBeenCalledWith(`${chalk.hex('#ff5500')('[1]')} `, 'foo', cmd);
440+
});
441+
442+
it('falls back to default color for malformed hex() syntax', () => {
443+
const { logger } = createLogger({});
444+
const cmd = new FakeCommand('', undefined, 1, {
445+
prefixColor: 'hex(invalid)',
446+
});
447+
logger.logCommandText('foo', cmd);
448+
449+
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
450+
});
451+
452+
it('falls back to default color for unknown function name', () => {
453+
const { logger } = createLogger({});
454+
const cmd = new FakeCommand('', undefined, 1, {
455+
prefixColor: 'unknownFunc(123)',
456+
});
457+
logger.logCommandText('foo', cmd);
458+
459+
expect(logger.log).toHaveBeenCalledWith(`${chalk.reset('[1]')} `, 'foo', cmd);
460+
});
461+
260462
it('does nothing if command is hidden by name', () => {
261463
const { logger } = createLogger({ hide: ['abc'] });
262464
const cmd = new FakeCommand('abc');

0 commit comments

Comments
 (0)