scrin-widgets 0.2.4

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

use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use scrin::{
    Color, Terminal,
    layout::{Constraint, Direction, Layout},
    widgets::{
        Paragraph, Widget,
        block::{Block, BorderStyle},
    },
};
use scrin_widgets::{AislingExt, AislingPalette, GlyphRain, NebulaGauge, SignalPanel};

fn main() -> io::Result<()> {
    let mut terminal = Terminal::init()?;
    let result = run(&mut terminal);
    terminal.restore()?;
    result
}

fn run(terminal: &mut Terminal) -> 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());
            let buffer = frame.buffer();

            let header = bordered_block("scrin-widgets", AislingPalette::cypherpunk().pulse);
            let header_inner = header.inner(root[0]);
            header.render(buffer, root[0]);
            Paragraph::new("Scrin Widgets ships ambient fields, luminous progress, signal panels, and Aisling effects that decorate any Scrin widget. Press q or Esc to leave the dream.")
                .with_word_wrap(true)
                .aisling()
                .tick(tick)
                .palette(AislingPalette::cypherpunk())
                .intensity(6)
                .render(buffer, header_inner);

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

            GlyphRain::new(tick)
                .density(42)
                .palette(AislingPalette::phosphor())
                .block(bordered_block(
                    "ambient glyph stream",
                    AislingPalette::phosphor().mid,
                ))
                .render(buffer, middle[0]);

            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())
                .render(buffer, 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;

            NebulaGauge::new(ratio)
                .tick(tick)
                .label(format!("charge {:>3}%", (ratio * 100.0) as u16))
                .block(bordered_block("gauge", AislingPalette::cypherpunk().low))
                .render(buffer, lower[0]);

            let wrapper = bordered_block("wrapper", AislingPalette::cypherpunk().pulse);
            let wrapper_inner = wrapper.inner(lower[1]);
            wrapper.render(buffer, lower[1]);
            Paragraph::new(
                "The Aisling wrapper is intentionally generic: render your own widget first, then let scrin-widgets tint the buffer.",
            )
            .with_word_wrap(true)
            .aisling()
            .tick(tick + 19)
            .palette(AislingPalette::cypherpunk())
            .intensity(5)
            .render(buffer, wrapper_inner);
        })?;

        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(())
}

fn bordered_block(title: &str, color: Color) -> Block<'_> {
    Block::new(title)
        .with_borders(BorderStyle::Plain)
        .with_border_color(color)
        .with_inner_margin(scrin::Rect::ZERO)
}