Added
-
Error::Emulator: an emulator panic is a diagnosis, not a timeout. The
emulation runs on the reader thread, so a panic there propagated nowhere —
the drain died, the screen froze, and every wait burned its full deadline
reporting a predicate that could never come true. The reader now catches it,
records it, and keeps draining (a stalled drain blocks the child writing
into a full buffer);wait_until,wait_frameandwait_idlefail at once
with the emulator's own message and the last screen taken before the
failure. The emulator is never asked for a screen again — after a panic its
state means nothing.wait_exitis deliberately unaffected: the child's
exit status is still true. (#211) -
snapshot_afterandwait_stable: the whole-screen snapshot as one
call, and a settle that output changing nothing cannot hold up.
snapshot_after(pred)waits for the predicate, then for the picture to
hold still for 100ms, and returns that screen — DESIGN §2's three rules
for race-free waits without having to remember them.wait_stable(quiet)
is the settle on its own, and differs fromwait_idlein what resets the
clock: changes rather than bytes, so a bell, a cell rewritten with the
glyph already in it or an answered query — outputwait_idlecan never
see silence through — is invisible to it. Both have_fortwins, refuse
to settle inside an open synchronized update, count stillness that
predates the call, and return the screen they settled on. -
termlens::bin!("myapp")spawns one of your package's binaries under
the harness defaults. Every integration test of a binary opened with the
same five lines — a fixed 80x24 grid,env_clear(), a five-second
deadline,spawn(env!("CARGO_BIN_EXE_myapp"))— so the chain has a name.
Builder calls follow the name and override any default:
termlens::bin!("myapp", size(120, 40), env("NO_COLOR", "1"))?. A
misspelled binary is a compile error naming the variable, not a spawn
failure at run time. -
Screen::mouse_modesreports every mouse tracking mode the application
enabled, andDECRQManswers each one on its own evidence. The backend
collapses?9/?1000/?1002/?1003into the one protocol a terminal
reports in — right for the input path, and unchanged there — so it could
not say which members of the group an application asked for: crossterm's
EnableMouseCapturesends three at once and only the last survived, a
regression from any-motion to button-motion tracking (losing hover) was
invisible, and aDECRQMprobe for any member but the last had to be
answered "not recognized". The sequence tracker now keeps the requested
set;mouse_mode()still reports the protocol. (#151) -
The fresh-install check verifies a
--no-default-featuresconsumer as
well as adecodeone.install.ymlis the only job that builds
termlens from outside this workspace, and it did so in one shape — the
one the fewest real consumers use: of the three in-house ones, two declare
default-features = false. Its matrix now runs both shapes on Ubuntu and
macOS, the registry check demands thedecodefeature only on the leg
that asks for it, and the no-defaults leg fails if the consumer's tree
still resolvesinsta. (#238) -
The
inspectexample answers--help, and takes its deadline and
silence window from flags.inspect --helpused to look for a program
called--help, and both timings were hardcoded, so an application slower
than five seconds to paint its first screen could not be inspected at all.
--timeout SECONDS(default 5) and--idle MILLIS(default 300) now sit
beside--size;--help/-hprint one usage text to stdout and exit 0,
a missing program prints the same text to stderr and exits 1, and
--versionnames the termlens version the example was built from. An
unknown option is refused rather than spawned. (#229, #236)
Changed
-
The smallest terminal is 2x2, not 1x1. One column panics the emulator on
a double-width character, and one row panics it on a line that wraps — on
the reader thread, in both profiles, where the panic propagates nowhere: the
grid froze, every later wait ran to its deadline against a plausible-looking
screen, andcargo testprintedtest result: okover a suite that had
stopped testing anything.80x1is an ordinary shape, not an exotic one.
spawnandresizenow refuse a dimension below 2 withError::Size, the
way they already refused 0 — #49's "no path can reach the emulator with a
zero" was satisfied exactly one value too low.2x8and2x2render both
trigger shapes correctly, so the floor is the smallest guard that closes
them. (#211) -
A child starts in the test process's working directory, not
$HOME.
Withoutcurrent_dirthe PTY layer fell back to the home directory, so a
relativespawn()path and a directory-sensitive program behaved unlike
every other Rust process API — whilecurrent_dir's rustdoc promised the
test runner's directory all along. The default is now what
std::process::Commanddoes;current_dirstill overrides it. A test that
relied on the old fallback should say.current_dir(std::env::home_dir())
explicitly. (#215)
Fixed
-
G2/G3 designation and SS2/SS3 single shifts are modelled, so a
one-character line-drawing shift draws┌rather thanl.ESC * 0
/ESC + 0designate the DEC Special Graphics set into G2/G3, and
ESC N(SS2) /ESC O(SS3) invoke that set for exactly one character
before the locking shift resumes. The designation was already consumed;
the shift did nothing, so a mixed line of text and box-drawing showed
the letter. A pending single shift is consumed by the next character —
including a multi-byte UTF-8 one — and does not surviveRIS. Locking
shifts remain G0/G1 only (SO/SI);LS2/LS3andDECSC/DECRCof
charset state are still unmodelled. (#235) -
DECSC/DECRCsave and restore the character-set state. Save,
jump, draw the frame, restore is how a full-screen application draws a
border, and the restore lost the designation, so the border after it
rendered aslqk— the failure #204 fixed, arriving through a different
door.ESC 7now saves G0–G3 and the locking shift alongside the cursor
the backend already saved,ESC 8restores them, a restore with nothing
saved returns to ASCII as xterm does, andRISclears the slot so a
restore cannot resurrect a designation from before the reset. (#232) -
DECSTR(soft reset,CSI ! p) is modelled. The polite reset a
well-behaved TUI sends on startup and teardown parsed cleanly and did
nothing, so text printed after it kept rendering in the character set the
application had told the terminal to forget, whileRISgot this right.
It now returns the character sets and theDECSCslot to power-on, the
cursor shape to the terminal's default, and turns off cursor-key mode,
bracketed paste, every mouse tracking mode and encoding, and focus
reporting; the cursor becomes visible and the alternate screen is left
alone, as specified. Attributes, margins, origin and insert modes and the
keypad are not replayed — nothing onScreenobserves them — and the
README says so. (#233) -
The UK character set is translated:
ESC ( Athen#draws£.
The designation was parsed and then rendered as ASCII, so an application
printing a price in the UK set showed#42on the grid, a test asserting
£42failed against a correct application, and a snapshot that blessed
#42kept passing. The set differs from ASCII in that one position, so
that is the one byte translated; the alternate-ROM sets and the other
national sets still read as ASCII, and the docs now say which sets are
translated. (#234) -
findno longer matches the blank padding past the end of a row. Its
single-row path searched the row padded out to the terminal width while
containssearched the trimmed text, sofind("Total: ")wasSomeon
a screen whose row readTotal:— against the invariant both rustdocs
state, that a needle is found precisely whencontainsis true. Both now
trim trailing whitespace per row first; the trim treats a drawn trailing
U+00A0/U+3000 as padding too, and both rustdocs say so. (#212) -
env_clear()no longer leaks the machine's login shell. The PTY layer
fillsSHELLfrom the host when the variable is absent, so a child's
supposedly hermetic environment differed between two machines with
different shells.SHELL=/bin/shis now pinned underenv_clearthe way
TERMis, an explicit.env("SHELL", …)still wins, and a test asserts
the whole environment rather than probing one name. (#221) -
A bare program name under
env_clear()is refused with the remedies.
Clearing the environment removesPATH, sospawn("sh")could not
resolve and failed with the PTY layer's "Unable to resolve the PATH",
which named neither the cause nor a way out.spawnnow refuses it up
front with anError::Spawnthat saysenv_clearremovedPATHand
offers both fixes: an absolute path, or.env("PATH", …). (#222) -
A byte that is not UTF-8 shows as U+FFFD instead of vanishing. The
backend drops both a byte it cannot decode and the U+FFFD its own parser
substitutes for one, so a Latin-1éin a file name left no trace on the
grid and every column after it shifted left — a test asserting on the
column of what followed passed against the wrong screen. Bytes are now
decoded once on the reader thread: an invalid sequence becomes the
replacement character a terminal shows (carried through the backend as a
noncharacter it will draw, and restored in the snapshot), and a character
split across two reads is carried rather than replaced.wait_idletreats
a stream that stopped mid-character as not yet idle. (#217) -
A highlight over CJK or emoji is one span again. A wide character's
continuation column carried no style, sowith_styles()rendered a bar
overab汉cdas two spans with a hole andcell(row, col).style()on
the second column reportedDefault— DESIGN §3 rule 5 andfind_by's
rustdoc both promised the opposite. The snapshot now gives the
continuation the leading cell's style, as a terminal paints it. (#218)