Close out the open fixes and add bin!, snapshot_after and wait_stable for v0.9.0 - #243
Merged
Conversation
`inspect --help` spawned a program called `--help` and failed with a PATH error, because the argument loop knew only `--size` and took the next argument as the program whatever it looked like (#229). Both timings were hardcoded too, so a program slower than five seconds to paint could not be inspected: the wait expired and the viewer printed a partial screen (#236). The loop now consumes options until the first non-option: - `-h`/`--help` print the usage to stdout and exit 0; a missing program prints the same text to stderr and exits 1. The usage text has one home. - `--timeout SECONDS` (default 5) sets the builder deadline; `--idle MILLIS` (default 300) the silence window for the still-running path. A malformed value is rejected in one line, shaped like the `--size` diagnostic. - `--version` prints the termlens version; an unknown option is refused rather than spawned; `--` ends option parsing. The still-running settle is bounded by the deadline as before, or by the silence window itself when that is longer, so `--idle 10000` cannot make the settle time out before it has had a chance to see silence. Closes #229 Closes #236 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
A relative program path is how the viewer is pointed at something just built, and it resolves only because a child now starts in the test process's working directory rather than $HOME (#215). The test pinning that default reads `pwd` inside a shell; nothing pinned the mechanism the viewer relies on, so it could stop working unnoticed (#237). The new case links /bin/echo into CARGO_TARGET_TMPDIR, runs inspect from there with `./echo`, and asserts the program's output reaches the screen. A symlink rather than a copy: macOS kills a system binary copied out of /bin, since its signature is trusted at that path only. Closes #237 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
#217 taught the reader to turn a byte that is not UTF-8 into U+FFFD instead of dropping it, and asked for two guards: an inline test, which landed as tests/utf8.rs, and a fixture line, so a parser change that reintroduces the drop turns a snapshot red rather than depending on one printf staying alive. The fixture line never landed (#230). unicode-torture now writes `raw: caf\xe9 done` straight to stdout — a string literal cannot hold the byte — and the reviewed snapshot shows the replacement character with `done` in the column it would occupy had the byte decoded. The snapshot test waits on row 8 now that the fixture prints one more line. Closes #230 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
The default-working-directory test read pwd's output from row 0 of an 80-column grid, so a checkout whose absolute path is longer than 80 characters wrapped the path onto row 1 and the test failed on canonicalize — deterministically, in both profiles, for a reason that has nothing to do with the behaviour it pins (#240). A wrap inserts no character of its own, so the visible screen with its row breaks removed is the path as printed, whatever its length. Verified from a 201-character directory: the old form fails there, this one passes, and both still fail if spawn stops defaulting to the test process's directory. Closes #240 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
`ESC ( A` designates the DEC United Kingdom set, whose one difference from ASCII is that `#` draws `£`. The tracker parsed the designation and then rendered ASCII, so a price printed in the UK set showed `#42` on the grid, a test asserting `£42` failed against a correct application, and a snapshot that blessed `#42` kept passing (#234). The charset enum gains a `Uk` variant, the designation parser recognises `A`, and the glyph lookup — renamed from graphics_glyph to charset_glyph now that it serves two sets — maps the single byte `#` to `£` for it. The staging path in the backend already rewrites a byte into the glyph it draws, so nothing downstream changes. Designations with no table (the alternate ROMs, the other national sets) still read as ASCII, and the module docs and README now say which sets are translated. Closes #234 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
Save, jump, draw the frame, restore is how a full-screen application draws a border, and DECRC (ESC 8) left the character sets as whatever was designated in between, so the border after it rendered as `lqk` — the failure #204 fixed for the designation itself, arriving through a different door (#232). The tracker gains a saved slot holding G0–G3 and the locking shift. ESC 7 fills it, ESC 8 restores it, a restore with nothing saved returns to the power-on defaults as xterm does, and RIS clears the slot so a restore after a reset cannot resurrect a designation from before it. vt100 saves and restores the cursor and attributes itself; only the half it does not know about lives here. Closes #232 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
`CSI ! p` parsed cleanly and did nothing, so an application that soft-reset on teardown and then printed text had that text rendered in the character set it had just told the terminal to forget — while RIS got the same case right, leaving the pair inconsistent rather than uniformly unimplemented (#233). The tracker recognises the `!` intermediate and, on `p`, returns its own state to power-on: the character sets and the DECSC slot, the cursor shape, focus reporting. It then reports a SoftReset event, and the backend replays to vt100 — which does not implement DECSTR — the sequences that reset the modes it holds: cursor-key mode, bracketed paste, every mouse tracking mode and encoding, and cursor visibility. The alternate screen is left alone, as specified. Attributes, margins, origin and insert modes and the keypad are deliberately not replayed: nothing on Screen observes them, and a replay nothing can check is a claim nothing can catch. The README lists both halves. Closes #233 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
…d for The backend collapses ?9/?1000/?1002/?1003 into one mutually exclusive value — right for the input path, since a terminal reports in exactly one protocol and `click` must encode for it — so it could not say which members of the group an application enabled. crossterm's EnableMouseCapture sends three at once and only the last survived: a regression from any-motion to button-motion tracking (losing hover) was invisible, and a DECRQM probe for any member but the last had to be answered "not recognized" (#151). The sequence tracker now keeps the requested set, beside the focus flag and the window title that live there for the same reason. Every mode named in one `CSI ? … h/l` list is set or cleared together; RIS and DECSTR empty the set. Screen::mouse_modes reports it as a small MouseModes set (contains/is_empty/len/iter), DECRQM answers each tracking mode from it, and Screen::mouse_mode and the input path keep the backend's one-protocol answer unchanged. DESIGN.md's honesty paragraph is rewritten around the two facts. Closes #151 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
install.yml is the only job that builds termlens from outside this workspace, and it did so in one shape — `--features decode`, the one the fewest real consumers use: of the three in-house ones, launchbound and reconverge declare default-features = false and only mossaic takes a feature. Feature resolution across a registry boundary is exactly the class this workflow exists to catch, which is why #226 recorded the gap as deferred rather than dropped (#238). The matrix gains a `features` dimension over `decode` and `no-default-features`, on both operating systems. The registry step demands the `decode` feature only on the leg that asks for it; the consumer step picks the matching `cargo add` flag; the decode test moves to its own file written only on that leg; and the no-defaults leg fails if the consumer's tree still resolves `insta`, the whole of the default feature's dependency tree. ci.yml's deferral note now says this half is done. Closes #238 Signed-off-by: Vyncint Ng <vyncint@icloud.com>
Every integration test of a binary opened with the same five lines: a
fixed 80x24 grid so snapshots are stable, env_clear() so nothing on the
host leaks into the program under test, a five-second deadline so a hang
is a readable failure, and spawn(env!("CARGO_BIN_EXE_myapp")). The chain
now has a name — `termlens::bin!("myapp")?` — and builder calls after the
name override any default: `bin!("myapp", size(120, 40), env("NO_COLOR",
"1"))`. A macro_rules macro, so it costs no dependency and hides no
Terminal; a misspelled binary is a compile error naming the variable
rather than a spawn failure at run time.
CARGO_BIN_EXE_* exists only in the integration tests of the package that
owns the binary, so the macro's tests live in the hello-tui fixture, which
gains termlens as a dev-dependency without default features — keeping the
features job's --no-default-features leg an actual no-insta build.
Roadmap P0.2 (ecosystem docs/TERMLENS-ROADMAP.md).
Signed-off-by: Vyncint Ng <vyncint@icloud.com>
wait_until guarantees only that the bytes which made the predicate true have been processed, and nothing marks where a repaint ends, so the common "wait for X, then snapshot the whole screen" needs three rules from DESIGN §2 to be race-free — and the first real user tripped over them. And the settle those rules ask for, wait_idle, is reset by bytes: an application that rings the bell, rewrites a cell with the glyph already in it or answers a query never goes silent, so a suite for such an application had no honest way to settle at all. snapshot_after(pred) waits for the predicate, then for the picture to hold still for 100ms, and returns that screen. wait_stable(quiet) is the settle on its own: what resets its clock is a change to the grid — any cell, the cursor or the size — not the arrival of bytes, checked under the same mid-sequence and open-synchronized-update conditions wait_idle uses, with stillness that predates the call counted and EOF counted as still. It returns the newest observation of the still picture, so counters on it are current. Both have _for twins. Roadmap P0.3 (ecosystem docs/TERMLENS-ROADMAP.md). Signed-off-by: Vyncint Ng <vyncint@icloud.com>
The crate-level snapshot example took a screen with no wait in front of it, which is the shape DESIGN §2 warns against; it now goes through snapshot_after, and the paragraph after it points a reader testing their own binary at bin!. Signed-off-by: Vyncint Ng <vyncint@icloud.com>
The stress workflow failed one of the new tests at 16 threads on macOS: the counter it drove with a shell loop and `sleep 0.02` stalled for longer than the 150ms of stillness the test asked for — a process spawned per iteration on a loaded machine — so the picture genuinely held still and wait_stable was right to return it. The premise was load-sensitive, not the library. Every timing premise in the file is now one that scheduling cannot defeat: the changing screen is `seq` scrolling as fast as the PTY takes it (one process, no per-iteration spawn), the stillness asked for is far longer than the deadline so the wait can only time out, the bell loop is a builtin without `sleep` and its wait_idle expectation is shaped the same way, the "already still" case compares a full second against half of one, and the two-paint case leaves two seconds between paints. Signed-off-by: Vyncint Ng <vyncint@icloud.com>
vyncint
force-pushed
the
feat/v0.9-close-out
branch
from
September 5, 2026 01:28
08f8c1b to
b2f70e4
Compare
cargo-deny treats a path dependency without a version requirement as a wildcard, and the hello-tui fixture now dev-depends on termlens by path for the bin! macro's test. Every fixture is publish = false, so a version requirement there would be a number to bump on every release and nothing more; allow-wildcard-paths is the option cargo-deny provides for exactly this, and wildcards on registry dependencies stay denied. Signed-off-by: Vyncint Ng <vyncint@icloud.com>
Merged
vyncint
added a commit
that referenced
this pull request
Sep 5, 2026
workspace.package.version 0.8.0 -> 0.9.0 with the lockfile refreshed, and the CHANGELOG's [Unreleased] section moved to [0.9.0] - 2026-09-05 with a fresh empty [Unreleased] above it. Nothing in the docs names 0.8 as current. The tree is the one #243 merged, which the stress workflow passed 10/10 shards on. Signed-off-by: Vyncint Ng <vyncint@icloud.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes every open issue that is a fix rather than a design decision, and adds the two APIs the 0.9.0 release was waiting on. One commit per item, each with its own test and CHANGELOG entry.
Fixes (issues verified against
main@ 63d7966 before each change)inspect--help/-hprint one usage text and exit 0;--timeout SECONDSand--idle MILLISreplace the hardcoded 5 s / 300 ms;--version; unknown options are refused rather than spawnedinspect/bin)unicode-torturewrites a raw0xE9; the reviewed snapshot shows U+FFFD withdonein its proper columnESC ( Athen#draws£DECSC/DECRCsave and restore G0–G3 and the locking shift; nothing saved → ASCII;RISclears the slotDECSTRmodelled: charsets,DECSCslot, cursor shape, focus, and — replayed to vt100 — DECCKM, bracketed paste, mouse tracking/encoding, cursor visibility; alternate screen left alone; the unmodelled remainder is listed in the READMEScreen::mouse_modes()reports it,DECRQManswers each mode on its own evidence,mouse_mode()and the input path are unchangedinstall.ymlmatrix gainsdecode×no-default-features; the registry check demandsdecodeonly on that leg; the no-defaults leg fails if the consumer's tree resolvesinsta; ci.yml's #226 note updated. Both legs dry-run locally against the published 0.8.0Two new APIs
termlens::bin!("myapp", size(120, 40), …)— the builder chain every binary test starts from, as amacro_rulesmacro. Tested from thehello-tuifixture, the one placeCARGO_BIN_EXE_*exists; the fixture's dev-dependency on termlens isdefault-features = falseso thefeaturesjob's no-defaults leg stays a real no-instabuild.snapshot_after(pred)(predicate → settle → the settledScreen) andwait_stable(quiet)(reset by changes, not bytes, so bells and no-op repaints cannot hold it up). Both have_fortwins and refuse to settle inside an open synchronized update. DESIGN §2 documents both and the route table gains a row. This is a wait/timing change, so stress.yml ran on this branch. The first run (https://github.com/vyncint/termlens/actions/runs/33935865723) failed one of the new tests at 16 threads on macOS: the test drove a counter with a shell loop andsleep 0.02, the spawn stalled past the 150 ms of stillness it asked for, andwait_stablewas right to report a still picture. The tests now use premises scheduling cannot defeat (seqflooding, stillness asked for far longer than the deadline, builtin loops withoutsleep); the re-run on the fixed tests is https://github.com/vyncint/termlens/actions/runs/33936252643. The only commit after it changesdeny.toml.Deferred to its own PR, deliberately: a Rust fixture binary replacing every
/bin/shhelper touches every test file and is a precondition for Windows rather than for 0.9.0. The four design issues (#146, #147, #149, #150) have status comments and stay open.Verification
cargo fmt --check; clippy-D warningsin all four feature configurations; the full suite in all four configurations plus the release profile;cargo doc -D warnings(both configurations); MSRV 1.85check --all-targets;cargo deny; the CI-gates-listed script; DCO and no-AI-attribution checks over the range.fixtures__unicode_torture_renders_with_correct_widths.snap.cargo denyneededallow-wildcard-pathsfor the fixture's path dev-dependency on termlens; registry wildcards stay denied.