Implement the colour palette behind OSC 4 and OSC 10/11/12 - #21
Merged
Conversation
All three handlers were stubs. OSC 4 parsed its arguments and hit a TODO, the
resets did nothing at all, and colour QUERIES answered with constants:
"10" => rgb:ff/ff/ff
"11" => rgb:00/00/00
"12" => rgb:ff/ff/ff
The query is the one that does damage. Programs ask OSC 11 for the background to
decide whether they are on a light or a dark terminal, and a hardcoded black told
every one of them "dark" -- so a light terminal got dark-theme colours drawn onto
it, and nothing in the exchange looked like an error.
ColorPalette holds 256 indexed colours plus foreground, background and cursor,
seeded from TerminalOptions.Theme, which existed and was read by nothing. That is
how an embedder declares a light terminal; ApplyTheme re-seeds at runtime for one
that follows the OS setting.
Defaults and current values are separate layers, and reset restores the
EMBEDDER'S theme rather than a factory palette. Otherwise any program sending
OSC 104 drags a light terminal to black and leaves it there -- a reset that
un-configures the terminal is worse than one that does nothing.
Where no theme is set the defaults reproduce the previous hardcoded answers, so
this moves no colours for anyone; it only stops the answers being fiction.
Details that are easy to get wrong and are tested:
- rgb: channels are 1 to 4 hex digits and scale by WIDTH, so rgb:f/0/0 is full
red rather than 0x0f0000.
- Query replies use four digits per channel, as xterm emits and as probing
programs are written to read, widening by repetition so 0xff becomes 0xffff.
- OSC 4 takes any number of index/spec pairs; theme scripts send all sixteen ANSI
colours in one sequence.
- OSC 10 with several specs walks on to 11 and 12, so OSC 10;fg;bg sets both.
- The 6x6x6 cube uses xterm's uneven levels, and the greyscale ramp deliberately
excludes pure black and white.
- IsLightBackground is luma-weighted, not a channel average, which would call
pure blue light.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Avoid raising ColorChanged for no-op ResetAllColors calls.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds stateful, theme-aware color palette support for OSC 4 and OSC 10/11/12.
Changes:
- Adds indexed, foreground, background, and cursor color state.
- Implements parsing, formatting, queries, updates, resets, and change notifications.
- Adds comprehensive palette and color-spec tests.
ResetAllColorsshould suppressColorChangedwhen no values change.
File summaries
| File | Description |
|---|---|
src/XTerm.NET/Terminal.cs |
Exposes the terminal color palette. |
src/XTerm.NET/InputHandler.cs |
Handles OSC color operations. |
src/XTerm.NET/Common/ColorSpec.cs |
Parses and formats color specifications. |
src/XTerm.NET/Common/ColorPalette.cs |
Stores themed and current colors. |
src/XTerm.NET.Tests/ColorPaletteTests.cs |
Tests palette behavior and color parsing. |
Review details
Suppressed comments (2)
src/XTerm.NET/Common/ColorPalette.cs:55
- Terminal.Dispose clears the events declared on Terminal, but this newly exposed ColorChanged event is owned by Colors and remains subscribed after disposal. A renderer can still receive notifications from terminal.Colors.SetColor/ResetColor afterward, and the palette retains that subscriber; add a palette disposal or subscription-clear path and invoke it from Terminal.Dispose.
public event EventHandler<ColorChangedEventArgs>? ColorChanged;
src/XTerm.NET/Common/ColorPalette.cs:298
Rgbis meaningful for foreground/background/cursor notifications too: those setters intentionally create args withIndex == -1while passing the new RGB value. This documentation would cause consumers to discard valid color data; limit the caveat to wholesale changes (Target == Allor indexed changes withIndex == -1).
/// The new colour as 0xRRGGBB. Not meaningful when <see cref="Index"/> is -1.
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
tomlm
approved these changes
Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All three colour handlers are stubs today. OSC 4 parses its arguments and hits a
TODO, the resets do nothing at all, and colour queries answer with constants:Why the query is the one that does damage
Programs ask OSC 11 for the background to decide whether they are on a light or a dark terminal. A hardcoded black tells every one of them "dark" - so a light terminal gets dark-theme colours drawn onto it, and nothing in the exchange looks like an error. It is a wrong answer rather than a missing one.
The change
ColorPaletteholds the 256 indexed colours plus foreground, background and cursor, seeded fromTerminalOptions.Theme- which already existed and was read by nothing. That is how an embedder declares a light terminal, andApplyThemere-seeds at runtime for one that follows the OS light/dark setting.Defaults and current values are separate layers, and reset restores the embedder's theme rather than a factory palette. Otherwise any program sending
OSC 104drags a light terminal to black and leaves it there - a reset that un-configures the terminal is worse than one that does nothing.Where no theme is set, the defaults reproduce the previous hardcoded answers, so this moves no colours for any existing embedder. It only stops the answers being fiction.
Details that are easy to get wrong
All covered by tests:
rgb:channels are 1-4 hex digits and scale by width, sorgb:f/0/0is full red, not0x0f0000.0xffbecomes0xffff, not0xff00.OSC 4takes any number of index/spec pairs; theme scripts send all sixteen ANSI colours in one sequence.OSC 10with several specs walks on to 11 and 12, soOSC 10;fg;bgsets both.IsLightBackgroundis luma-weighted rather than a channel average, which would call pure blue light.Colour specs accept
rgb:R/G/B,#RGB/#RRGGBB/#RRRRGGGGBBBB, and the common colour names. AColorChangedevent lets a renderer repaint, and is not raised for no-op sets.Testing
39 new tests in
ColorPaletteTests.cs. Full suite: 646 passed, 0 failed.Context
Third of three independent PRs against
main, none stacked: #19 adds a general OSC passthrough event, #20 adds OSC 133 and OSC 9. This one is disjoint from both - it rewrites the colour handlers, which neither of the others touches.🤖 Generated with Claude Code