Skip to content

util: downgrade hex colors to terminal color depth - #64955

Open
Tobbe wants to merge 6 commits into
nodejs:mainfrom
Tobbe:util-styletext-color-depth
Open

util: downgrade hex colors to terminal color depth#64955
Tobbe wants to merge 6 commits into
nodejs:mainfrom
Tobbe:util-styletext-color-depth

Conversation

@Tobbe

@Tobbe Tobbe commented Aug 2, 2026

Copy link
Copy Markdown

styleText() emits 24-bit TrueColor sequences for hex colors regardless of what the terminal supports, so FORCE_COLOR=1 and FORCE_COLOR=2, which document 16-color and 256-color support, get a sequence their terminal may render as literal text or ignore.

This makes the hex path honor the color depth that is already computed for the stream:

Depth Sequence for #ffcc00
FORCE_COLOR=3, or a 16m-color terminal \e[38;2;255;204;0m
FORCE_COLOR=2, or a 256-color terminal \e[38;5;220m
FORCE_COLOR=1, or a 16-color terminal \e[93m
FORCE_COLOR=0, NO_COLOR, non-TTY no styling (unchanged)

Named formats are unaffected: they already are basic color codes. When validateStream is false there is no stream to inspect, so the color is only downgraded if FORCE_COLOR is set, and stays 24-bit otherwise.

Implementation notes:

  • internal/util/colors gains getColorDepth(stream) and getForcedColorDepth(). shouldColorize() becomes a > 2 check on the former, keeping its previous behavior.
  • The hex style cache now stores the open sequence for each depth, so a color is converted once no matter how often it is used.
  • The conversions mirror ansis and ansi-styles, so a hex color downgrades to the same color the rest of the ecosystem picks for it. Verified by comparing against ansis/src/color-math.js over all 16777216 colors: zero differences at both depths.

I don't know enough about color math to really understand the conversions used here. I pretty much copied the implementation ansis uses.

EDIT: I took a second look at the Node docs, and it's pretty clear that FORCE_COLOR=0 should disable colors:

Any other value will result in colorized output being disabled.

So I updated the implementation. But do note two things:

  • https://force-color.org states "When this variable is present and not an empty string (regardless of its value), it should force the addition of ANSI color". This goes against Node's docs
  • This PR doesn't touch the implementation for simple string named colors (like styleText('red', ...)). So those are not disabled by setting FORCE_COLOR=0

Assisted-by: claude-opus-5, deep-seek-v4-flash-0731, kimi-k3

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module. labels Aug 2, 2026
styleText() always emitted 24-bit TrueColor sequences for hex colors,
even when the terminal only supports 16 or 256 colors. Terminals that do
not understand the sequence render it as literal text or drop the color.

Emit the sequence matching the color depth reported by the stream, or by
FORCE_COLOR when it is set: TrueColor for level 3, the closest entry of
the 256-color palette for level 2, and the closest of the 16 basic
colors for level 1. Named formats are unaffected, as they already are
basic color codes. When validateStream is false there is no stream to
inspect, so the color is only downgraded if FORCE_COLOR is set.

The conversions mirror the ones used by ansis and ansi-styles, so a hex
color downgrades to the same color the rest of the ecosystem picks.

Refs: https://nodejs.org/api/cli.html#force_color1-2-3
Signed-off-by: Tobbe Lundberg <tobbe@tlundberg.com>
@avivkeller avivkeller added the semver-major PRs that contain breaking changes and should be released in the next major version. label Aug 2, 2026
Comment thread doc/api/util.md
@avivkeller avivkeller removed the semver-major PRs that contain breaking changes and should be released in the next major version. label Aug 2, 2026

// Hex colors are downgraded to the color depth reported by `FORCE_COLOR`, so
// make sure the environment running the test does not set it.
delete process.env.FORCE_COLOR;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

happy to be wrong on this but in my own practice I generally avoid delete as the education I've received has been that it's bad to use and that's been reinforced by code style tooling I've always used.

It doesn't seem like we use it anywhere else in the codebase in this way. I don't have a direct recommendation on what to replace it with, but I'm cautious to see us introduce it as a precedent with this PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worked around it with a spread assignment. The if can be dropped if you like, but the linter complains about an unused variable if I do, so I'd have to add a comment to disable the linter in that case. I'm happy with either solution.

@Tobbe Tobbe Aug 3, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I also did find a few delete process... in the code base already, like here:

delete process.env.NODE_USE_ENV_PROXY; // Ensure the environment variable is not set.

So it might be okay to just go with the simpler delete usage.

function styled(format, forceColor) {
process.env = { ...originalEnv };
if (forceColor === undefined) {
delete process.env.FORCE_COLOR;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as as my previous comment about delete usage.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worked around this delete as well with a spread instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants