scrin 0.1.80

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

```toml
[dependencies]
scrin = "0.1.80"
```

## 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

```rust
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.

```rust,no_run
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.

```rust
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`.

```rust
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.

```rust
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/`:

```bash
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:

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