Background
Codex CLI previously implemented OSC 10/11 color queries at the application layer and later ran into input stream contention issues: the hard part isn’t sending the query sequence, but safely reading the reply. Because OSC color query replies share the same input stream as keyboard/mouse events, an application-layer implementation can easily race with the event loop, leading to timeouts, parse failures, or accidentally consuming/disrupting other input data.
In openai/codex#5935, Codex addressed this by depending on a crossterm fork that includes color query support, pushing this capability down into the library layer and coordinating it with crossterm’s event reading internally. That PR description also notes that it resolves some user-visible input issues (e.g. openai/codex#4945, openai/codex#5603).
Proposal
It would be great if crossterm could natively support color query APIs for OSC 10/11 and handle coordination with crossterm::event reading internally. For terminals that don’t support the feature, return None (or other suggestions):
use std::io;
use crossterm::style::Color;
pub fn query_foreground_color() -> io::Result<Option<Color>>;
pub fn query_background_color() -> io::Result<Option<Color>>;Notes:
- The exact behavior for cases like timeouts / unrecognized replies is open to suggestions.
Goals
- Reduce ecosystem fragmentation so other users with similar needs can rely on upstream crossterm directly;
- Reduce Codex’s long-term cost of tracking crossterm updates, and make it feasible to return to upstream crossterm instead of relying on a fork indefinitely.
References
- Reference implementation: nornagon/crossterm@87db8bf (color-query)
- Spec: xterm
OSC 10/11“dynamic colors” (foreground/background) and thePt=?query semantics: XTerm Control Sequences (XFree86 mirror) (see “Operating System Controls”)
If this direction makes sense, I’m happy to submit a minimal PR for discussion.
Note: Since I’m not a native English speaker, I used an AI agent to help draft the wording, and I reviewed/edited it before posting.
Activity
3This has been on my list of things I'd like to see in Crossterm since 2025, when I was working at OpenAI and discussed it directly with Jeremy (@nornagon) as it was being used in Codex. My initial thought was that we could probably bring his changes over fairly directly. Looking into Codex's experience since then, though, there's considerably more involved.
My recollection from discussing this with Jeremy is that some of the difficulty comes from Crossterm's async architecture: the async interface is layered over a thread-driven reader with shared locking, rather than asynchronous operation being built through the input core. That makes coordinating queries, replies, cancellation, and timeouts awkward. Moving color-query handling into Crossterm avoids a competing application-level reader, but does not by itself resolve those coordination problems.
Several parts of Codex's experience are relevant:
-
Sharing the input stream caused real user-visible bugs. The original Codex change addressed truncated drag-and-drop paths and terminal color replies appearing in the input field. Query replies and user input arrive together, so reading either independently can consume bytes intended for the other. This is a strong reason for handling queries within the library that owns input.
-
Using the internal reader still leaves concurrency problems. Crossterm's existing query-concurrency issue describes queries contending with
EventStreamfor the reader mutex. Codex later received reports of freezing during focus-triggered palette queries, and removed those queries from focus handling because they could block input and discard keystrokes. A timeout around polling does not necessarily bound the entire operation, including acquiring the reader and collecting the response. -
Optional queries became a noticeable startup cost. Codex introduced bounded startup probes, then batched queries under a shared deadline. Waiting longer accommodates delayed replies, but a terminal that does not answer should not add several seconds to startup. The API also needs clear semantics for missing and unrecognized responses: silence cannot reliably distinguish an unsupported query from a delayed or filtered reply.
-
Related parser-recovery problems have already been reported in Crossterm. Unrecognized DEC-private reports can leave subsequent input buffered because a completed but unsupported sequence is treated as incomplete. The original OSC patch has the same failure pattern for unsupported or invalid slots. Separately, an unfinished bracketed paste can indefinitely retain input. These are distinct bugs, but they expose related requirements: distinguish incomplete input from completed unsupported sequences, and define recovery when completion never arrives.
-
Stopping a query does not make its remaining reply disappear. Replies can be fragmented, arrive late, or be interleaved with keys and paste data. Codex subsequently added startup input preservation and replay to handle consumed input and partial sequences. Recovery must preserve ordinary input without turning leftover protocol bytes into user actions. The legacy Alt-
]encoding, which shares the OSC prefix, adds another ambiguity. A query deadline alone does not settle how the parser should handle any of these cases.
There are also smaller implementation issues in the reference patch. Its /dev/tty path opens a read-only handle and then attempts to write through it, causing a fallback to stdout. Its RGB conversion computes 0x8000 / 257 as 127 while the added tests expect 128. Those details need correction, but they are relatively straightforward compared with the input ownership and recovery questions.
On compatibility, the original color-query patch adds a private InternalEvent variant, so it does not itself break exhaustive matches on public Event. Live color-scheme notifications do extend that public enum. Making Event non-exhaustive would also be a breaking change, and is a broader API-policy decision already discussed in the bracketed-paste issue.
So yes, the capability makes sense, but I don't think importing the existing patch gets us close to a complete solution. It opens up substantial engineering work, including existing problems that are only indirectly related to color queries and would need to be resolved along the way. Given that depth and scope, this isn't something I'd want to invest time in myself. Jeremy's implementation, Codex's subsequent changes, and the existing query and color-query proposals provide useful context for anyone taking it on, but I wouldn't characterize it as a straightforward patch import.
Assisted by Codex in digging into the implementation, related Crossterm issues, and the deeper problems and subsequent fixes in Codex's terminal input handling. This is based on source and discussion review, rather than new terminal reproductions.
@MartenBE Thanks for following up. I did try implementing this after opening the issue, but I found it much harder to make robust on top of Crossterm’s current input architecture than I initially expected. @joshka’s analysis matches the difficulties I ran into.
For a specific application such as Codex, it may be possible to make a usable implementation by controlling when queries run and accepting some application-specific constraints. For a general-purpose library, though, I would want stronger guarantees around interaction with the event reader, timeouts, late or partial replies, and preserving ordinary user input.
My current impression is that a reliable upstream implementation would need substantial changes to the input-reading and parsing architecture, rather than just adding OSC 10/11 support. I don’t currently have a solution that I would be comfortable proposing for general use.
I also opened a similar PR for termwiz, the terminal library in the WezTerm repository: wezterm/wezterm#7690. Its streaming VT parser already understands OSC dynamic-color sequences, which seems like a better foundation for exploring this. That PR is still a draft, however: the current probe can block when a terminal does not reply and can consume unrelated input, so it still needs a bounded no-response path and better input handling.
I still think this capability would be useful, but I underestimated the work needed to make it reliable at the library level.
This seems very handy, is there any update on this?