# Scrin 0.2.1 — MP4 examples and recording workflow
Date: 2026-09-29. Repository: KnottTUI. Crate: `scrin`.
## What changed
This release adds a repeatable video workflow and actual MP4 examples of Scrin
widgets composed with Aisling effects and loaders. Library APIs and dependency
requirements are unchanged from 0.2.0. Applications can adopt `scrin = "0.2.1"`;
apps still on 0.1 should follow the 0.2 migration notes below.
The user's ongoing requirement is recorded in `AGENTS.md` and the README release
instructions: make good MP4 examples for releases, including Aisling alongside
the features being demonstrated. Regenerate videos, inspect representative
frames, and validate them before publishing.
## Videos and gallery
`docs/previews/index.html` is a local gallery with manual playback controls,
posters, and direct MP4 downloads. The collection includes:
- `workspace.mp4`: 12 seconds of a native file list, Rust code, scripted input,
progress gauge, Aurora/Hologram/Prism text, and an Orbit loader. The code sample
changes effect kind with the preview.
- `effects.mp4`: 12 seconds showing the nine Aisling 0.4 effects together in Scrin
panes. Each has deterministic seeded playback and a final hold.
- `loaders.mp4`: 12 seconds showing Tqdm, Orbit, Matrix, Fireworks, GridPulse, and
Bar beside a task queue, sparkline, and Hologram text. Progress is demo data.
- `showreel.mp4`: the three scenes in sequence, totaling 36 seconds.
The clips are silent, 1920x1080, 30 fps, H.264/yuv420p, with MP4 faststart for
progressive playback. Source frames come from real Scrin buffers; the videos
are deterministic exports, not terminal performance measurements. `manifest.json`
records the producing crate version, dimensions, frame counts, and durations.
## Reproduce and validate
```bash
scripts/video_previews
scripts/check_video_previews
# Optional focused/short export.
SCRIN_VIDEO_OUTPUT=/tmp/scrin-preview scripts/video_previews --scene workspace --frames 90
scripts/check_video_previews /tmp/scrin-preview
```
The optional recording tools require Python 3 with Pycairo and PyGObject/Pango,
FFmpeg with libx264, FFprobe for validation, and Rust. Preferred fonts are Adwaita
Mono and Adwaita Sans, with Pango fallback. No desktop, capture service, network
service, or new Rust dependencies are needed. Existing Cargo environment settings,
including `CARGO_TARGET_DIR`, are honored.
`--scene all|workspace|effects|loaders` selects scenes. `--frames N` accepts 1–3600
frames per clip, default 360. The wrapper checks optional dependencies and uses a
pipe with error propagation; no raw frame files are retained. Failed recording or
encoding leaves the previous videos intact. A filtered run's manifest/gallery
lists only that run's clips; release recordings should use the default full set.
The cell export includes RGB foreground/background, bold/italic/underline flags,
and display widths/continuation cells. The renderer paints backgrounds before
wide glyphs so a continuation background cannot erase half a character.
## Changed files
- `examples/export_video_frames.rs`: native widget/effect scenes, deterministic
animation/progress, configurable frame counts, and JSON-lines buffer export.
- `scripts/video_previews`: recording entry point and optional tool checks.
- `scripts/render_video_previews.py`: Pango/Cairo cell rasterization, staged FFmpeg
encoding, posters, concatenated showreel, manifest, and accessible local gallery.
- `scripts/check_video_previews`: FFprobe codec/dimension/frame/duration checks and
FFmpeg decoded motion samples.
- `docs/previews/**`: generated MP4s, posters, gallery, manifest, and recording docs.
- `AGENTS.md`: durable project/release instructions, including the MP4 requirement.
- `Cargo.toml`, `Cargo.lock`, `src/lib.rs`, `README.md`, `for_code_tool.MD`,
`changes.MD`, `newest_release.MD`, `new_changes.md`: version 0.2.1, documentation,
release notes, and editor handoff. Prior handoffs remain below.
Generated media and `AGENTS.md` remain outside the Cargo package. The exporter and
recording scripts are included so crate users can reproduce the examples. No
incoming KnottCode requests or sibling repository sources are changed by this work.
Root-document include patterns in `Cargo.toml` are anchored with `/` so the
gallery's nested README does not accidentally enter the Cargo package.
## Verification
- All four complete MP4s pass `scripts/check_video_previews`: H.264/yuv420p,
1920x1080, 30 fps, 360 frames per scene and 1080 frames in the showreel, expected
durations, successful decoding, and varying motion samples.
- Visually reviewed posters and nine early/middle/final keyframes extracted from
the encoded videos. Pane text, code, animation, progress, and final text remain
legible; the final export uses a consistent dark background and subdued input.
- Short 90-frame exports pass the same video validator. Invalid scene names and
out-of-range frame counts are rejected before emitting frame data.
- A deliberately truncated frame stream fails safely, preserves a previous
recording, and removes its temporary output. Gallery media/poster links resolve.
- `cargo test --offline --locked --quiet`: 314 unit/integration tests and 3 doc
tests pass. `cargo check --offline --locked --examples`: all 12 examples compile.
- Strict rustdoc (`RUSTDOCFLAGS=-Dwarnings`), Rust formatting, diff whitespace,
Bash syntax, and Python compilation checks pass.
- `cargo publish --dry-run --locked --allow-dirty` passes. The 90-file archive
includes the exporter/scripts and excludes `docs/**`, generated media, build
artifacts, `AGENTS.md`, `for_code_tool.MD`, and `FromKnottCode.MD`.
---
# Scrin 0.2.0 — Aisling 0.4 editor handoff
Date: 2026-09-29. Repository: KnottTUI. Crate: `scrin`.
## Adopt this release
```toml
[dependencies]
scrin = "0.2.0"
```
Scrin now uses the published `aisling = "0.4.0"` dependency from KnottEffects.
The 0.2 version boundary is intentional: Scrin publicly re-exports Aisling's
`EffectKind`, whose nine new variants require exhaustive matches to be updated.
Existing `scrin = "0.1.85"` applications will not silently receive that change.
If an app also uses Aisling directly and passes its types to Scrin, change its
Aisling dependency to `aisling = "0.4"` as well.
## Added effects and configuration
All nine additions are available through `scrin::effects::EffectKind`:
| `Aurora` | `aurora` | Curtains of light reveal the text. |
| `Constellation` | `constellation` | Connected stars assemble the glyphs. |
| `Hologram` | `hologram` | A flickering projection stabilizes. |
| `Ripple` | `ripple` | Concentric waves refract the text. |
| `Shatter` | `shatter` | Fragments assemble into the text. |
| `Weave` | `weave` | Interlacing threads reveal the text. |
| `Circuit` | `circuit` | Pulses power circuit traces into glyphs. |
| `Prism` | `prism` | Refracted text planes converge. |
| `Origami` | `origami` | Hinged strips unfold into the text. |
`EffectPlayer::all_kinds()` now returns 46 effects. The existing name parser,
`EffectPlayer`, `RetainedEffectWidget`, and `CachedEffectPane` accept every new
kind without a separate rendering path. Gallery demos that enumerate the catalog
pick them up automatically. `examples/demo_native.rs` now uses Aurora; run
`scripts/demo_native` to preview it with native partial redraws and input.
`ExistingColorHandling` is newly re-exported from `scrin::effects` so ANSI input
color preservation can be configured without a separate direct dependency.
The existing color, gradient, selector, and configuration re-exports remain.
```rust
use scrin::effects::{
EffectConfig, EffectKind, EffectPlayer, ExistingColorHandling, RetainedEffectWidget,
};
let config = EffectConfig {
existing_color_handling: ExistingColorHandling::Always,
hold_final_frames: 8,
..EffectConfig::default()
.with_canvas_size(64, 8)
.with_duration(32)
.with_seed(7)
};
let effect = RetainedEffectWidget::from_player(EffectPlayer::with_config(
EffectKind::Aurora, "KnottCode", config,
));
```
## Behavior and compatibility
- Aisling 0.4 fixes `EffectConfig::no_color` so configured canvas backgrounds
and temporary glyphs cannot restore ANSI colors. `get_ansi_string` inherits
this fix. Scrin buffer mapping still uses the player's accent for uncolored
foreground cells; this release does not change that existing behavior.
- Existing effect variants retain their upstream IDs and seed behavior. Player
builders, cached rendering, frame selection, partial terminal presentation,
interaction APIs, and the 0.1.85 fixes documented below remain available.
- No new loaders were added upstream in this release; loader APIs remain.
- No compiled/indexed Aisling renderer was added upstream. `EffectPlayer` still
generates and stores its frame sequence once; it does not claim streaming or
lower cold-start memory. The existing KnottCode upstream request remains
relevant, and no duplicate request was added to `../KnottEffects/FromKnottTUI.MD`.
- No KnottCode source or manifest is modified by this release. The app must opt
into `scrin = "0.2.0"` to receive the new effects.
## Changed files and verification
- `Cargo.toml`, `Cargo.lock`: Scrin 0.2.0 and Aisling 0.4.0; other dependency
requirements remain unchanged.
- `src/effects/mod.rs`: re-export `ExistingColorHandling`.
- `src/lib.rs`, `README.md`: current install version, new catalog, migration
instructions, and Aurora example.
- `examples/demo_native.rs`: native Aurora demonstration.
- `tests/aisling_04.rs`: discoverability and parsing of every new kind, raw frame
parity with Aisling, single-frame durations, final holds, loop wraparound,
retained cache/clipping parity, and raw monochrome checks across the catalog.
- `tests/core_tests.rs`: inventory expectation raised to at least 46 effects.
- `changes.MD`, `newest_release.MD`, `for_code_tool.MD`, `new_changes.md`: release
notes and editor handoff. `for_code_tool.MD` stays outside the Cargo package.
Validation completed before publication:
- `cargo test --locked`: 314 unit/integration tests and 3 documentation tests pass.
- `cargo fmt --all -- --check` and `git diff --check`: pass.
- `cargo check --offline --locked --examples`: all 11 examples compile.
- `RUSTDOCFLAGS=-Dwarnings cargo doc --offline --locked --no-deps`: passes.
- `scripts/check_terminal_pty`: Aurora playback, paste, partial redraws, resize,
Ctrl+Q, and restoration pass at 80x24 and 32x10, including tiny resizes; failed
initialization restores raw mode too.
- A fresh temporary snapshot of `../KnottCode` compiles with its Scrin dependency
changed to 0.2.0 and patched to this checkout. Its actual source and manifest
remain untouched; no KnottCode source changes were needed for the upgrade.
- `cargo publish --dry-run --locked --allow-dirty`: package verification passes.
The archive includes this handoff and the new integration tests, and excludes
`for_code_tool.MD`, `FromKnottCode.MD`, and build artifacts.
---
# Scrin 0.1.85 — code editor handoff
Date: 2026-09-29. Repository: KnottTUI. Crate: `scrin`.
This release improves retained animation allocation, terminal rendering correctness,
partial interaction updates, and forms while retaining Scrin's native buffer model,
its Aisling integration, existing widgets, and existing public rendering APIs.
No dependency requirements were added, removed, or updated. `Cell` remains a
copyable single-character cell, and `TextInput::cursor` remains a character index.
## 1. KnottCode requests
Both requests in `FromKnottCode.MD` are implemented in this release.
### Reuse retained frame buffers
Files: `src/effects/retained.rs`, `src/core/buffer.rs`.
- `RetainedEffectWidget::with_cached_buffer` and `with_cached_frame_buffer` now
reuse the same Scrin buffer when the frame changes without a dimension change.
- A changed frame resets all cells, colors, style flags, background, and wide-glyph
continuation metadata before drawing. Content outside a smaller Aisling frame
cannot retain old glyphs or styles.
- Same-frame hits still skip rendering. Resize requests rebuild storage as needed
and continue to clip to the requested dimensions.
- `RetainedLoaderWidget::with_cached_buffer` likewise reuses its Scrin buffer when
ticks or progress change at the same size. Aisling still generates its own loader
frame; this does not claim that uncached loader rendering has zero allocations.
- `CachedEffectPane` compares cache keys by reference instead of cloning its text
and palette on every lookup or rendered frame.
- Existing `render_cached_only`, `render_frame_into`, frame-count, cache-key,
cached-row, visible-row, and area-presentation APIs remain available.
### Generate a fully configured effect once
Files: `src/effects/aisling_bridge.rs`, `src/effects/mod.rs`,
`src/effects/retained.rs`.
New constructor:
```rust
EffectPlayer::with_config_and_palette(
kind: EffectKind,
text: &str,
config: EffectConfig,
colors: Vec<scrin::Color>,
angle: f32,
) -> EffectPlayer
```
It applies the complete configuration and palette before generating frames.
The existing `with_size`, `with_duration`, `with_seed`, and
`with_gradient_colors` builders remain compatible and eager.
New re-exports from `scrin::effects`:
- `AislingColor`
- `AislingGradient`
- `GradientDirection`
These are the Aisling types accepted by `EffectConfig::with_gradient`, for callers
that prefer to assemble the entire configuration themselves.
Recommended KnottCode construction:
```rust
use scrin::Color;
use scrin::effects::{EffectConfig, EffectKind, EffectPlayer, RetainedEffectWidget};
let config = EffectConfig::default()
.with_canvas_size(64, 8)
.with_seed(7)
.with_duration(32);
let player = EffectPlayer::with_config_and_palette(
EffectKind::Matrix,
"KnottCode",
config,
vec![Color::CYAN, Color::BLUE],
45.0,
);
let retained = RetainedEffectWidget::from_player(player);
```
`CachedEffectPane` uses this construction path internally. Palette angle mapping
is unchanged: 0 means horizontal, 90 vertical, and other angles diagonal. Effect
reconfiguration now normalizes the current frame index if the new sequence is
shorter. `advance_n(usize::MAX)` no longer overflows.
No Aisling source change is needed for these fixes, so no request was added to
`../KnottEffects/FromKnottTUI.MD`.
## 2. Buffer text, wide glyphs, and allocation
File: `src/core/buffer.rs`.
New APIs:
```rust
buffer.set_str_clipped(x, y, text, max_width, fg, bg) -> usize
buffer.set_str_styled_clipped(x, y, text, max_width, style) -> usize
buffer.reset()
buffer.repair_wide_cells()
```
- Clipped writers return terminal columns actually written, respecting both the
supplied width and buffer bounds. They stop before a glyph that will not fit;
they do not skip that glyph to render later characters.
- Existing plain, bold, and styled text writers now advance by display columns.
For example, `A中B` occupies four columns without overwriting `中`.
- String writers replace control characters with spaces and skip zero-width
formatting characters. Direct single-cell writes normalize non-displayable
characters or a wide character in the final column to a styled space.
- Fast ASCII writes inspect the boundaries of the affected row, not every cell
in the buffer. They can safely overwrite wide glyphs and work when another
row contains wide text. Non-ASCII input still returns `false` from the explicit
ASCII fast-path method without writing.
- Overwriting either half of a wide glyph clears the other half. Overlapping
wide glyphs and fast fills no longer leave stale continuation flags.
- Wide-character fills tile pairs of columns and pad an odd final column with
a styled space. Fills clip their loops to the buffer dimensions.
- `merge_from` skips source continuation cells so a background-colored wide
glyph is not erased by its own second cell.
- Offset copies/merges reject out-of-bounds work without overflowing coordinates.
Resize repairs glyphs clipped at the new right edge and does nothing if the
dimensions have not changed.
- `reset` restores fresh-buffer cells, styles, background, and continuation state
without reallocating. `Buffer::clone_from` reuses destination vector storage.
- `copy_area_from` expands horizontal edges to include complete intersecting
glyphs in both source and destination and uses row-slice copies.
### Direct cell mutation compatibility
`cell_mut` and `cells_mut` remain available with the same return types. Changing
only colors/styles requires no migration. If an app changes symbols through these
APIs, call `repair_wide_cells()` afterward, or use `set` to maintain metadata
immediately. KnottCode's style-only post-render effects can stay as they are.
`Cell::ch` still stores one `char`. This release does not add grapheme-cluster or
emoji-sequence storage, nor a complete new Unicode width database.
## 3. Terminal diff, interaction metadata, and lifecycle
Files: `src/terminal/mod.rs`, `src/interaction/mod.rs`.
- Partial diffs and partial front-buffer commits use the same wide-glyph boundary
expansion. An area covering only a continuation cell now presents the glyph
from its leading column. Adjacent old/new wide glyphs are included as needed.
- Area-limited diff iteration visits relevant row spans rather than checking
every terminal cell against every area. Overlapping areas do not duplicate cells.
- An unchanged or empty diff writes **zero bytes**, including no trailing SGR reset.
- Nonempty diff/full output establishes its initial style explicitly, including
black foregrounds, so it does not inherit unknown terminal colors or modifiers.
- Full writers replace glyphs clipped by an explicitly narrower output width with
a space. Non-displayable characters inserted through raw cell access are not
emitted as terminal control commands.
- Terminal clears and front/back commits reuse buffer allocations through
`reset` and `clone_from`.
- `Areas`, `MarkedDirty`, and cursor-preserving area draws retain mouse/selection/
scroll metadata belonging to unaffected panes. New metadata is committed only
after the presentation succeeds.
- New `InteractionLayer::replace_areas(&[Rect], replacement)` supports the same
partial update semantics in custom render loops. Re-register all affected
regions/spans: records intersecting a redraw are replaced as a whole, records
outside the redraw are preserved, and replacement records outside it are ignored.
A moved stable ID replaces its previous record.
- `Terminal::set_cursor` records the logical cursor as well as moving it, allowing
subsequent cursor-preserving area presentations to restore that position.
- Initialization queries dimensions before changing modes and holds a cleanup
guard while enabling output modes. An initialization output failure restores
raw mode and attempts to undo enabled terminal modes.
- `restore` attempts paste, mouse, cursor, alternate-screen, and raw-mode cleanup
even after an earlier operation fails. Failed output resets remain retryable;
a successful restoration is still idempotent.
### Behavior downstream code should account for
A selected area may now update neighboring columns belonging to the same wide
glyph. This keeps the real terminal and committed buffer coherent. Partial draw
closures should register interaction metadata for the widgets they redraw.
ANSI snapshot expectations must allow explicit initial style resets and an empty
output for a clean diff. Existing method signatures and presentation strategies
remain intact.
## 4. Input fields, forms, geometry, and sanitization
Files: `src/widgets/form.rs`, `src/input/mod.rs`, `src/core/rect.rs`,
`src/scroll_state.rs`, `src/sanitize.rs`.
- Long focused text fields scroll horizontally to keep their cursor visible.
Wide labels, wide values, masked values, and placeholders use display columns.
- New `TextInput::cursor_position(area) -> Option<(u16, u16)>` exposes the rendered
cursor position for `Frame::set_cursor_position`. It returns `None` when the
field is unfocused or there is no visible input space.
- Paste is filtered and inserted in one operation instead of inserting each
character individually. Controls, including newline/tab/escape, are discarded
in the single-line input. Alt/control character shortcuts are not inserted.
- Text editing clamps the public cursor after an application changes the value.
Dropdown and form navigation avoid overflow on externally changed indices.
- Forms render borrowed fields without cloning their values/options, clip titles,
dropdowns, toggles, and status text to their area, and reserve the status row.
Input/dropdown rendering preserves configured style flags.
- Text inputs, dropdowns, toggles, and `EventRouter` ignore key-release events;
key presses and repeats continue to work.
- `Rect::rows`, `cols`, and `positions` iterate without allocating. Empty rectangles
do not intersect; oversized inner margins produce an empty rectangle within the
parent's bounds. The iterator lifetime spelling no longer requires precise
`use<...>` captures introduced after the declared Rust version.
- Scroll-down and visible-range arithmetic saturate for oversized requests.
- `sanitize_str` counts replacement spaces toward its width limit; its existing
newline/tab preservation is retained. `sanitize_and_set` uses the bounded buffer
writer and returns only columns actually written, with controls rendered as spaces.
## 5. Examples, tests, and documentation
- `examples/demo_native.rs` and `scripts/demo_native` demonstrate Scrin's Terminal
API, an Aisling pane, retained partial animation frames, cursor preservation,
bracketed paste, and a scrolling input. Ctrl+Q or Esc exits.
- `scripts/check_terminal_pty` validates the demo using Unix PTYs, exercising paste,
partial redraws, shrinking/growing windows, tiny geometry, exit, and restoration.
On Linux it also checks startup rollback with failing `/dev/full` output.
- `tests/rendering_regressions.rs` covers glyph transitions, clipping, styles,
partial areas, short/erroring writers, forms, metadata, geometry, and scrolling.
A separate screen model verifies actual ANSI output and committed buffers over
400 deterministic randomized wide-glyph transitions, followed by a full diff.
- `tests/retained_allocations.rs` instruments allocations on the test thread and
checks 500 frame updates/cache hits, cached-pane rendering, and core buffer reuse.
- Effect tests compare frame counts and ANSI/color output for every Aisling effect
kind between one-shot construction and the existing builder chain. Reused effect
buffers are poisoned with styles/wide glyphs before comparing to fresh rendering;
loader output and storage reuse are checked over 200 updates.
- README, crate installation docs, `changes.MD`, `newest_release.MD`, and
`for_code_tool.MD` describe the release. This handoff is included in the crate
package. `for_code_tool.MD` and `FromKnottCode.MD` remain repository-only files.
## 6. Validation and release
Release version: **0.1.85**. Dependency version requirements remain unchanged.
Completed validation:
| `cargo fmt --all --check` and `git diff --check` | Passed |
| `cargo test --all-targets --offline` | 311 tests passed; all 11 examples compiled |
| `cargo test --doc --offline` | 3 documentation tests passed |
| `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --offline` | Passed |
| `cargo clippy --all-targets --offline` | Completed; existing style warnings remain in older code/examples |
| `scripts/check_terminal_pty` | 80x24 and 32x10 starts, paste, partial frames, 18x6 and 3x2 resize, Ctrl+Q, complete restoration passed |
| Linux initialization output failure (`/dev/full`) | Raw-mode rollback passed |
| KnottCode `cargo check --offline --bins` against this checkout | Passed using a temporary source snapshot and Cargo patch; original KnottCode files untouched |
| `cargo publish --dry-run --allow-dirty --locked` | Package verification passed; upload intentionally skipped |
The allocation tests observed zero allocations across 500 warmed same-size effect
frame changes/cache hits and 500 cached-pane frame changes with a palette. These
are allocation assertions, not frame-rate or whole-application benchmark claims.
All effect kinds passed deterministic frame/color parity against the existing
builder sequence. The previous 277 tests continue to pass alongside the additions.
Generated build artifacts are excluded from the release package and source commit.
To adopt the published release in KnottCode:
```toml
scrin = "0.1.85"
```
Then refresh the Scrin lockfile entry in the KnottCode repository. This work does
not modify KnottCode's source files, dependency declaration, or lockfile.