Skip to content

release: v0.10.2 — the CLI is verified, and unsupported() stops contradicting the cell - #339

Merged
vyncint merged 3 commits into
mainfrom
release/v0.10.2
Sep 10, 2026
Merged

vyncint merged 3 commits into
mainfrom
release/v0.10.2

Conversation

@vyncint

@vyncint vyncint commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Closes #326 and #320, then cuts 0.10.2.

Why a patch now

#310 is fixed on main and has never been published. termlens <subcommand> --version exits 2 in the published 0.10.0 and 0.10.1. It was found by a user of the published binary. That alone makes this release overdue; the README restructure (#337, #338) and the skill/CI commits ride along.

#326 — the published CLI is now installed and exercised

install.yml asked "does cargo add termlens work for a stranger?" and never asked the same of the binary. A cli job now installs termlens-cli from crates.io on Linux and macOS — every run, never from a cache, because a cached binary is evidence about whatever the registry served the day the cache was filled — and asserts every exit code termlens --help documents:

assertion expected
--version in all four positions, same string 0
inspect prints the screen, its header and its trailer 0
render --text/--svg/--html/--ansi, --svg starting <svg 0
diff, same picture 0
diff, different pictures 1
diff/render, input it cannot read; missing file; unknown subcommand 2

The assertions are a script that takes the binary, not a block inlined in the workflow, so the same contract runs against a path build. That is how it earned trust: deleting one match arm to re-create #310 made it fail on diff --versionexit 2, expected 0 — and restoring the arm made it pass.

ci.yml runs it on every pull request against target/debug/termlens. The published check is the net; this is the fast feedback, and #326's whole complaint is that the failure was found too late. gates-listed demanded the new commands in CONTRIBUTING §1, which is that gate doing its job.

#320unsupported() stops contradicting the cell

5, 6, 8, 9, 25, 28 and 29 are exactly what emu/shadow.rs exists to recover: vt100 drops them, they arrive at unhandled_csi, and the shadow parser puts the attribute on the cell anyway. So one screen said Style::blink was true and unsupported() said ^[[5m had been dropped, at the same instant — and assert!(screen.unsupported().is_empty()) could not pass for any application that blinks or strikes through.

Three tests, one per line of the done-when:

  • every recovered parameter, on and off, names nothing — and the attribute is on the cell, which is what makes it a fix rather than silence;
  • ^[[59m (underline colour, modelled by nobody) is still named;
  • ^[[5;59m is named whole, because dropping a mixed sequence would hide a real gap.

The residue, measured rather than assumed. ^[[1;5;31m is still named although bold, red and blink all reach the cell — 1 and 31 are vt100's to implement, and this tracker knows the shadow's set, not the backend's. Narrowing that needs vt100's own SGR surface enumerated, which is more than a patch should claim. A test records it so it is a documented edge, not a surprise.

shadow::carrier and unhandled::SHADOW_SGR_PARAMS describe the same set in two places and could drift silently in both directions. A test holds them equal; removing 29 from one was observed to fail it.

The styled_json snapshot loses ^[[8m/^[[28m — that fixture renders conceal, so it was reporting conceal as unimplemented on a screen that shows it. The fix, visible in a recording.

Verified

cargo test --workspace --all-features 518 passed, 0 failed; every feature configuration (--no-default-features, decode, regex, serde, default) green; clippy in four configurations; MSRV 1.85; rustdoc -D warnings; gates-listed; skill-snippets; the CLI contract against a path build.

After merge

Tag v0.10.2. The new install.yml leg then runs from release.yml's post-publish step — so 0.10.2 becomes the first release whose published CLI is actually proven rather than assumed.

Closes #320
Closes #326

Signed-off-by: Vyncint Ng 115854244+vyncint@users.noreply.github.com

`install.yml` answered "does `cargo add termlens` work for a stranger?" and
never asked the same of the binary. #310 was the cost: `termlens inspect
--version` exited 2 in the published 0.10.0, exited 2 again in 0.10.1, and
was found by a user of the published binary rather than by anything here.

A `cli` job installs `termlens-cli` from crates.io on Linux and macOS --
every run, never from a cache, because a cached binary is evidence about
whatever the registry served the day the cache was filled and this job
exists to be evidence about what it serves now.

It asserts every exit code `termlens --help` documents and STABILITY.md
promises: `--version` in all four positions returning the same string,
`inspect` printing a screen with its header and its trailer, `render` in
all four formats with `--svg` actually starting `<svg`, and `diff`
returning 0 for the same picture, 1 for a different one, and 2 for input it
cannot read.

The assertions are a script that takes the binary, not a block inlined in
the workflow, so the same contract runs against a path build. That is how a
check like this earns trust: re-introducing #310 by deleting one match arm
made it fail on `diff --version` -- exit 2, expected 0 -- and restoring the
arm made it pass.

`ci.yml` runs it on every pull request against `target/debug/termlens`. The
published check is the net; this is the fast feedback, and #326's whole
complaint is that the failure was found too late. `gates-listed` required
the new commands in CONTRIBUTING §1, which is that gate doing its job.

One thing this pinned rather than fixed: `termlens inspect > file` is not a
file `termlens render` will read -- the `--- exited:` trailer is a human
line, not part of the snapshot format, and the parser says "line 26: e".
The script strips it, and the round trip deserves its own issue.

Closes #326

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
…ented

`Screen::unsupported()` is documented as listing every sequence *the
emulator* did not implement. It was listing the backend's gaps instead: 5,
6, 8, 9, 25, 28 and 29 are exactly the parameters `emu/shadow.rs` exists to
recover -- vt100 drops them, they arrive at `unhandled_csi`, and the shadow
parser then puts the attribute on the cell anyway.

So one screen said `Style::blink` was true and `unsupported()` said `^[[5m`
had been dropped, at the same instant. A reader checking the list before
trusting a blink or masked-password assertion concluded a correct assertion
was unreliable, and `assert!(screen.unsupported().is_empty())` -- the
natural "did my app emit anything this harness cannot see?" check -- could
not pass for any application using either attribute.

An SGR whose every parameter the shadow recovers is now handled. One
parameter outside the set keeps the whole sequence, because dropping a
mixed sequence would hide a real gap: `^[[5;59m` is blink recovered and
underline colour modelled by nobody, and it stays.

Three tests, one per line of the issue's done-when. The third records the
residue rather than hiding it: `^[[1;5;31m` is still named although bold,
red and blink all reach the cell -- measured, not assumed. `1` and `31` are
vt100's to implement and this tracker knows the shadow's set, not the
backend's. Narrowing it means enumerating vt100's SGR surface, which is a
bigger claim than a patch should make.

`shadow::carrier` and `unhandled::SHADOW_SGR_PARAMS` describe the same set
in two places and could drift silently in both directions -- a parameter
added to one and not the other is either named while its attribute reaches
the cell, or dropped from the record while nothing carries it. A test holds
them equal; removing 29 from one list was observed to fail it.

The `styled_json` insta snapshot loses `^[[8m` and `^[[28m`: that fixture
renders conceal, so it was reporting conceal as unimplemented on a screen
that shows it. The fix, visible in a recording.

Closes #320

Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
@vyncint
vyncint merged commit 3ff0922 into main Sep 10, 2026
15 checks passed
@vyncint
vyncint deleted the release/v0.10.2 branch September 10, 2026 07:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant