# 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.83"
```
## 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.
- Render-time interaction metadata for stable hit regions, hover/click routing,
scroll-aware row hit testing, selectable spans, and code-only copy groups.
- 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())?;
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, transcript viewports, status decks, todo lists,
pane cockpits, kanban boards, popups, toggles, scrollbars, and clearing helpers.
- `scrin::terminal`: raw-mode setup, frame drawing, diff/full presenters,
cursor control, mouse capture, bracketed paste, hit lookup, dirty region marks,
and restore-once lifecycle.
- `scrin::interaction`: stable widget IDs, hit regions, widget roles, selectable
spans, hover enter/leave tracking, normalized UI events, and selection models.
- `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`, `DirtyBounds`, and `MarkedDirty`.
- `Frame::mark_dirty(...)` lets hover/selection changes invalidate only affected
rectangles, and `Terminal::last_dirty_regions()` exposes the last marked set.
- `Frame::register_hit_region(...)`, `register_metadata_region(...)`,
`register_selectable_span(...)`, and `register_scroll_region(...)` attach
interaction metadata while rendering.
- `Terminal::hit_test(...)`, `scroll_hit_test(...)`, `selectable_at(...)`, and
`handle_pointer_event(...)` query the latest frame's interaction metadata and
emit normalized UI events with widget-local coordinates.
- `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.
## Interaction Metadata
Scrin can now keep widget interaction data beside rendered cells. Widgets can
register stable `WidgetId` regions with `WidgetRole`, labels, tooltips, actions,
cursor hints, row/column metadata, selection groups, accessibility state, and
values. `InteractionLayer` supports topmost hit testing, scroll-aware logical row
lookups, selectable/copyable span extraction, hover enter/leave dirty regions,
and `UiEventMapper` for mouse move/down/drag/up/click events.
Metadata-aware render paths are available on `CodeBlock`, `RetainedMarkdownOutput`,
`ScrollableText`, `List`, `Tabs`, `TodoList`, `StatusDeck`, `PaneCockpit`,
`KanbanBoard`, `TranscriptViewport`, and `CachedEffectPane`.
## KnottCode Widgets
New higher-level widgets centralize repeated app-side glue:
- `TranscriptViewport` owns retained Markdown rows, scroll bounds, bottom-sticky
transcript behavior, and selectable transcript metadata.
- `PaneMouseRouter` maps terminal mouse coordinates to named panes, routes wheel
events to an allow-list, and keeps prompt/input rectangles non-scrollable.
- `CachedEffectPane` rebuilds Aisling effects only when kind/text/size/duration/
seed/palette changes, renders requested frame indices, and supports muted
palettes plus header/footer overlays.
- `StatusDeck` renders compact toggles, runtime/key rows, right-pane visibility
state, and validation badges with hit regions.
- `PaneCockpit` lays out named panes as columns or stacked panels depending on
terminal width, with consistent titles, borders, focus colors, padding, and
optional subtle background motion.
- `TodoList` renders checklist rows with stable IDs, hover/selection state,
click actions, and selectable row text.
- `KanbanBoard` renders compact lanes/cards with counts, badges, empty hints,
intake labels, overflow hints, and stable card hit regions.
```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
```