Skip to content

feat: v0.21.1 — interaction-signal completeness + ergonomics + WASM/perf + crate hygiene#276

Merged
subinium merged 29 commits into
mainfrom
release/v0.21.1
May 30, 2026
Merged

feat: v0.21.1 — interaction-signal completeness + ergonomics + WASM/perf + crate hygiene#276
subinium merged 29 commits into
mainfrom
release/v0.21.1

Conversation

@subinium

Copy link
Copy Markdown
Owner

v0.21.1 — post-0.21.0 competitive-gap audit closeout

Closes the additive (non-breaking) gaps surfaced by the v0.22 backlog audit, folded into a single 0.21.1 release. Every item below is purely additive — no public signature changes, no deprecation removals.

Added

  • Response interaction signalssubmitted (Enter in a focused single-line text_input), double_clicked (same-cell within ~400ms; the 2nd still reports clicked), scroll_delta: i32 (hover-gated wheel). Chainable on_click/on_changed/on_focus/on_submit/on_double_click.
  • Focus edges for hand-assembled widgetstext_input/slider/number_input now populate gained_focus/lost_focus via a shared focus_transitions helper (closes the feat(context): expand Response with right_clicked / gained_focus / lost_focus #208 follow-up).
  • Focus traversalfocus_next/focus_prev (modal-trap aware) + group-scoped focus_next_in_group/focus_prev_in_group.
  • ColorFrom<(u8,u8,u8)>/From<[u8;3]>/From<u32>/FromStr (+ColorParseError); from_hsl/from_hsv/rotate_hue.
  • Gradientsgradient_stops(&[(f32,Color)]), bg_gradient, bg_gradient_stops.
  • KeyMap dispatchBinding::matches, KeyMap::matched, Context::keymap_match (bubbletea key.Matches parity).
  • Spinner presetsSpinnerPreset + moon/bounce/circle/points/arc/toggle/arrow/preset/frame_count.
  • List reorderListState::move_item, opt-in list_reorderable/_coloredListResponse{ reordered }.
  • Measurementmeasure_text(text, Option<max_width>) -> (w, rows), measured_rect(name) -> Option<Rect>.
  • WASMDomBackend::resize + wheel/resize/blur/paste event wiring (Phase 1-2 parity).
  • TestBackendfind_text/region/assert_region/assert_styled_contains/snapshot/assert_snapshot_eq.
  • Examplesv0211_tour (new APIs) + v0210_widgets (paginator/number_input/variable virtual_list/scheduler/inspector).

Changed

  • Synchronized-output (BSU/ESU) emission gated on a DECRQM ?2026 probe; headless/silent terminals keep emitting as before. Capabilities::sync_output is now populated.

Perf

  • WASM DOM flush diffs vs a prev-frame buffer (mutates only changed cells).
  • Sprixel re-blit scan: hashed-key set + per-row clean/hash shortcut.
  • Resize coalescing: one on_resize per poll-batch (SIGCONT path unchanged).
  • New criterion benches for the kitty flush + sprixel re-blit paths.

Docs

Housekeeping

  • Published crate no longer ships agent/skill scaffolding, VHS recordings, or dev/CI config (AGENTS.md, .agents/, .claude/, *.tape, deny.toml, _typos.toml) — ~140KB that leaked into the 0.21.0 tarball.
  • Removed the orphaned committed.toml (its CI gate was dropped in 0.21.0).
  • VHS gallery tapes rewritten to run prebuilt example binaries (fixes the cold-cache Sleep-overrun blank-frame failures).

Verification

Full extended gate green locally (real exit codes): fmt, check --all-features, clippy --all-features --all-targets -D warnings, test --all-features, check --examples, typos, check --no-default-features, check -p slt-wasm --target wasm32-unknown-unknown, cargo hack check --each-feature, audit, deny check, MSRV 1.81.

Intentionally NOT included (with rationale)

  • idle frame-skip — a sound version needs a repaint-request signal + scheduler-aware poll; a naive version risks freezing animations / delaying timers.
  • kitty alternate-keys flag — crossterm 0.28 does not surface the alternate-key data, so the flag would be a no-op.
  • OSC 52 clipboard read event — the concurrency hazard is now documented; a non-blocking read-event is an architectural follow-up.
  • cursor screen-position query — needs cross-backend prev-frame plumbing (P2).

🤖 Generated with Claude Code

subinium and others added 29 commits May 30, 2026 17:13
Add find_text, region/assert_region, assert_styled_contains, snapshot,
and assert_snapshot_eq to TestBackend for ergonomic content+style and
sub-rectangle assertions, plus unified-diff snapshot comparison.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the text gradient API additively:
- gradient_stops(&[(f32, Color)]): multi-stop horizontal foreground
  gradient, stops clamped to 0..1 and sorted internally; empty slice
  is a no-op, single stop renders solid.
- bg_gradient(from, to) / bg_gradient_stops(&[(f32, Color)]): background
  analogues applying the gradient to cell background instead of fg.

All variants reuse the existing per-column char mapping (denom =
len-1, t = i/denom) and Color::blend clamping so width handling stays
consistent with gradient(). Adds 8 TestBackend unit tests covering
first/middle/last interpolation, unsorted input, single stop, and
empty edge cases for both fg and bg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add bubbletea key.Matches parity to the keymap catalog:
- Binding::matches(&KeyEvent) -> bool, with press-only and modifier
  contains() semantics matching Context::key_mod.
- KeyMap::matched(&KeyEvent) -> Option<&Binding>, first-registration-wins.
- Context::keymap_match(&KeyMap) -> Option<&Binding> peeking the frame's
  unconsumed key presses, respecting the modal/overlay guard.

Covered by unit tests (incl. release-event and overlap edge cases) and
TestBackend-driven Context tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ListState::move_item(from, to) — bounds-checked, no-op safe, keeps
the search cache and per-item heights aligned, rebuilds the filtered
view, and follows selection onto the moved item.

Add Context::list_reorderable / list_reorderable_colored — opt-in
reorderable list returning a ListResponse (Deref<Response>) exposing
.reordered: Option<(usize, usize)>. Shift/Alt + Up/Down moves the
selected item; plain navigation and the existing list() API are
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add moon, bounce, circle, points, arc, toggle, and arrow presets to
SpinnerState plus a SpinnerPreset enum and SpinnerState::preset() builder
for cli-spinners / ratatui-throbber parity. dots() and line() are
unchanged. The chars field stays private; only constructors and a new
frame_count() helper are exposed. Adds focused unit tests covering each
preset's sequence, cycling, preset/constructor equivalence, and a
large-tick wrap-around edge case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task A — intrinsic-size measurement (v0.21.1):
- Context::measure_text(&str, Option<u16>) -> (u16, u16): reuses the layout
  engine's wrap_lines kernel (no duplicated width logic) to report the
  (width, rows) text would occupy unwrapped (None / Some(0)) or wrapped to a
  column budget (Some(w)). Display-width aware (wide CJK = 2, combining = 0),
  saturating to u16.
- Context::measured_rect(&str) -> Option<Rect>: read-only lookup into the
  previous frame's name->rect bookkeeping (prev_group_rects) for a named
  group(...) container.

Task B — resize debounce/coalesce (v0.21.1):
- poll_events no longer fires on_resize per Event::Resize. A burst within one
  poll batch now collapses to a SINGLE on_resize at end-of-batch, picking up
  the final terminal size (handle_resize re-reads terminal::size()). Avoids
  N x (Clear(All) + double realloc + size() syscall) while dragging a window
  edge. The SIGCONT/resume redraw path in run_with is untouched.
- Added pure helper resize_invocations_for_batch for a unit-testable rule;
  a debug_assert keeps the has_resize flag and the helper in agreement.

Tests: 6 measure_text/measured_rect cases (multi-line, wrapping, Some(0)
edge, wide-CJK, first-frame None, post-render group rect) + 3 resize-coalesce
cases (3-event batch -> 1 invocation, no-resize -> 0, has_resize agreement).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add additive, non-breaking ergonomic APIs to Color:
- From<(u8,u8,u8)>, From<[u8;3]>, From<u32> (0xRRGGBB) -> Color::Rgb
- FromStr parsing #RRGGBB / RRGGBB / #RGB / RGB and named colors,
  with a new public ColorParseError (Display + std::error::Error)
- from_hsl, from_hsv (h in degrees, s/l/v in 0..1) and rotate_hue,
  using pure f32 HSL/HSV round-trip math (no new deps)

Named/indexed colors resolve through the existing palette before hue
rotation. Adds focused unit tests covering hex round-trips, HSL/HSV
primaries, hue rotation, clamping/wrapping, and FromStr error cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the two biggest WASM backend parity gaps against the native crossterm
path.

DOM flush diff: DomBackend now keeps a `prev: Buffer` snapshot and mutates only
the spans whose cell actually changed (Cell derives PartialEq over symbol +
style + hyperlink), mirroring the native ANSI diff in src/buffer.rs. Steady-state
frames now issue almost no DOM writes instead of touching every cell. A grid
rebuild (first frame or resize) forces a full repaint.

Event parity (Phase 1-2): wire the remaining input sources into slt Events the
core run loop consumes, matching crossterm's mapping:
- mouse wheel -> ScrollUp/Down/Left/Right at the cell under the cursor
  (dominant-axis resolution, zero-delta ignored)
- window resize -> Resize(cols, rows); the backend recomputes the grid from the
  container pixel size, resizes its buffer, and rebuilds the DOM grid next flush
- focus/blur -> FocusGained / FocusLost
- clipboard paste -> Paste(text)

WheelEvent/FocusEvent/ClipboardEvent/DataTransfer web-sys wrappers are not in
this crate's feature set, so the new listeners read the generic web_sys::Event
reflectively via js-sys (no Cargo changes). Position-dependent listeners now read
the live grid size from the backend so they stay correct after a resize. Image
overlay (Phase 3) is intentionally left as a follow-up.

Adds host-runnable logic tests for the wheel-delta mapping and the color/style
CSS translation (incl. zero-delta, tie, and default-style edge cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…SC52 read hazard

- sync-output capability gate: probe DECRQM ?2026 in probe_capabilities,
  set Capabilities::sync_output on positive support, and gate BSU/ESU
  emission on a tri-state resolution. Silent/headless probes stay Unknown
  and keep emitting exactly as before (behavior-preserving default).
- sprixel reblit scan: replace the O(n*m) previous.sprixels.iter().any(..)
  position lookup with a hashed-key HashSet and add a per-row clean+hash
  shortcut so untouched footprint rows skip the per-cell annihilation scan.
- read_clipboard: document the stdin typeahead-swallow concurrency hazard
  and recommended usage (signature unchanged).
- bench: add #[doc(hidden)] __bench_flush_kitty, __BenchSprixelFixture,
  __bench_new_sprixel_fixture, and __bench_flush_sprixels wrappers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-click, scroll_delta, focus traversal

Close the issue #208 follow-up: text_input/slider/number_input now report
gained_focus/lost_focus via a shared focus_transitions() helper. Add
Response.submitted (Enter in text_input), .double_clicked (same-cell within
~400ms), .scroll_delta (hover-gated wheel), and on_click/on_changed/on_focus/
on_submit/on_double_click callback chaining. Add public focus_next/focus_prev
and focus_next_in_group/focus_prev_in_group for programmatic + group-scoped
focus traversal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… fixes

Wire crate-root re-exports for the 9 merged units (ColorParseError,
ListResponse, SpinnerPreset, sprixel bench fns), fix the focus_next doc
example to a real API, and add explicit let-_ discards for registration-only
widget calls in tests. Full core gate green: fmt/check --all-features/
test --all-features (all pass)/clippy --all-features --all-targets -D warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the missing rustdoc the audit flagged:
- # Example blocks on definition_list and divider_text.
- # Panics sections (with the actual emitted message) on the hooks that
  can panic on a rules-of-hooks / type-mismatch violation: use_memo,
  use_memo_ref, use_effect, animate_value.
- # Family cross-references on gauge / line_gauge linking the gauge family.
- # See also on animate_bool / animate_value.

Doc-only; no code or signatures changed. All added doctests compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire a timed criterion arm for the existing __BenchKittyFixture +
__bench_new_kitty_fixture hooks (issue #206 alloc suite), which were
re-exported at the crate root but had no bench arm. The new
flush_kitty group sweeps 1/8/32 placements, flushing each fixture into
a reused hermetic Vec<u8> sink in the same shape as the flush group.

The sprixel/__bench_flush_kitty hooks named in the unit brief
(__bench_flush_kitty, __bench_new_sprixel_fixture, __BenchSprixelFixture,
__bench_flush_sprixels) are not present in this fork yet, so those arms
are deferred to the integration step. See integration notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump to 0.21.1 (root + slt-wasm + dep). Expand the published-crate exclude so
agent/skill scaffolding, VHS recordings, and dev/CI config (AGENTS.md, .agents/,
.claude/, *.tape, deny.toml, _typos.toml, .gitignore) no longer ship in the
tarball (~140KB leaked into 0.21.0). Remove the orphaned committed.toml (its CI
gate was dropped in 0.21.0) and its stale CI-reference row. Add the 0.21.1
changelog section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add examples/v0210_widgets.rs covering the v0.21.0 widgets that previously
shipped without a runnable example: the standalone paginator (PaginatorState
/ PaginatorStyle), the numeric stepper (NumberInputState / number_input), the
variable-height virtual_list (virtual_list_variable + ListState item heights),
the async-free frame-clock scheduler (schedule / every / debounce, #248), and
the devtools inspector panel (set_inspector / Ctrl+F12, #268).

Self-contained Standard-archetype screen (no overlay / no scrollback), builds
on the default feature set since the scheduler is wall-clock based. In-frame
async (spawn/poll) is noted as out of scope to keep the binary async-free.

Requires an explicit [[example]] entry in Cargo.toml (autoexamples = false);
the integrator must add it — see integration_notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x, example registration

- examples/v0211_tour.rs: showcase of the new 0.21.1 APIs (HSL/hex/rotate_hue,
  gradient_stops/bg_gradient, spinner presets, list_reorderable, measure_text,
  KeyMap dispatch, focus_next, Response.submitted/double_clicked/scroll_delta).
- Register v0210_widgets + v0211_tour as [[example]] (autoexamples=false).
- benches: add bench_flush_sprixel_reblit using the merged sprixel bench hooks;
  gate the kitty/sprixel bench re-exports behind crossterm (fixes no-default).
- VHS gallery: rewrite the 6 build-inside-pty tapes to call the prebuilt
  ./target/release/examples/<x> binary (CI already pre-builds examples), so a
  cold-cache build no longer overruns the fixed Sleep and captures a blank
  frame — the gallery_manifest parity gate stays green.
- typos: rename test-local `ofr` binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vhs-action@v2's bundled ffmpeg installer fails on the current runner
("Failed to install ffmpeg"), so the advisory gallery job never reaches the
(now prebuilt-binary) tapes. Install ffmpeg from apt first so the action finds
it present. CI-only; the gallery job is continue-on-error and the README<->tape
parity hard gate (gallery_manifest) is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@subinium subinium merged commit ac079b9 into main May 30, 2026
15 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant