scrin-widgets 0.1.0

Plug-and-play Ratatui widgets and Aisling terminal effects for Scrin TUIs.
Documentation
use std::{io, time::Duration};

use crossterm::{
    event::{self, Event, KeyCode, KeyEventKind},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
    Terminal,
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout},
    widgets::{Block, Paragraph, Wrap},
};
use scrin_widgets::{AislingExt, AislingPalette, GlyphRain, NebulaGauge, SignalPanel};

type Tui = Terminal<CrosstermBackend<io::Stdout>>;

fn main() -> io::Result<()> {
    let mut terminal = setup_terminal()?;
    let result = run(&mut terminal);
    restore_terminal(terminal)?;
    result
}

fn setup_terminal() -> io::Result<Tui> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen)?;
    Terminal::new(CrosstermBackend::new(stdout))
}

fn restore_terminal(mut terminal: Tui) -> io::Result<()> {
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()
}

fn run(terminal: &mut Tui) -> io::Result<()> {
    let mut tick = 0_u64;

    loop {
        terminal.draw(|frame| {
            let root = Layout::default()
                .direction(Direction::Vertical)
                .constraints([
                    Constraint::Length(6),
                    Constraint::Min(10),
                    Constraint::Length(7),
                ])
                .split(frame.area());

            frame.render_widget(
                Paragraph::new("Scrin Widgets ships ambient fields, luminous progress, signal panels, and Aisling effects that decorate any Ratatui widget. Press q or Esc to leave the dream.")
                    .block(Block::bordered().title("scrin-widgets"))
                    .wrap(Wrap { trim: true })
                    .aisling()
                    .tick(tick)
                    .palette(AislingPalette::dream())
                    .intensity(6),
                root[0],
            );

            let middle = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
                .split(root[1]);

            frame.render_widget(
                GlyphRain::new(tick)
                    .density(42)
                    .palette(AislingPalette::phosphor())
                    .block(Block::bordered().title("ambient glyph stream")),
                middle[0],
            );

            frame.render_widget(
                SignalPanel::new("aisling relay")
                    .line("phase: lucid")
                    .line("carrier: 8.13 THz")
                    .line("noise: below horizon")
                    .line("mode: exotic TUI")
                    .tick(tick)
                    .palette(AislingPalette::flare()),
                middle[1],
            );

            let lower = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
                .split(root[2]);
            let wave = ((tick % 100) as f64 / 100.0 - 0.5).abs();
            let ratio = 1.0 - wave * 2.0;

            frame.render_widget(
                NebulaGauge::new(ratio)
                    .tick(tick)
                    .label(format!("dream charge {:>3}%", (ratio * 100.0) as u16))
                    .block(Block::bordered().title("nebula gauge")),
                lower[0],
            );

            frame.render_widget(
                Paragraph::new("The Aisling wrapper is intentionally generic: render your own widget first, then let scrin-widgets tint the buffer.")
                    .block(Block::bordered().title("wrapper"))
                    .wrap(Wrap { trim: true })
                    .aisling()
                    .tick(tick + 19)
                    .palette(AislingPalette::flare())
                    .intensity(5),
                lower[1],
            );
        })?;

        if event::poll(Duration::from_millis(33))? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press
                    && matches!(key.code, KeyCode::Char('q') | KeyCode::Esc)
                {
                    break;
                }
            }
        }

        tick = tick.wrapping_add(1);
    }

    Ok(())
}