scrin 0.1.83

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyModifiers},
    execute,
    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use scrin::core::buffer::Buffer;
use scrin::core::color::Color;
use scrin::core::rect::Rect;
use scrin::effects::LoaderPlayer;
use scrin::layout::{Constraint, Layout};
use scrin::status_bar::{StatusBar, StatusBarPosition};
use scrin::widgets::block::{Block, BorderStyle};
use scrin::widgets::Widget;
use std::io::{self, Write};

fn main() -> io::Result<()> {
    let mut stdout = io::stdout();
    terminal::enable_raw_mode()?;
    execute!(stdout, EnterAlternateScreen, cursor::Hide)?;

    let all_kinds = LoaderPlayer::all_kinds();
    let grid_cols = 4;
    let grid_rows = 4;
    let page_size = (grid_cols * grid_rows) as usize;
    let mut page = 0usize;
    let mut tick: usize = 0;
    let mut progress_val: f32 = 0.0;
    let mut progress_dir: f32 = 0.02;
    let mut status = StatusBar::new().with_position(StatusBarPosition::Bottom);

    let result = (|| -> io::Result<()> {
        loop {
            let (cols, rows) = terminal::size()?;
            let mut buffer = Buffer::new(cols as usize, rows as usize);

            let outer = Rect::new(0, 0, cols, rows - 2);
            let block = Block::new("Loader Demo — arrows navigate, j/k adjust progress, q quit")
                .with_borders(BorderStyle::Rounded)
                .with_border_color(Color::rgb(88, 166, 255));
            block.render(&mut buffer, outer);

            let inner = block.inner(outer);
            let start = page * page_size;
            let end = (start + page_size).min(all_kinds.len());
            let count = end.saturating_sub(start);

            let cols_count = grid_cols.min(count as u16);
            let rows_count = grid_rows.min(((count as u16 + grid_cols - 1) / grid_cols).max(1));

            let col_constraints: Vec<Constraint> = (0..cols_count)
                .map(|_| Constraint::Percentage(100 / grid_cols))
                .collect();
            let row_constraints: Vec<Constraint> = (0..rows_count)
                .map(|_| Constraint::Percentage(100 / grid_rows))
                .collect();

            let row_layout = Layout::vertical(row_constraints).margin(Rect::new(1, 1, 1, 1));
            let row_rects = row_layout.split(inner);

            for (row_idx, &row_rect) in row_rects.iter().enumerate() {
                let col_layout = Layout::horizontal(col_constraints.clone());
                let col_rects = col_layout.split(row_rect);

                for (col_idx, &col_rect) in col_rects.iter().enumerate() {
                    let item_idx = row_idx * grid_cols as usize + col_idx;
                    if item_idx >= count {
                        break;
                    }
                    let kind = all_kinds[start + item_idx];
                    let loader = LoaderPlayer::new(kind);
                    let progress = LoaderPlayer::progress_from_fraction(progress_val);
                    let name = loader.name();
                    let mini_block = Block::new(name)
                        .with_borders(BorderStyle::Rounded)
                        .with_border_color(Color::rgb(48, 54, 61));
                    mini_block.render(&mut buffer, col_rect);
                    let inner_area = mini_block.inner(col_rect);
                    loader.render(tick, progress, &mut buffer, inner_area);
                }
            }

            progress_val += progress_dir;
            if progress_val >= 1.0 {
                progress_val = 1.0;
                progress_dir = -0.01;
            } else if progress_val <= 0.0 {
                progress_val = 0.0;
                progress_dir = 0.02;
            }

            let total_pages = (all_kinds.len() + page_size - 1) / page_size;
            status.set_left(
                &format!("Loaders page {}/{}", page + 1, total_pages),
                Color::rgb(88, 166, 255),
            );
            status.set_right(
                &format!("Progress: {:.0}%", progress_val * 100.0),
                Color::rgb(139, 148, 158),
            );
            let status_area = Rect::new(0, rows - 1, cols, 1);
            status.render(&mut buffer, status_area);

            write!(stdout, "\x1b[H{}", buffer.to_ansi_string())?;
            stdout.flush()?;

            if event::poll(std::time::Duration::from_millis(80))? {
                if let Event::Key(key) = event::read()? {
                    match (key.modifiers, key.code) {
                        (KeyModifiers::CONTROL, KeyCode::Char('c'))
                        | (KeyModifiers::CONTROL, KeyCode::Char('q'))
                        | (_, KeyCode::Char('q')) => return Ok(()),
                        (_, KeyCode::Right) | (_, KeyCode::Char('l')) => {
                            if (page + 1) * page_size < all_kinds.len() {
                                page += 1;
                            }
                        }
                        (_, KeyCode::Left) | (_, KeyCode::Char('h')) => {
                            page = page.saturating_sub(1);
                        }
                        (_, KeyCode::Up) | (_, KeyCode::Char('k')) => {
                            progress_val = (progress_val + 0.1).min(1.0);
                        }
                        (_, KeyCode::Down) | (_, KeyCode::Char('j')) => {
                            progress_val = (progress_val - 0.1).max(0.0);
                        }
                        (_, KeyCode::Char('r')) => {
                            tick = 0;
                            progress_val = 0.0;
                            progress_dir = 0.02;
                        }
                        _ => {}
                    }
                }
            }

            tick = tick.wrapping_add(1);
        }
    })();

    execute!(stdout, cursor::Show, LeaveAlternateScreen)?;
    terminal::disable_raw_mode()?;
    result
}