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},
    style::Style,
    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 orchard = AislingPalette {
        low: Color::rgb(80, 255, 176),
        mid: Color::rgb(129, 96, 255),
        high: Color::rgb(245, 255, 173),
        pulse: Color::rgb(255, 93, 217),
        shadow: Color::rgb(10, 11, 26),
    };
    let ember = AislingPalette {
        low: Color::rgb(255, 120, 72),
        mid: Color::rgb(255, 54, 138),
        high: Color::rgb(255, 236, 181),
        pulse: Color::rgb(93, 226, 255),
        shadow: Color::rgb(34, 12, 18),
    };
    let mut tick = 0_u64;

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

            let title = color_block("void orchard", orchard.pulse);
            let title_inner = title.inner(root[0]);
            title.render(buffer, root[0]);
            Paragraph::new("bioluminescent packet-fruit blooming in a black terminal grove")
                .with_style(Style::default().fg(orchard.high).bold())
                .with_word_wrap(true)
                .aisling()
                .tick(tick)
                .palette(orchard)
                .intensity(8)
                .render(buffer, title_inner);

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

            GlyphRain::new(tick)
                .density(54)
                .glyphs("✶✦✧✺✹✸✷✳")
                .palette(orchard)
                .block(color_block("spore fall", orchard.low))
                .render(buffer, middle[0]);

            let charge = ((tick % 120) as f64 / 120.0 - 0.5).abs();
            NebulaGauge::new(1.0 - charge * 2.0)
                .tick(tick)
                .label("mycelial uplink")
                .palette(ember)
                .block(color_block("root pulse", ember.pulse))
                .render(buffer, middle[1]);

            SignalPanel::new("orchid daemon")
                .line("sap: neon")
                .line("hive: listening")
                .line("gate: half-open")
                .line("fruit: encrypted")
                .tick(tick)
                .palette(orchard)
                .render(buffer, middle[2]);

            let lower = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Percentage(38), Constraint::Percentage(62)])
                .split(root[2]);

            NebulaGauge::new(((tick % 88) as f64 / 88.0).sin().abs())
                .tick(tick + 31)
                .label("moon-sugar pressure")
                .palette(orchard)
                .block(color_block("vessel", orchard.high))
                .render(buffer, lower[0]);

            let note = color_block("field note", ember.mid);
            let note_inner = note.inner(lower[1]);
            note.render(buffer, lower[1]);
            Paragraph::new("Every panel is Scrin-native: colored borders, live gauges, signal bars, glyph fields, and Aisling shimmer share one buffer.")
                .with_word_wrap(true)
                .with_style(Style::default().fg(ember.high))
                .aisling()
                .tick(tick + 9)
                .palette(ember)
                .intensity(6)
                .render(buffer, note_inner);
        })?;

        if event::poll(Duration::from_millis(45))? {
            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 color_block(title: &str, color: Color) -> Block<'_> {
    Block::new(title)
        .with_borders(BorderStyle::Plain)
        .with_border_color(color)
        .with_inner_margin(scrin::Rect::ZERO)
}