Tell an ST-terminated OSC from an interrupted one #110

Merged
natsukium merged 1 commit from vt/osc-terminator into main 2026-10-12 15:11:42 +09:00
Owner

Second step of #67. The parser dispatched an OSC body as soon as an ESC arrived in the string, so a sink could not tell ESC ] … ESC \ from ESC ] … ESC [0m. OSC 7501 may apply a report only once BEL or ST actually ended it, so the grid wiring needs that distinction.

What changes

  • Sink::osc_dispatch(body, bell_terminated: bool) becomes osc_dispatch(body, end: OscEnd) with OscEnd::{Bel, St, Interrupted}.
  • An ESC that leaves the OSC string holds the body until the next byte arrives. \ dispatches the body as St, and any other byte (including ESC, CAN, SUB, or a C0 control) dispatches it as Interrupted. That byte is then processed exactly as before, so sinks see events in the same order.
  • CAN/SUB inside the body still dispatch an empty body, now marked Interrupted.
  • The grid ignores end, just as it ignored bell_terminated, so every OSC applies as before. The only difference is timing: an OSC ended by a lone ESC dispatches when the following byte arrives, not at the ESC itself.
  • The new osc_pending flag is carried in the parser's upgrade dump. check_restored rejects a pending OSC outside Escape. The dump has not shipped in a release, so DUMP_VERSION stays the same.

This is an API break for out-of-tree Sink implementors.

Tests

  • Unit tests:
    • an OSC interrupted by a CSI or an ESC dispatch, or by a second OSC
    • a doubled ESC
    • CAN/SUB or a C0 control after the ESC
    • an ESC at a chunk end, completed by the next chunk as ST or as an interruption
    • consecutive ST-terminated OSCs
    • an ESC outside any OSC, which dispatches no OSC
  • New property osc_dispatch_reports_how_each_osc_ended: a random stream of OSCs, each with a random ending, fed at random chunk boundaries, must yield exactly the expected (body, end) list.
  • The state-dump test that resumes a parser stopped mid-sequence now includes an interrupted OSC, split at every byte.
  • cargo-mutants on the diff: every mutant is caught except transition_to's !matches!(next, State::OscString) guard, which is the equivalent mutant already documented above that match.
  • just check passes.
Second step of #67. The parser dispatched an OSC body as soon as an ESC arrived in the string, so a sink could not tell `ESC ] … ESC \` from `ESC ] … ESC [0m`. OSC 7501 may apply a report only once BEL or ST actually ended it, so the grid wiring needs that distinction. ## What changes - `Sink::osc_dispatch(body, bell_terminated: bool)` becomes `osc_dispatch(body, end: OscEnd)` with `OscEnd::{Bel, St, Interrupted}`. - An ESC that leaves the OSC string holds the body until the next byte arrives. `\` dispatches the body as `St`, and any other byte (including ESC, CAN, SUB, or a C0 control) dispatches it as `Interrupted`. That byte is then processed exactly as before, so sinks see events in the same order. - CAN/SUB inside the body still dispatch an empty body, now marked `Interrupted`. - The grid ignores `end`, just as it ignored `bell_terminated`, so every OSC applies as before. The only difference is timing: an OSC ended by a lone ESC dispatches when the following byte arrives, not at the ESC itself. - The new `osc_pending` flag is carried in the parser's upgrade dump. `check_restored` rejects a pending OSC outside `Escape`. The dump has not shipped in a release, so `DUMP_VERSION` stays the same. This is an API break for out-of-tree `Sink` implementors. ## Tests - Unit tests: - an OSC interrupted by a CSI or an ESC dispatch, or by a second OSC - a doubled ESC - CAN/SUB or a C0 control after the ESC - an ESC at a chunk end, completed by the next chunk as ST or as an interruption - consecutive ST-terminated OSCs - an ESC outside any OSC, which dispatches no OSC - New property `osc_dispatch_reports_how_each_osc_ended`: a random stream of OSCs, each with a random ending, fed at random chunk boundaries, must yield exactly the expected `(body, end)` list. - The state-dump test that resumes a parser stopped mid-sequence now includes an interrupted OSC, split at every byte. - cargo-mutants on the diff: every mutant is caught except `transition_to`'s `!matches!(next, State::OscString)` guard, which is the equivalent mutant already documented above that match. - `just check` passes.
vt: tell an ST-terminated OSC from one an ESC interrupted
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m9s
windows / detect relevant changes (pull_request) Successful in 18s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 7m28s
bench / instruction-count regression gate (pull_request) Successful in 1m4s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 2m55s
pr / nix flake check (pull_request) Successful in 51s
pr / cargo build / clippy / test / deny (pull_request) Successful in 3m19s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
windows / frontend smoke (Windows) (pull_request) Successful in 1m9s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 50s
windows / cargo clippy (Windows cross) (pull_request) Successful in 26s
f8583ebd60
The parser dispatched an OSC body the moment an ESC arrived in the
string, so a sink saw `ESC ] … ESC [0m` exactly as it saw
`ESC ] … ESC \`. OSC 7501 program status may apply a report only once a
real terminator arrived; the sink needs to know which one it got.

`Sink::osc_dispatch` now takes an `OscEnd` (Bel, St, Interrupted) in
place of `bell_terminated`. An ESC leaving the OSC string holds the
body until the next byte: `\` dispatches it as St, anything else as
Interrupted, and that byte is then processed as before, so the event
order a sink sees is unchanged. CAN/SUB inside the body still dispatch
an empty body, now marked Interrupted.

The grid ignores the ending as it ignored `bell_terminated`, so every
OSC is applied as before; only the timing moves: an OSC ended by a lone
ESC dispatches when the byte after that ESC arrives, not at the ESC.
The pending flag rides the parser's upgrade dump, which has not shipped
in a release yet, so the dump version stays.

Assisted-by: Claude Code
natsukium deleted branch vt/osc-terminator 2026-10-12 15:11:42 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!110
No description provided.