diff --git a/README.md b/README.md
index 8e79b4fc..2c41ae2d 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,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`
diff --git a/bin/index.ts b/bin/index.ts
index f8220167..22286380 100755
--- a/bin/index.ts
+++ b/bin/index.ts
@@ -10,6 +10,28 @@ import { concurrently } from '../lib/index.js';
import { castArray } from '../lib/utils.js';
import { readPackageJson } from './read-package-json.js';
+/**
+ * Splits a color arguments string on commas, but preserves commas inside parentheses.
+ * e.g., "red,rgb(255,255,0),blue" → ["red", "rgb(255,255,0)", "blue"]
+ */
+function splitColorArgs(input: string): string[] {
+ const colors: string[] = [];
+ let current = '';
+ let parenDepth = 0;
+ for (const char of input) {
+ if (char === '(') parenDepth++;
+ if (char === ')') parenDepth--;
+ if (char === ',' && parenDepth === 0) {
+ if (current.trim()) colors.push(current.trim());
+ current = '';
+ } else {
+ current += char;
+ }
+ }
+ if (current.trim()) colors.push(current.trim());
+ return colors;
+}
+
const version = String(readPackageJson().version);
const epilogue = `For documentation and more examples, visit:\nhttps://github.com/open-cli-tools/concurrently/tree/v${version}/docs`;
@@ -256,7 +278,7 @@ concurrently(
hide: args.hide.split(','),
group: args.group,
prefix: args.prefix,
- prefixColors: args.prefixColors.split(','),
+ prefixColors: splitColorArgs(args.prefixColors),
prefixLength: args.prefixLength,
padPrefix: args.padPrefix,
restartDelay:
diff --git a/docs/cli/prefixing.md b/docs/cli/prefixing.md
index c22b638a..48d9a477 100644
--- a/docs/cli/prefixing.md
+++ b/docs/cli/prefixing.md
@@ -118,6 +118,34 @@ $ concurrently -c bgGray,red.bgBlack 'echo Hello there' 'echo General Kenobi!'
- `bgYellow`
+### 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.
diff --git a/lib/logger.spec.ts b/lib/logger.spec.ts
index b2f8f74a..a974bf88 100644
--- a/lib/logger.spec.ts
+++ b/lib/logger.spec.ts
@@ -247,6 +247,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');
diff --git a/lib/logger.ts b/lib/logger.ts
index 2e370081..6c5d71d6 100644
--- a/lib/logger.ts
+++ b/lib/logger.ts
@@ -9,10 +9,90 @@ import { escapeRegExp } 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 as unknown as Record)[key], chalk);
+/**
+ * Parses a color string into segments, preserving function calls as single tokens.
+ * e.g., "black.bgHex(#533AFD).dim" → ["black", "bgHex(#533AFD)", "dim"]
+ */
+function parseColorSegments(colorString: string): string[] {
+ const segments: string[] = [];
+ let current = '';
+ let parenDepth = 0;
+
+ for (const char of colorString) {
+ if (char === '(') parenDepth++;
+ if (char === ')') parenDepth--;
+ if (char === '.' && parenDepth === 0) {
+ if (current) segments.push(current);
+ current = '';
+ } else {
+ current += char;
+ }
+ }
+ if (current) segments.push(current);
+ return segments;
+}
+
+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)[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 = parseColorSegments(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 {
@@ -154,18 +234,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);
}