scrin 0.1.81

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation

scrin

Made by KnottDynamics.

scrin is a Rust terminal UI toolkit for building polished command-line interfaces without Ratatui. It provides Scrin-native buffers, colors, layout, styled text, widgets, panes, overlays, command palettes, status bars, input routing, terminal lifecycle helpers, and animation utilities.

Scrin also ships with Aisling-powered effects and loaders. The aisling crate is baked in as a first-class dependency, and Scrin adapts Aisling frames into Scrin buffers so animated text effects and progress loaders can live beside normal widgets.

Install

[dependencies]
scrin = "0.1.81"

Why Scrin

  • Scrin-native terminal rendering with no Ratatui dependency.
  • Composable widgets for blocks, paragraphs, lists, tables, tabs, charts, gauges, markdown output, forms, popups, toggles, and more.
  • App structure primitives for panes, overlays, modals, toasts, status bars, command palettes, event routing, scrolling, and text expansion.
  • Rich terminal text with spans, lines, wrapping, scrolling, alignment, colors, and modifiers.
  • Aisling integration for cinematic effects and loaders rendered directly into Scrin buffers.
  • Demos and scripts that exercise widgets, overlays, Aisling effects, loaders, and a full Scrin shell showcase.

Draw A Widget

use scrin::core::buffer::Buffer;
use scrin::core::color::Color;
use scrin::core::rect::Rect;
use scrin::style::Style;
use scrin::widgets::block::Block;
use scrin::widgets::paragraph::Paragraph;
use scrin::widgets::Widget;

let mut buffer = Buffer::new(48, 8);
buffer.fill(Rect::new(0, 0, 48, 8), ' ', Color::WHITE, None);

let block = Block::bordered()
    .title("scrin")
    .border_style(Style::new().fg(Color::CYAN));
block.render(&mut buffer, Rect::new(0, 0, 48, 8));

let text = Paragraph::new("Scrin renders terminal UI from its own buffer model.");
text.render(&mut buffer, Rect::new(2, 2, 44, 4));

Use The Terminal API

Terminal::draw builds a frame, renders into Scrin's back buffer, and presents a diff to the terminal. The diff presenter repositions for separated dirty runs on the same row and handles wide-glyph continuation cells.

use scrin::terminal::{Terminal, TerminalOptions};
use scrin::style::Style;
use scrin::widgets::block::Block;

let mut terminal = Terminal::init_with(TerminalOptions::default())?;

terminal.draw(|frame| {
    let area = frame.area();
    let block = Block::bordered()
        .title("app")
        .border_style(Style::new().fg(scrin::Color::CYAN));
    frame.render_widget(block, area);
})?;

terminal.restore()?;
# Ok::<(), std::io::Error>(())

For dense animation or integrations that prefer repainting every cell, Terminal::draw_full and Terminal::present_full are available.

Aisling Effects

Scrin exposes Aisling through scrin::effects. You can select an Aisling EffectKind, tune the effect with Scrin colors and sizing, and render the current Aisling frame into any Scrin Buffer area.

use scrin::core::buffer::Buffer;
use scrin::core::color::Color;
use scrin::core::rect::Rect;
use scrin::effects::{EffectKind, EffectPlayer};

let mut buffer = Buffer::new(64, 8);

let mut effect = EffectPlayer::new(EffectKind::Matrix, "Scrin + Aisling")
    .with_size(64, 8)
    .with_duration(32)
    .with_seed(7)
    .with_accent(Color::rgb(88, 166, 255));

effect.render_to_buffer(&mut buffer, Rect::new(0, 0, 64, 8));
effect.advance();

Useful effect APIs:

  • EffectPlayer::new(kind, text) creates an Aisling-backed text effect.
  • EffectPlayer::with_config(kind, text, config) accepts an Aisling EffectConfig re-exported from scrin::effects.
  • with_size, with_duration, with_seed, with_accent, and with_gradient_colors tune how the effect is generated and mapped.
  • render_to_buffer and render_frame_to_buffer copy Aisling frame cells into a Scrin buffer region.
  • get_ansi_string returns Aisling's raw ANSI frame when you want standalone effect output.
  • EffectPlayer::all_kinds() returns every EffectKind available in the baked Aisling version.

Aisling Loaders

Loaders are also Aisling-backed. Scrin maps loader frames into buffers and keeps progress helpers on LoaderPlayer.

use scrin::core::buffer::Buffer;
use scrin::core::color::Color;
use scrin::core::rect::Rect;
use scrin::effects::{LoaderKind, LoaderPlayer};

let mut buffer = Buffer::new(52, 5);

let loader = LoaderPlayer::new(LoaderKind::Bar)
    .with_size(52, 5)
    .with_label("indexing".to_string())
    .with_unit("files")
    .with_fraction(true)
    .with_accent(Color::rgb(255, 128, 64));

let progress = LoaderPlayer::progress_from_counts(42, 100);
loader.render(8, progress, &mut buffer, Rect::new(0, 0, 52, 5));

Useful loader APIs:

  • LoaderPlayer::new(kind) creates an Aisling-backed loader.
  • LoaderPlayer::with_config(kind, config) accepts an Aisling LoaderConfig re-exported from scrin::effects.
  • with_size, with_label, with_unit, with_fraction, with_accent, and with_gradient_colors tune the loader presentation.
  • render(tick, progress, buffer, area) draws the current loader frame into a Scrin buffer.
  • get_ansi_string(tick, progress) returns the raw ANSI loader frame.
  • progress_from_counts and progress_from_fraction create Aisling progress values without importing Aisling directly.
  • LoaderPlayer::all_kinds() returns every LoaderKind available in the baked Aisling version.

Modules

  • scrin::core: Buffer, Cell, Color, Gradient, and Rect.
  • scrin::layout: horizontal and vertical layout constraints.
  • scrin::widgets: blocks, paragraphs, rich text, lists, tables, forms, charts, gauges, markdown output, popups, toggles, scrollbars, and clearing helpers.
  • scrin::terminal: raw-mode setup, frame drawing, diff/full presenters, cursor control, mouse capture, bracketed paste, and restore-once lifecycle.
  • scrin::effects: Aisling-backed EffectPlayer, LoaderPlayer, effect kinds, loader kinds, progress, and config types.
  • scrin::panes, scrin::overlays, scrin::command_palette, scrin::status_bar, and scrin::input: higher-level application structure.
  • scrin::sanitize: display-width-safe terminal string helpers.

Scrin-Only Porting Helpers

Scrin includes small ergonomic APIs for downstream crates that previously used a Ratatui-style structure:

  • Block::bordered().title(...).border_style(...) for concise block builders.
  • Block::inner(area) defaults to one border cell per side; extra padding is opt-in with with_inner_margin(...).
  • Frame::render_widget(widget, area) for frame-centric rendering.
  • Buffer::cell_mut(x, y) plus Cell::set_symbol, set_fg, set_bg, and set_style for post-render effects.
  • Rect::is_empty() for direct zero-area checks.

Performance And Ergonomics APIs

Scrin includes APIs for larger, editor-like terminal applications:

  • ScrollableText accepts Scrin rich Text/Line/Span data, owns a ScrollState, caches wrapped rows by content/style/wrap mode/width, and renders from cached row slices without cloning row vectors.
  • MarkdownOutput::retained() and RetainedMarkdownOutput cache parsed and wrapped Markdown rows by content, width, and wrap mode for preview/export panes, then render from cached row slices.
  • RetainedEffectWidget and RetainedLoaderWidget cache Aisling effect/loader frames as Scrin buffers for reuse across render calls.
  • RetainedEffectWidget::render_frame_into(...) renders a specific cached effect frame into an existing buffer area without mutating the widget's current frame.
  • RetainedEffectWidget::render_cached_only(...), cache_hit(...), current_cache_key(), and frame_count() help latency-sensitive animation paths avoid surprise rebuild work.
  • Terminal::draw_areas_timed(...) clears/renders/presents only selected areas and commits only those areas, preventing unpresented writes from leaking into a later full diff.
  • Terminal::draw_area_preserve_cursor_timed(...) renders one area, presents only that area, and restores the logical cursor in one call.
  • Terminal::draw_with_present_strategy(...) supports PresentStrategy::Diff, Full, Areas, and DirtyBounds.
  • Terminal::present_area(...) and Terminal::present_areas(...) present only changed cells inside selected rectangles and copy those areas into the front buffer.
  • Terminal::present_area_preserve_cursor_timed(...) presents an already-rendered area and restores the logical cursor in one call.
  • Terminal::size_cached() explicitly returns cached terminal size without I/O or resize checks.
  • Terminal::dirty_bounds() returns the bounding rectangle of changed cells between the front and back buffers.
  • Timed variants such as draw_timed, draw_full_timed, present_timed, draw_areas_timed, present_full_timed, and present_areas_timed return FrameTiming.
  • Terminal::set_frame_timing_hook(...) installs an optional timing callback for slow-frame diagnostics.
  • Frame::render_widget_timed(...), Frame::time_named(...), and Frame::time_pane(...) collect per-widget or named-pane diagnostics available from Terminal::last_frame_diagnostics() after a draw.
  • FrameDiagnostic::static_name(...) records common pane diagnostics without allocating the diagnostic name.
  • NamedFrameTiming provides a low-allocation static-name timing shape for small hot-path areas.
  • ThemeTokens provides shared widget roles: panel, text, dim, accent, success, warning, and error.
  • Theme::tokens(), Block::with_theme_tokens(...), CodeTheme::from_tokens, OutputTheme::from_tokens, and ScrollableText::with_theme_tokens(...) let apps style common Scrin widgets without hand-mapping every color.
  • Block::inner_for_bordered(area) returns the fast one-cell bordered content rect without constructing a block.
use scrin::core::rect::Rect;
use scrin::terminal::{PresentStrategy, Terminal};
use scrin::theme::Theme;
use scrin::widgets::{Block, RetainedMarkdownOutput, ScrollableText};

let tokens = Theme::DARK.tokens();
let transcript = ScrollableText::raw("alpha beta gamma").with_theme_tokens(tokens);
let markdown = RetainedMarkdownOutput::new("# Preview").with_theme(
    scrin::widgets::markdown_output::OutputTheme::from_tokens(tokens),
);

# fn demo(mut terminal: Terminal) -> std::io::Result<()> {
let area = Rect::new(0, 0, 40, 10);
let timing = terminal.draw_with_present_strategy(PresentStrategy::Areas(vec![area]), |frame| {
    frame.render_widget_timed("panel", Block::bordered().title("status"), area);
})?;
let _slow = timing.elapsed.as_millis() > 16;
let _diagnostics = terminal.last_frame_diagnostics();
# Ok(())
# }

Validation Still Needed

Scrin's automated checks cover formatting, compilation, examples, tests, docs, packaging, and publish dry-runs. Applications with highly animated full-screen TUIs should still add app-level terminal validation:

  • Fixed-size pseudo-terminal captures for the full-buffer frame path.
  • Visible prompt input captures.
  • Overlay captures for model, context, kanban, revert, provider setup, and other application-specific panels.
  • Restore-path tests for quit flows such as Ctrl+Q, including cursor/raw-mode recovery.
  • A human visual pass in at least one real terminal emulator to catch subjective spacing, clipping, contrast, and animation pacing issues that byte-level tests cannot judge.

Demos

Run demos with the scripts in scripts/:

scripts/demo_widgets
scripts/demo_overlays
scripts/demo_scrin_shell
scripts/demo_toggle_exotic
scripts/demo_aisling
scripts/demo_loaders
scripts/demo_aisling_story

The Aisling story demo is the broadest visual smoke test: it walks the available effect inventory and rotates through loader systems using the Aisling bridge.

Publishing Notes

The crate is intended to publish cleanly to crates.io and build on docs.rs. The package excludes generated target/** artifacts and includes source, examples, tests, scripts, and this README.

Recommended pre-publish checks:

cargo fmt
cargo check
cargo check --examples
cargo test
cargo doc --no-deps
cargo publish --dry-run --allow-dirty