scrin 0.1.74

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::layout::{Constraint, Layout};
use scrin::status_bar::{StatusBar, StatusBarPosition};
use scrin::widgets::barchart::{Bar, BarChart};
use scrin::widgets::block::{Block, BorderStyle};
use scrin::widgets::gauge::Gauge;
use scrin::widgets::list::{List, ListItem};
use scrin::widgets::scrollbar::{ScrollBar, ScrollBarOrientation};
use scrin::widgets::sparkline::Sparkline;
use scrin::widgets::table::{self, Table};
use scrin::widgets::tabs::Tabs;
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 tab_names = &["Gauges", "Lists", "Tables", "Charts", "Scroll"];
    let mut tab_index: usize = 0;
    let mut progress: f32 = 0.0;
    let mut progress_dir: f32 = 0.008;
    let mut scroll_pos: usize = 0;
    let mut list_selected: usize = 0;
    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("Widget Showcase — Tab switch, 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 main_layout = Layout::vertical(vec![Constraint::Length(1), Constraint::Min(0)]);
            let main_rects = main_layout.split(inner);

            let tabs = Tabs::new(tab_names).with_selected(tab_index);
            tabs.render(&mut buffer, main_rects[0]);

            match tab_index {
                0 => render_gauges(&mut buffer, main_rects[1], progress),
                1 => render_lists(&mut buffer, main_rects[1], list_selected),
                2 => render_tables(&mut buffer, main_rects[1]),
                3 => render_charts(&mut buffer, main_rects[1], progress),
                4 => render_scroll(&mut buffer, main_rects[1], scroll_pos),
                _ => {}
            }

            progress += progress_dir;
            if progress >= 1.0 {
                progress = 1.0;
                progress_dir = -0.005;
            } else if progress <= 0.0 {
                progress = 0.0;
                progress_dir = 0.008;
            }

            status.set_left(
                &format!("Tab: {}", tab_names[tab_index]),
                Color::rgb(88, 166, 255),
            );
            status.set_right(
                &format!("Progress: {:.0}%", progress * 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::Tab) | (_, KeyCode::Right) => {
                            tab_index = (tab_index + 1) % tab_names.len();
                        }
                        (_, KeyCode::BackTab) | (_, KeyCode::Left) => {
                            tab_index = if tab_index == 0 {
                                tab_names.len() - 1
                            } else {
                                tab_index - 1
                            };
                        }
                        (_, KeyCode::Up) | (_, KeyCode::Char('k')) => {
                            list_selected = list_selected.saturating_sub(1);
                            scroll_pos = scroll_pos.saturating_sub(1);
                        }
                        (_, KeyCode::Down) | (_, KeyCode::Char('j')) => {
                            list_selected += 1;
                            scroll_pos += 1;
                        }
                        _ => {}
                    }
                }
            }
        }
    })();

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

fn render_gauges(buf: &mut Buffer, area: Rect, progress: f32) {
    let layout = Layout::vertical(vec![
        Constraint::Length(3),
        Constraint::Length(3),
        Constraint::Length(3),
        Constraint::Length(6),
        Constraint::Min(0),
    ]);
    let rects = layout.split(area);

    let gauge1 = Gauge::new()
        .with_ratio(progress as f64)
        .with_label("Download");
    gauge1.render(buf, rects[0]);

    let gauge2 = Gauge::new()
        .with_ratio((1.0 - progress) as f64)
        .with_label("Upload");
    gauge2.render(buf, rects[1]);

    let gauge3 = Gauge::new()
        .with_ratio((progress * 0.7 + 0.15) as f64)
        .with_label("CPU");
    gauge3.render(buf, rects[2]);

    let block = Block::new("Info")
        .with_borders(BorderStyle::Rounded)
        .with_border_color(Color::rgb(48, 54, 61));
    block.render(buf, rects[3]);
    let inner = block.inner(rects[3]);
    let lines = [
        "Gauges show progress with animated bars.",
        "They support unicode and ASCII modes.",
        "Use with_label() to add text overlay.",
    ];
    for (i, line) in lines.iter().enumerate() {
        if i as u16 >= inner.height {
            break;
        }
        buf.set_str(
            inner.x as usize,
            inner.y as usize + i,
            line,
            Color::rgb(110, 118, 129),
            None,
        );
    }

    let sparkline = Sparkline::new()
        .with_data(vec![
            3, 5, 7, 4, 6, 8, 5, 7, 9, 6, 8, 10, 7, 9, 11, 8, 12, 10, 8, 6,
        ])
        .with_max(12)
        .with_color(Color::rgb(88, 166, 255));
    sparkline.render(buf, rects[4]);
}

fn render_lists(buf: &mut Buffer, area: Rect, selected: usize) {
    let block = Block::new("List Widget")
        .with_borders(BorderStyle::Rounded)
        .with_border_color(Color::rgb(88, 166, 255));
    block.render(buf, area);
    let inner = block.inner(area);

    let items = vec![
        ListItem::new("Dashboard"),
        ListItem::new("Settings"),
        ListItem::new("Analytics"),
        ListItem::new("Notifications"),
        ListItem::new("Profile"),
        ListItem::new("Help"),
        ListItem::new("About"),
        ListItem::new("Logout"),
    ];
    let sel = selected % items.len();
    let list = List::new(&items).with_selected(sel);
    list.render(buf, inner);
}

fn render_tables(buf: &mut Buffer, area: Rect) {
    let block = Block::new("Table Widget")
        .with_borders(BorderStyle::Rounded)
        .with_border_color(Color::rgb(88, 166, 255));
    block.render(buf, area);
    let inner = block.inner(area);

    let header = table::Row::new(vec![
        table::Cell::new("Name"),
        table::Cell::new("Status"),
        table::Cell::new("Progress"),
    ]);
    let rows = vec![
        table::Row::new(vec![
            table::Cell::new("Build"),
            table::Cell::new("Running"),
            table::Cell::new("42%"),
        ]),
        table::Row::new(vec![
            table::Cell::new("Test"),
            table::Cell::new("Passed"),
            table::Cell::new("100%"),
        ]),
        table::Row::new(vec![
            table::Cell::new("Deploy"),
            table::Cell::new("Pending"),
            table::Cell::new("0%"),
        ]),
        table::Row::new(vec![
            table::Cell::new("Lint"),
            table::Cell::new("Failed"),
            table::Cell::new("67%"),
        ]),
    ];
    let widths = &[12, 12, 10];
    let table = Table::new(&rows, widths).with_header(&header);
    table.render(buf, inner);
}

fn render_charts(buf: &mut Buffer, area: Rect, progress: f32) {
    let layout = Layout::vertical(vec![Constraint::Length(10), Constraint::Min(0)]);
    let rects = layout.split(area);

    let bar_chart = BarChart::new().with_bars(vec![
        Bar::new("CPU", ((progress * 80.0) as u64).max(5)),
        Bar::new("MEM", (progress * 60.0 + 20.0) as u64),
        Bar::new("DISK", 45),
        Bar::new("NET", (progress * 50.0 + 10.0) as u64),
        Bar::new("GPU", (progress * 70.0 + 5.0) as u64),
    ]);
    bar_chart.render(buf, rects[0]);

    let block = Block::new("Chart Info")
        .with_borders(BorderStyle::Rounded)
        .with_border_color(Color::rgb(48, 54, 61));
    block.render(buf, rects[1]);
    let inner = block.inner(rects[1]);
    let lines = [
        "BarChart: vertical or horizontal bars",
        "Chart: scatter plots with datasets",
        "Sparkline: real-time data visualization",
    ];
    for (i, line) in lines.iter().enumerate() {
        if i as u16 >= inner.height {
            break;
        }
        buf.set_str(
            inner.x as usize,
            inner.y as usize + i,
            line,
            Color::rgb(110, 118, 129),
            None,
        );
    }
}

fn render_scroll(buf: &mut Buffer, area: Rect, scroll_pos: usize) {
    let block = Block::new("Scrollbar Demo — up/down to scroll")
        .with_borders(BorderStyle::Rounded)
        .with_border_color(Color::rgb(88, 166, 255));
    block.render(buf, area);
    let inner = block.inner(area);

    let line_layout = Layout::horizontal(vec![Constraint::Min(0), Constraint::Length(2)]);
    let line_rects = line_layout.split(inner);

    let total_lines: usize = 50;
    let viewport = line_rects[0].height as usize;
    let max_scroll = total_lines.saturating_sub(viewport);
    let pos = scroll_pos.min(max_scroll);

    for i in 0..viewport.min(total_lines) {
        let line_num = pos + i;
        if line_num >= total_lines {
            break;
        }
        let text = format!("  Line {:03}: Lorem ipsum dolor sit amet", line_num + 1);
        let display: String = text.chars().take(line_rects[0].width as usize).collect();
        let color = if line_num % 5 == 0 {
            Color::rgb(88, 166, 255)
        } else {
            Color::rgb(139, 148, 158)
        };
        buf.set_str(
            line_rects[0].x as usize,
            line_rects[0].y as usize + i,
            &display,
            color,
            None,
        );
    }

    let scrollbar = ScrollBar::new(ScrollBarOrientation::Vertical)
        .with_position(pos)
        .with_total(total_lines)
        .with_viewport(viewport);
    scrollbar.render(buf, line_rects[1]);
}