Skip to content

Commit d74c6a0

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 2c30e74 commit d74c6a0

File tree

5 files changed

+342
-17
lines changed

5 files changed

+342
-17
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ Check out documentation and other usage examples in the [`docs` directory](./doc
105105
Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
106106
Default: the name of the process, or its index if no name is set.
107107
- `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.
108+
Supports all Chalk color functions: `#RRGGBB`, `bg#RRGGBB`, `hex()`, `bgHex()`, `rgb()`, `bgRgb()`, `ansi256()`, `bgAnsi256()`.
109+
Functions and modifiers can be chained (e.g., `rgb(255,136,0).bold`, `black.bgHex(#00FF00).dim`).
108110
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.
109111
Prefix colors specified per-command take precedence over this list.
110112
- `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
@@ -247,6 +247,208 @@ describe('#logCommandText()', () => {
247247
);
248248
});
249249

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

0 commit comments

Comments
 (0)