Skip to content

Implement the colour palette behind OSC 4 and OSC 10/11/12 - #21

Merged
tomlm merged 2 commits into
tomlm:mainfrom
JohnCampionJr:osc-color-palette
Aug 24, 2026
Merged

Implement the colour palette behind OSC 4 and OSC 10/11/12#21
tomlm merged 2 commits into
tomlm:mainfrom
JohnCampionJr:osc-color-palette

Conversation

@JohnCampionJr

Copy link
Copy Markdown
Collaborator

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:

"10" => $"\u001b]{colorType};rgb:ff/ff/ff\u0007",  // Foreground
"11" => $"\u001b]{colorType};rgb:00/00/00\u0007",  // Background
"12" => $"\u001b]{colorType};rgb:ff/ff/ff\u0007",  // Cursor

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

ColorPalette holds the 256 indexed colours plus foreground, background and cursor, seeded from TerminalOptions.Theme - which already existed and was read by nothing. That is how an embedder declares a light terminal, and ApplyTheme re-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 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 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, so rgb:f/0/0 is full red, not 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, not 0xff00.
  • 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 (the 0 to 95 step is larger, so the darkest cell is properly black), and the greyscale ramp excludes pure black and white since the cube already has them.
  • IsLightBackground is 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. A ColorChanged event 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

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>

Copilot AI left a comment

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.

🟡 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.
  • ResetAllColors should suppress ColorChanged when 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

  • Rgb is meaningful for foreground/background/cursor notifications too: those setters intentionally create args with Index == -1 while passing the new RGB value. This documentation would cause consumers to discard valid color data; limit the caveat to wholesale changes (Target == All or indexed changes with Index == -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.

Comment thread src/XTerm.NET/Common/ColorPalette.cs
@tomlm
tomlm merged commit c144947 into tomlm:main Aug 24, 2026
1 check passed
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.

3 participants