use std::{io, time::Duration};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use scrin::{
Terminal,
layout::{Constraint, Direction, Layout},
widgets::Widget,
widgets::block::{Block, BorderStyle},
};
use scrin_widgets::{
AislingPalette, Bordered, Gauge, List, Paragraph, Sparkline, SplitPane, StatusBar, StreamPanel,
TabBar, Table,
};
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;
let mut active_tab: usize = 0;
let mut selected_item: usize = 0;
let mut scroll_offset: u16 = 0;
let log_lines: Vec<String> = (0..200)
.map(|i| format!("[{i:04}] task_{i}: completed in {}.{}ms", i % 97, i % 10))
.collect();
let table_rows: Vec<[String; 3]> = (0..50)
.map(|i| {
[
format!("agent_{i:03}"),
format!("{}", ["idle", "running", "blocked", "done"][i % 4]),
format!("{}ms", (i * 37) % 500),
]
})
.collect();
let list_items: Vec<String> = (0..40)
.map(|i| format!("resource_{}", ["alpha", "bravo", "charlie", "delta"][i % 4]))
.collect();
loop {
let sel = selected_item;
let scr = scroll_offset;
let tick_val = tick;
terminal.draw(|frame| {
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Min(10),
Constraint::Length(1),
])
.split(frame.area());
let buf = frame.buffer();
let palette = AislingPalette::cypherpunk();
TabBar::new(["Stream", "Layout", "Data", "Metrics"])
.selected(active_tab)
.tick(tick_val)
.palette(palette)
.render(buf, root[0]);
match active_tab {
0 => render_stream_page(buf, root[1], tick_val, &palette, &log_lines, scr),
1 => render_layout_page(buf, root[1], tick_val, &palette),
2 => render_data_page(
buf,
root[1],
tick_val,
&palette,
&list_items,
&table_rows,
sel,
),
_ => render_metrics_page(buf, root[1], tick_val, &palette),
}
StatusBar::new()
.left(format!("page {}/4", active_tab + 1))
.center(format!("tick {tick_val}"))
.right("scrin-widgets · new widgets")
.palette(palette)
.render(buf, root[2]);
})?;
if event::poll(Duration::from_millis(33))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Char('1') => active_tab = 0,
KeyCode::Char('2') => active_tab = 1,
KeyCode::Char('3') => {
active_tab = 2;
selected_item = 0;
}
KeyCode::Char('4') => active_tab = 3,
KeyCode::Up => {
if active_tab == 2 {
selected_item = selected_item.saturating_sub(1);
}
}
KeyCode::Down => {
if active_tab == 2 {
let max = list_items.len().max(table_rows.len()) - 1;
selected_item = (selected_item + 1).min(max);
}
}
KeyCode::Left => {
scroll_offset = scroll_offset.saturating_sub(1);
}
KeyCode::Right => {
scroll_offset = scroll_offset.saturating_add(1);
}
KeyCode::Tab => {
active_tab = (active_tab + 1) % 4;
}
KeyCode::BackTab => {
active_tab = active_tab.wrapping_sub(1) % 4;
}
_ => {}
}
}
}
}
tick = tick.wrapping_add(1);
}
Ok(())
}
fn render_stream_page(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
tick: u64,
palette: &AislingPalette,
lines: &[String],
scroll: u16,
) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(8), Constraint::Length(5)])
.split(area);
let visible_count = lines.len().min(tick as usize + 30);
let visible_lines: Vec<String> = lines[..visible_count].to_vec();
StreamPanel::new()
.lines(visible_lines)
.show_line_numbers(true)
.follow_tail(true)
.tick(tick)
.palette(palette.clone())
.block(
Block::new("streaming output")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.low),
)
.render(buf, layout[0]);
let bottom = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[1]);
Paragraph::new(format!(
"StreamPanel demo.\n\
Lines appear over time.\n\
scroll_offset: {scroll}\n\
Total lines: {}",
lines.len()
))
.palette(palette.clone())
.block(
Block::new("info")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.mid),
)
.render(buf, bottom[0]);
Gauge::new(tick as f64 / 200.0)
.label(format!("{}%", (tick as f64 / 2.0).min(100.0) as u16))
.palette(palette.clone())
.block(
Block::new("stream progress")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.pulse),
)
.render(buf, bottom[1]);
}
fn render_layout_page(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
tick: u64,
palette: &AislingPalette,
) {
let (left, right, div) = SplitPane::vertical().ratio(0.45).divider('┃').split(area);
SplitPane::vertical()
.ratio(0.45)
.divider('┃')
.render_divider(buf, div, palette.clone());
let (top_left, bottom_left, hdiv) =
SplitPane::horizontal().ratio(0.55).divider('━').split(left);
SplitPane::horizontal()
.ratio(0.55)
.divider('━')
.render_divider(buf, hdiv, palette.clone());
Bordered::new("top left")
.palette(palette.clone())
.render(buf, top_left);
Bordered::new("bottom left")
.palette(palette.clone())
.render(buf, bottom_left);
let (top_right, bottom_right, _hdiv2) = SplitPane::horizontal().ratio(0.4).split(right);
Bordered::new("top right")
.palette(palette.clone())
.render(buf, top_right);
Bordered::new("bottom right")
.palette(palette.clone())
.render(buf, bottom_right);
let inner_tr = Bordered::new("top right").render_inner(buf, top_right);
Sparkline::new(vec![
5,
8,
12,
(tick % 20) as u16 + 3,
15,
9,
7,
11,
(tick % 15) as u16 + 5,
13,
10,
6,
])
.palette(palette.clone())
.render(buf, inner_tr);
let inner_br = Bordered::new("bottom right").render_inner(buf, bottom_right);
let wave = ((tick as f64 * 0.05).sin() * 0.5 + 0.5) as f64;
Gauge::new(wave)
.label(format!("{}%", (wave * 100.0) as u16))
.palette(palette.clone())
.render(buf, inner_br);
}
fn render_data_page(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
_tick: u64,
palette: &AislingPalette,
list_items: &[String],
table_rows: &[[String; 3]],
selected: usize,
) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(area);
let left = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[0]);
let sel_list = if selected < list_items.len() {
Some(selected)
} else {
None
};
List::new()
.items(list_items.iter().take(30).cloned())
.selected(sel_list)
.palette(palette.clone())
.block(
Block::new("list")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.low),
)
.render(buf, left[0]);
Sparkline::new(vec![
3, 7, 1, 9, 4, 8, 2, 6, 5, 10, 3, 7, 8, 2, 9, 5, 1, 6, 4, 10,
])
.palette(palette.clone())
.block(
Block::new("sparkline")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.mid),
)
.render(buf, left[1]);
let table_sel = if selected < table_rows.len() {
Some(selected)
} else {
None
};
Table::new(["name", "status", "latency"])
.rows(table_rows.iter().take(20).cloned())
.selected(table_sel)
.palette(palette.clone())
.block(
Block::new("table")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.high),
)
.render(buf, layout[1]);
}
fn render_metrics_page(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
tick: u64,
palette: &AislingPalette,
) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(5),
])
.split(area);
let wave1 = ((tick as f64 * 0.04).sin() * 0.5 + 0.5) as f64;
Gauge::new(wave1)
.label(format!("cpu {}%", (wave1 * 100.0) as u16))
.palette(palette.clone())
.block(
Block::new("cpu")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.low),
)
.render(buf, layout[0]);
let wave2 = ((tick as f64 * 0.03).cos() * 0.5 + 0.5) as f64;
Gauge::new(wave2)
.label(format!("mem {}%", (wave2 * 100.0) as u16))
.palette(AislingPalette::phosphor())
.block(
Block::new("memory")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.mid),
)
.render(buf, layout[1]);
let wave3 = ((tick as f64 * 0.025 + 1.0).sin() * 0.5 + 0.5) as f64;
Gauge::new(wave3)
.label(format!("disk {}%", (wave3 * 100.0) as u16))
.palette(AislingPalette::flare())
.block(
Block::new("disk")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.pulse),
)
.render(buf, layout[2]);
let bottom = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[3]);
Sparkline::new(
(0..24)
.map(|i| ((tick as f64 * 0.03 + i as f64 * 0.5).sin() * 40.0 + 50.0) as u16)
.collect(),
)
.palette(palette.clone())
.block(
Block::new("network throughput")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.low),
)
.render(buf, bottom[0]);
Paragraph::new(format!(
"dashboard metrics\n\
tick: {tick}\n\
gauges: 3 live\n\
sparkline: 24 points"
))
.palette(palette.clone())
.block(
Block::new("status")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.shadow),
)
.render(buf, bottom[1]);
}