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 forge = AislingPalette {
        low: Color::rgb(255, 111, 63),
        mid: Color::rgb(255, 183, 77),
        high: Color::rgb(255, 248, 194),
        pulse: Color::rgb(93, 210, 255),
        shadow: Color::rgb(39, 19, 10),
    };
    let frost = AislingPalette {
        low: Color::rgb(72, 197, 255),
        mid: Color::rgb(117, 124, 255),
        high: Color::rgb(224, 244, 255),
        pulse: Color::rgb(170, 255, 232),
        shadow: Color::rgb(9, 15, 30),
    };
    let mut tick = 211_u64;

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

            GlyphRain::new(tick)
                .density(24 + (tick % 20) as u16)
                .glyphs("*:.+xX#@")
                .palette(frost)
                .block(color_block("cold starfield", frost.mid))
                .render(buffer, root[0]);

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

            SignalPanel::new("forge left")
                .line("bellows: awake")
                .line("ore: glass-gold")
                .line("gravity: braided")
                .line("tongs: singing")
                .tick(tick)
                .palette(forge)
                .render(buffer, middle[0]);

            let core = color_block("helium anvil", forge.high);
            let core_inner = core.inner(middle[1]);
            core.render(buffer, middle[1]);
            Paragraph::new("     STARFORGE\n\n plasma threads folded into terminal glass\n border light behaves like hot metal\n gauges breathe pressure while glyphs drift cold\n press q or Esc to bank the furnace")
                .with_style(Style::default().fg(forge.high).bold())
                .with_word_wrap(true)
                .aisling()
                .tick(tick)
                .palette(forge)
                .intensity(9)
                .render(buffer, core_inner);

            SignalPanel::new("forge right")
                .line("quench: blue")
                .line("runes: aligned")
                .line("shell: intact")
                .line("output: crown")
                .tick(tick + 17)
                .palette(frost)
                .render(buffer, middle[2]);

            let lower = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([
                    Constraint::Percentage(33),
                    Constraint::Percentage(34),
                    Constraint::Percentage(33),
                ])
                .split(root[2]);
            let crown = wave_ratio(tick, 96, 0);
            let field = wave_ratio(tick, 128, 31);
            let dream = wave_ratio(tick, 88, 57);

            NebulaGauge::new(crown)
                .tick(tick)
                .label(format!("thermal crown {:>3}%", (crown * 100.0) as u16))
                .palette(forge)
                .block(color_block("crown", forge.pulse))
                .render(buffer, lower[0]);

            NebulaGauge::new(field)
                .tick(tick + 11)
                .label(format!("magnet lock {:>3}%", (field * 100.0) as u16))
                .palette(frost)
                .block(color_block("field", frost.pulse))
                .render(buffer, lower[1]);

            NebulaGauge::new(dream)
                .tick(tick + 23)
                .label(format!("coolant {:>3}%", (dream * 100.0) as u16))
                .palette(AislingPalette::cypherpunk())
                .block(color_block("coolant", AislingPalette::cypherpunk().mid))
                .render(buffer, lower[2]);
        })?;

        if event::poll(Duration::from_millis(40))? {
            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 wave_ratio(tick: u64, period: u64, offset: u64) -> f64 {
    let phase = ((tick + offset) % period) as f64 / period as f64;
    1.0 - (phase - 0.5).abs() * 2.0
}

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