Today — Screen::unsupported() is documented as listing "every sequence the emulator did not implement… so a test can tell a plausible-looking wrong grid from a right one". Measured against 0.10.1 from crates.io, driving a real ratatui application (taskboard, in vyncint/termlens-demo), one screen reports both of these at the same instant:
badge.blink = true
unsupported() = ["^[[9m", "^[[29m", "^[[5m", "^[[25m", "^[[59m"]
So SGR 5 is named as unimplemented on the same Screen whose Style::blink correctly reports the cell as blinking. The same holds for SGR 9/29 against Style::strikethrough — termlens-demo's hard.rs::done_titles_are_struck_through_as_well_as_dimmed and the_overdue_badge_blinks_and_plain_red_does_not have asserted both attributes successfully since 0.4.
Why this is wrong rather than merely noisy — 5, 25, 9 and 29 are exactly the four parameters emu/shadow.rs exists to recover. They reach Callbacks::unhandled_csi because vt100 drops them, and the second parser then puts them on the cell anyway. unsupported() is reporting the backend's gap rather than the emulator's, and the emulator's is what the accessor promises. DESIGN.md §4 makes the same distinction: what the backend does not do, termlens does in front of it.
The consequence is precisely the one the accessor was added to prevent, inverted: a user checking unsupported() before trusting a masked-password or blink assertion is told the sequence was dropped, and concludes a correct assertion is unreliable. It also makes assert!(screen.unsupported().is_empty()) — the natural "did my app emit anything this harness cannot see?" check — impossible to write for any application that uses blink or strikethrough.
^[[59m (underline colour: default) in the same list is correct and should stay: nothing models underline colour.
Reproduction — against published 0.10.1, no path dependency:
let t = /* spawn an app that emits SGR 5 and SGR 9 */;
let s = t.screen();
let (row, col) = s.find("! Handle SIGWINCH").expect("a blinking cell");
assert!(s.cell(row, col).unwrap().style().blink); // passes
assert!(!s.unsupported().iter().any(|q| &**q == "^[[5m")); // fails
Fix — the tracker already filters the shapes it handles itself before recording (TRACKED_PRIVATE_MODES, the CSI/ESC/OSC lists in emu/unhandled.rs). The SGR parameters the shadow recovers belong in that filter: an SGR whose parameters are entirely within {5, 6, 8, 9, 25, 28, 29} is implemented and should not be recorded. Careful with mixed sequences — ^[[1;5;31m carries a parameter the shadow does not carry, and dropping the whole sequence from the record would hide a real gap; deciding per parameter rather than per sequence is the safer shape, and worth a test either way.
Done when
Found by termlens-demo while upgrading it from 0.6.1 to 0.10.1 — the testing tier's purpose is exactly this: a claim measured against the published crate by an application the crate's own suite does not contain.
Today —
Screen::unsupported()is documented as listing "every sequence the emulator did not implement… so a test can tell a plausible-looking wrong grid from a right one". Measured against 0.10.1 from crates.io, driving a real ratatui application (taskboard, invyncint/termlens-demo), one screen reports both of these at the same instant:So
SGR 5is named as unimplemented on the sameScreenwhoseStyle::blinkcorrectly reports the cell as blinking. The same holds forSGR 9/29againstStyle::strikethrough—termlens-demo'shard.rs::done_titles_are_struck_through_as_well_as_dimmedandthe_overdue_badge_blinks_and_plain_red_does_nothave asserted both attributes successfully since 0.4.Why this is wrong rather than merely noisy —
5,25,9and29are exactly the four parametersemu/shadow.rsexists to recover. They reachCallbacks::unhandled_csibecause vt100 drops them, and the second parser then puts them on the cell anyway.unsupported()is reporting the backend's gap rather than the emulator's, and the emulator's is what the accessor promises.DESIGN.md§4 makes the same distinction: what the backend does not do, termlens does in front of it.The consequence is precisely the one the accessor was added to prevent, inverted: a user checking
unsupported()before trusting a masked-password or blink assertion is told the sequence was dropped, and concludes a correct assertion is unreliable. It also makesassert!(screen.unsupported().is_empty())— the natural "did my app emit anything this harness cannot see?" check — impossible to write for any application that uses blink or strikethrough.^[[59m(underline colour: default) in the same list is correct and should stay: nothing models underline colour.Reproduction — against published 0.10.1, no path dependency:
Fix — the tracker already filters the shapes it handles itself before recording (
TRACKED_PRIVATE_MODES, the CSI/ESC/OSC lists inemu/unhandled.rs). The SGR parameters the shadow recovers belong in that filter: an SGR whose parameters are entirely within{5, 6, 8, 9, 25, 28, 29}is implemented and should not be recorded. Careful with mixed sequences —^[[1;5;31mcarries a parameter the shadow does not carry, and dropping the whole sequence from the record would hide a real gap; deciding per parameter rather than per sequence is the safer shape, and worth a test either way.Done when
blink/conceal/strikethroughdoes not name the SGR that set them inunsupported().^[[59mand other genuinely unmodelled SGRs are still reported.Found by
termlens-demowhile upgrading it from 0.6.1 to 0.10.1 — the testing tier's purpose is exactly this: a claim measured against the published crate by an application the crate's own suite does not contain.