Skip to content

Commit 74dffcd

Browse files
logger: add support for Chalk functions in prefix colors (#578)
1 parent 50beecb commit 74dffcd

File tree

7 files changed

+362
-23
lines changed

7 files changed

+362
-23
lines changed

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { hideBin } from 'yargs/helpers';
77
import { assertDeprecated } from '../lib/assert.js';
88
import * as defaults from '../lib/defaults.js';
99
import { concurrently } from '../lib/index.js';
10-
import { castArray } from '../lib/utils.js';
10+
import { castArray, splitOutsideParens } from '../lib/utils.js';
1111
import { readPackageJson } from './read-package-json.js';
1212

1313
const version = String(readPackageJson().version);
@@ -256,7 +256,7 @@ concurrently(
256256
hide: args.hide.split(','),
257257
group: args.group,
258258
prefix: args.prefix,
259-
prefixColors: args.prefixColors.split(','),
259+
prefixColors: splitOutsideParens(args.prefixColors, ','),
260260
prefixLength: args.prefixLength,
261261
padPrefix: args.padPrefix,
262262
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');

lib/logger.ts

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,72 @@ import Rx from 'rxjs';
44
import { Command, CommandIdentifier } from './command.js';
55
import { DateFormatter } from './date-format.js';
66
import * as defaults from './defaults.js';
7-
import { escapeRegExp } from './utils.js';
7+
import { escapeRegExp, splitOutsideParens } from './utils.js';
88

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

12-
function getChalkPath(chalk: ChalkInstance, path: string): ChalkInstance | undefined {
13-
return path
14-
.split('.')
15-
.reduce(
16-
(prev, key) => prev && (prev as unknown as Record<string, ChalkInstance>)[key],
17-
chalk,
18-
);
12+
const HEX_PATTERN = /^#[0-9A-Fa-f]{3,6}$/;
13+
14+
/**
15+
* Applies a single color segment to a chalk instance.
16+
* Handles: function calls (hex, bgHex, rgb, bgRgb, ansi256, bgAnsi256, etc.),
17+
* shorthands (#HEX, bg#HEX), and named colors/modifiers.
18+
*/
19+
function applySegment(color: ChalkInstance, segment: string): ChalkInstance | undefined {
20+
// Function call: name(args) - handles chalk color functions
21+
const fnMatch = segment.match(/^(\w+)\((.+)\)$/);
22+
if (fnMatch) {
23+
const [, fnName, argsStr] = fnMatch;
24+
const args = argsStr.split(',').map((a) => {
25+
const t = a.trim();
26+
return /^\d+$/.test(t) ? parseInt(t, 10) : t;
27+
});
28+
29+
// Explicit function calls for known chalk color functions
30+
switch (fnName) {
31+
case 'rgb':
32+
return color.rgb(args[0] as number, args[1] as number, args[2] as number);
33+
case 'bgRgb':
34+
return color.bgRgb(args[0] as number, args[1] as number, args[2] as number);
35+
case 'hex':
36+
if (!HEX_PATTERN.test(args[0] as string)) return undefined;
37+
return color.hex(args[0] as string);
38+
case 'bgHex':
39+
if (!HEX_PATTERN.test(args[0] as string)) return undefined;
40+
return color.bgHex(args[0] as string);
41+
case 'ansi256':
42+
return color.ansi256(args[0] as number);
43+
case 'bgAnsi256':
44+
return color.bgAnsi256(args[0] as number);
45+
default:
46+
return undefined;
47+
}
48+
}
49+
50+
// Shorthands
51+
if (segment.startsWith('bg#')) return color.bgHex(segment.slice(2));
52+
if (segment.startsWith('#')) return color.hex(segment);
53+
54+
// Property: black, bold, dim, etc.
55+
return (color as unknown as Record<string, ChalkInstance>)[segment] ?? undefined;
56+
}
57+
58+
/**
59+
* Applies a color string to chalk, supporting chained colors and modifiers.
60+
* Returns undefined if any segment is invalid (triggers fallback to default).
61+
*/
62+
function applyColor(chalkInstance: ChalkInstance, colorString: string): ChalkInstance | undefined {
63+
const segments = splitOutsideParens(colorString, '.');
64+
if (segments.length === 0) return undefined;
65+
66+
let color: ChalkInstance = chalkInstance;
67+
for (const segment of segments) {
68+
const next = applySegment(color, segment);
69+
if (!next) return undefined;
70+
color = next;
71+
}
72+
return color;
1973
}
2074

2175
export class Logger {
@@ -157,18 +211,9 @@ export class Logger {
157211
}
158212

159213
colorText(command: Command, text: string) {
160-
let color: ChalkInstance;
161-
if (command.prefixColor?.startsWith('#')) {
162-
const [hexColor, ...modifiers] = command.prefixColor.split('.');
163-
color = this.chalk.hex(hexColor);
164-
const modifiedColor = getChalkPath(color, modifiers.join('.'));
165-
if (modifiedColor) {
166-
color = modifiedColor;
167-
}
168-
} else {
169-
const defaultColor = getChalkPath(this.chalk, defaults.prefixColors) as ChalkInstance;
170-
color = getChalkPath(this.chalk, command.prefixColor ?? '') ?? defaultColor;
171-
}
214+
const prefixColor = command.prefixColor ?? '';
215+
const defaultColor = applyColor(this.chalk, defaults.prefixColors) as ChalkInstance;
216+
const color = applyColor(this.chalk, prefixColor) ?? defaultColor;
172217
return color(text);
173218
}
174219

0 commit comments

Comments
 (0)