Skip to content

Implement dynamic 8-bit color palettes#19883

Open
lhecker wants to merge 2 commits intomainfrom
dev/lhecker/generate-256-colors
Open

Implement dynamic 8-bit color palettes#19883
lhecker wants to merge 2 commits intomainfrom
dev/lhecker/generate-256-colors

Conversation

@lhecker
Copy link
Member

@lhecker lhecker commented Feb 18, 2026

Unlike the proposal this PR implements it using Oklab, which
we already had and previously found to be superior to CIELAB.

Lots of changes due to const-correctness (giga-meh)
and several to plumb settings through the model and UI.
The core is in the TerminalSettings class, as you'd expect.

The way this is implemented the dynamically generated palette
will react to changes in the palette via VT sequences.

Closes #19881

Validation Steps Performed

  • Print the basic 256 colors
  • Observe them change as this feature is toggled on/off

@lhecker lhecker requested a review from DHowett February 18, 2026 21:34
Comment on lines +233 to 248
if (const auto v = appearance.Foreground())
{
_DefaultForeground = til::color{ appearance.Foreground().Value() };
_DefaultForeground = til::color{ v.Value() };
}
if (appearance.Background())
if (const auto v = appearance.Background())
{
_DefaultBackground = til::color{ appearance.Background().Value() };
_DefaultBackground = til::color{ v.Value() };
}
if (appearance.SelectionBackground())
if (const auto v = appearance.SelectionBackground())
{
_SelectionBackground = til::color{ appearance.SelectionBackground().Value() };
_SelectionBackground = til::color{ v.Value() };
}
if (appearance.CursorColor())
if (const auto v = appearance.CursorColor())
{
_CursorColor = til::color{ appearance.CursorColor().Value() };
_CursorColor = til::color{ v.Value() };
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Unrelated cleanup.

@lhecker lhecker force-pushed the dev/lhecker/generate-256-colors branch from 9a6e2ac to a865c8d Compare February 18, 2026 21:37
X(IMediaResource, BackgroundImagePath, "backgroundImage", implementation::MediaResource::Empty()) \
X(Model::IntenseStyle, IntenseTextStyle, "intenseTextStyle", Model::IntenseStyle::Bright) \
X(Core::AdjustTextMode, AdjustIndistinguishableColors, "adjustIndistinguishableColors", Core::AdjustTextMode::Automatic) \
X(bool, GeneratePalette, "generatePalette", true) \
Copy link
Member

Choose a reason for hiding this comment

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

Should the default indeed be true?

Copy link
Member Author

@lhecker lhecker Feb 18, 2026

Choose a reason for hiding this comment

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

In my opinion, yes. For the vast majority of users, I expect the downside to be low.

More importantly, I have thought about ways to make this setting more discoverable. We could put it onto the compatibility page as well, given that it affects standard color rendering.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think there is a fair chance that this is going to break some applications. These colors were always intended to be absolute values, and now we're replacing them with colors that could be completely different, potentially resulting in color combinations that are unreadable. If that happens you're going to have some very angry users.

Copy link
Member Author

@lhecker lhecker Feb 19, 2026

Choose a reason for hiding this comment

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

I do like, however, that this "fixes" the behavior when using a light theme. It also improves the stylistic look when using themes that aren't traditional (e.g. with a non-black background color).

While I believe that this would be most useful if enabled by default, it would certainly still be fine if we disable it instead.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I do like, however, that this "fixes" the behavior when using a light theme.

I would have thought the most common problem with light themes came from apps that are just using the 8 basic ANSI colors (when it's assumed that white is going to be visible on the default background color, and the default foreground color will be visible on black), and this mode doesn't help with that.

That said, I'm assuming there must be some apps that would benefit from it, and I'm sure some people will love it, but I'd be very wary of forcing it on everyone and expecting them to opt out if they don't like it.

Choose a reason for hiding this comment

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

@j4james The default palette interpolates from black to white. As a result, using color 231 (white) on a white background is unreadable. The generated palette instead interpolates from background to foreground. So for a light theme, color 231 is dark and readable against the background.

Whether to set this as default is a difficult choice. I believe that quicker and wider adoption would lead program maintainers to consider the palette a viable choice and actually use it, making all the benefits a reality.

Though breaking changes are a big deal so I understand either way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

As a result, using color 231 (white) on a white background is unreadable. The generated palette instead interpolates from background to foreground. So for a light theme, color 231 is dark and readable against the background.

I was talking about apps using the basic ANSI colors though. ANSI white 7 (or the bright white 15) is still going to be unreadable on a light background.

And apps that try to detect light color schemes, and then compensate by using dark colors from the 256-color palette, could actually end up being worse off with this mode (they might decide to use 16 or 17 in place of 231 when the color scheme is light, which would assumedly now have the opposite effect than what was intended).

Choose a reason for hiding this comment

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

I was talking about apps using the basic ANSI colors though. ANSI white 7 (or the bright white 15) is still going to be unreadable on a light background.

Oh. I think ansi 7 for light themes should be dark. Like 15 should not mean bright white but instead intense/high contrast.

I understand this may not be the case for some base16 themes and will result in problems trying to use them for programs created with dark themes in mind. Though this will be an issue regardless.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think ansi 7 for light themes should be dark.

Just looking at the color schemes here, that's not typical for a light color scheme. Regardless of whether the scheme is light or dark, ANSI black is typically black (or at least a dark color), and ANSI white is typically white/gray (or at least a lighter color).

There are obviously a lot of weird color schemes that do there own thing, but I thought it was generally expected that ANSI colors match their names, at least to some extent.

{
if (_colorTableDirty)
{
_generate256ColorTable();
Copy link
Member

Choose a reason for hiding this comment

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

I probably would have just thrown a mutable on and called it a day lmao

Copy link
Member Author

Choose a reason for hiding this comment

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

We could consider silencing the const rule and just not use const anymore. It's rarely useful in my opinion.

Comment on lines 125 to 129
void RenderSettings::SetColorTableEntry(const size_t tableIndex, const COLORREF color)
{
_colorTable.at(tableIndex) = color;
_flagColorTableDirty();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you flag the color table as dirty, does that not mean it's going to be regenerated as soon as someone tries to use it? Because then it looks like an application attempting to change one palette entry is going to immediately cause all other entries to be reset. In fact, wouldn't that even reset the entry that has just been changed? That surely can't be right. Am I misunderstanding how this works?

Same thing applies to most of the other calls to _flagColorTableDirty below. Other than maybe RestoreDefaultIndexed256ColorTable, it doesn't seem like any of those operations should be triggering a reset of the palette.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I have only considered what happens if you change the color aliases and base colors, not what happens if you change the 240 other entries. I wonder what the best way to fix this is... Obviously, we could hold a "has this slot been overriden" array, but maybe I should simply not regenerate the table outside of the initial setup.

Now that I mention this, the 2nd array may actually be useful, because we already have this ambiguity around INVALID_COLOR...

Choose a reason for hiding this comment

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

@lhecker I used the "has this slot been overridden" approach for my PRs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I should simply not regenerate the table outside of the initial setup.

Personally I think that would be preferable. Even if it wasn't potentially corrupting the color table, I wouldn't want it changing colors at runtime because of the performance implications. I have a number of games that use palette animation, which I suspect would be triggering this color table regeneration almost non-stop.

@jake-stewart
Copy link

There is active discussion at the Ghostty repo about approaching this slightly differently.

The idea is that light themes should not invert the palette so that ansi 16 remains black. This is with the intention of removing the breaking change of generating by default. Then, an opt-in option for "harmonious" colors which inverts the colors for light themes since there are advantages for doing so.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate 256 color palette from 16 color palette

4 participants