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
[]
= "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
use Buffer;
use Color;
use Rect;
use Style;
use Block;
use Paragraph;
use Widget;
let mut buffer = new;
buffer.fill;
let block = bordered
.title
.border_style;
block.render;
let text = new;
text.render;
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 ;
use Style;
use Block;
let mut terminal = init_with?;
terminal.draw?;
terminal.restore?;
# Ok::
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 Buffer;
use Color;
use Rect;
use ;
let mut buffer = new;
let mut effect = new
.with_size
.with_duration
.with_seed
.with_accent;
effect.render_to_buffer;
effect.advance;
Useful effect APIs:
EffectPlayer::new(kind, text)creates an Aisling-backed text effect.EffectPlayer::with_config(kind, text, config)accepts an AislingEffectConfigre-exported fromscrin::effects.with_size,with_duration,with_seed,with_accent, andwith_gradient_colorstune how the effect is generated and mapped.render_to_bufferandrender_frame_to_buffercopy Aisling frame cells into a Scrin buffer region.get_ansi_stringreturns Aisling's raw ANSI frame when you want standalone effect output.EffectPlayer::all_kinds()returns everyEffectKindavailable 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 Buffer;
use Color;
use Rect;
use ;
let mut buffer = new;
let loader = new
.with_size
.with_label
.with_unit
.with_fraction
.with_accent;
let progress = progress_from_counts;
loader.render;
Useful loader APIs:
LoaderPlayer::new(kind)creates an Aisling-backed loader.LoaderPlayer::with_config(kind, config)accepts an AislingLoaderConfigre-exported fromscrin::effects.with_size,with_label,with_unit,with_fraction,with_accent, andwith_gradient_colorstune 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_countsandprogress_from_fractioncreate Aisling progress values without importing Aisling directly.LoaderPlayer::all_kinds()returns everyLoaderKindavailable in the baked Aisling version.
Modules
scrin::core:Buffer,Cell,Color,Gradient, andRect.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-backedEffectPlayer,LoaderPlayer, effect kinds, loader kinds, progress, and config types.scrin::panes,scrin::overlays,scrin::command_palette,scrin::status_bar, andscrin::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 withwith_inner_margin(...).Frame::render_widget(widget, area)for frame-centric rendering.Buffer::cell_mut(x, y)plusCell::set_symbol,set_fg,set_bg, andset_stylefor post-render effects.Rect::is_empty()for direct zero-area checks.
Performance And Ergonomics APIs
Scrin includes APIs for larger, editor-like terminal applications:
ScrollableTextaccepts Scrin richText/Line/Spandata, owns aScrollState, caches wrapped rows by content/style/wrap mode/width, and renders from cached row slices without cloning row vectors.MarkdownOutput::retained()andRetainedMarkdownOutputcache parsed and wrapped Markdown rows by content, width, and wrap mode for preview/export panes, then render from cached row slices.RetainedEffectWidgetandRetainedLoaderWidgetcache 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(), andframe_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(...)supportsPresentStrategy::Diff,Full,Areas,DirtyBounds, andMarkedDirty.Frame::mark_dirty(...)lets hover/selection changes invalidate only affected rectangles, andTerminal::last_dirty_regions()exposes the last marked set.Frame::register_hit_region(...),register_metadata_region(...),register_selectable_span(...), andregister_scroll_region(...)attach interaction metadata while rendering.Terminal::hit_test(...),scroll_hit_test(...),selectable_at(...), andhandle_pointer_event(...)query the latest frame's interaction metadata and emit normalized UI events with widget-local coordinates.Terminal::present_area(...)andTerminal::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, andpresent_areas_timedreturnFrameTiming. Terminal::set_frame_timing_hook(...)installs an optional timing callback for slow-frame diagnostics.Frame::render_widget_timed(...),Frame::time_named(...), andFrame::time_pane(...)collect per-widget or named-pane diagnostics available fromTerminal::last_frame_diagnostics()after a draw.FrameDiagnostic::static_name(...)records common pane diagnostics without allocating the diagnostic name.NamedFrameTimingprovides a low-allocation static-name timing shape for small hot-path areas.ThemeTokensprovides shared widget roles:panel,text,dim,accent,success,warning, anderror.Theme::tokens(),Block::with_theme_tokens(...),CodeTheme::from_tokens,OutputTheme::from_tokens, andScrollableText::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:
TranscriptViewportowns retained Markdown rows, scroll bounds, bottom-sticky transcript behavior, and selectable transcript metadata.PaneMouseRoutermaps terminal mouse coordinates to named panes, routes wheel events to an allow-list, and keeps prompt/input rectangles non-scrollable.CachedEffectPanerebuilds Aisling effects only when kind/text/size/duration/ seed/palette changes, renders requested frame indices, and supports muted palettes plus header/footer overlays.StatusDeckrenders compact toggles, runtime/key rows, right-pane visibility state, and validation badges with hit regions.PaneCockpitlays out named panes as columns or stacked panels depending on terminal width, with consistent titles, borders, focus colors, padding, and optional subtle background motion.TodoListrenders checklist rows with stable IDs, hover/selection state, click actions, and selectable row text.KanbanBoardrenders compact lanes/cards with counts, badges, empty hints, intake labels, overflow hints, and stable card hit regions.
use Rect;
use ;
use Theme;
use ;
let tokens = DARK.tokens;
let transcript = raw.with_theme_tokens;
let markdown = new.with_theme;
#
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/:
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: