pub enum Terminator {
St,
Bel,
}Expand description
Which byte ended an OSC sequence — and therefore which one ends its reply.
The engine relays this rather than choosing: a query event carries the
terminator the request arrived with, and the consumer hands it back to the
matching report_*. Under ADR-0017 the parse-time fact is a mechanism only
the engine can observe, while which terminator to send is policy — and a
consumer cannot exercise a policy on a fact it was never given, which is what
#836 measured: bell_terminated was discarded at the parser boundary before
any event was queued.
The spec settles the direction, not just the reference tally.
ctlseqs.txt:2020 — “XTerm accepts either BEL or ST for terminating OSC
sequences, and when returning information, uses the same terminator used in a
query. While the latter is preferred, the former is supported for legacy
applications.” Under ADR-0004 that outranks every implementation, this one
included. On the colour path all three implementations that echo carry the
terminator outward with the request rather than remembering it: alacritty
binds it into the reply formatter it sends its consumer
(alacritty_terminal/src/term/mod.rs:1678), ghostty makes it a field on the
parsed command (src/terminal/osc.zig:87, written at
src/termio/stream_handler.zig:1497), and xterm threads it as a parameter
(misc.c:3567, emitted at :3593). xterm.js is the one that always sends ST
(CoreBrowserTerminal.ts:239) — which is what this engine used to do.
The colour qualifier is load-bearing, because OSC 52 has a counterexample.
xterm does store the terminator for the clipboard: int base64_final; on the
screen (ptyx.h:2637), written when the request is parsed (misc.c:3389) and
read a file away when the paste finally arrives (button.c:2218), because its
X selection retrieval is asynchronous. That is shape (b) — the one this engine
weighed and did not take — on the exact sequence report_clipboard answers.
It is recorded here because it strengthens the choice rather than undoing
it. xterm stores precisely because the reply is detached from the request, and
its store is a single scalar, so two overlapping paste requests would collide
exactly as one stored terminator does here. Detachment is the condition
crate::Engine::drain_events creates for every family at once, not just one —
so carrying answers it where storing only postpones it.
Carrying beats remembering for a reason that is this channel’s.
crate::Engine::drain_events hands over a batch, so a consumer can hold two
colour queries at once and answer them in either order; one remembered scalar
could not say which exchange it belonged to. An occurrence’s payload is
detached from its instant by the queue — ADR-0029 D4 records the same shape
for coordinates — so there is no re-ask and the fact must ride the event.
Exhaustive on purpose (#843’s rule). The space is closed at exactly two,
with a date for each (ctlseqs.txt:2024-2028), so there is no member a later
slice may name and nothing for #[non_exhaustive] to preserve.
⚠ The closure rests on the input space, not on the spec alone (#847). ECMA-48
gives ST a third encoding — the 8-bit C1 0x9C — and it is absent here because
crate::Engine::feed does not treat a lone 0x80..=0x9F byte as a control at
all, not because the spec stops at two. A reader who finds 0x9C in ctlseqs.txt
and concludes this enum is missing a member has the reasoning backwards: were that
contract ever revisited, the member would follow, and it is the contract that is
load-bearing.
ghostty reaches the same shape independently — src/terminal/osc.zig:252 is a
two-member { st, bel } with no trailing _, which is Zig’s marker for an
open enum and is used at 23 other sites in that tree. Convergence on both the
partition and the closure is the non-arbitrariness signal.
Variants§
St
ESC \ (ST), the terminator ECMA-48 documents and xterm prefers.
The default, and what an OSC ended by any other byte that ends one
resolves to. Those streams are real rather than theoretical: vte ends a
string on three byte classes — BEL, the cancel pair CAN/SUB (0x18 /
0x1a), and a bare ESC opening the next sequence — and only the first is
reported as bell-terminated. So a cancelled query is still relayed, and
answered ST.
Read “any other byte that ends one” strictly: the 8-bit C1 ST (0x9C) is
not a fourth class (#847). It does not end the string, so there is no event
to carry a terminator and nothing resolves to this variant — the OSC stays
open instead. See crate::Engine::feed for why that is a contract.
That is the right answer, not a fallback: xterm hardcodes ST on exactly this
shape (charproc.c:8964, “should be ST”) and ghostty’s Terminator.init
returns .st for a missing byte (src/terminal/osc.zig:263).
Bel
BEL (0x07), supported for legacy applications.
Real applications still emit it: nvim 0.8.0 asks ESC ] 11 ; ? BEL in
this crate’s own cursor_color_nvim.raw fixture. And the shell idiom
printf '\e]11;?\a'; read -d $'\a' reads until BEL, so an ST answer to
a BEL question blocks that read until it times out.
Trait Implementations§
Source§impl Clone for Terminator
impl Clone for Terminator
Source§fn clone(&self) -> Terminator
fn clone(&self) -> Terminator
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more