use std::{io, time::Duration};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use scrin::{
Color, Terminal,
layout::{Constraint, Direction, Layout},
widgets::{
Paragraph, Widget,
block::{Block, BorderStyle},
clear::Clear,
tabs::Tabs,
},
};
use scrin_widgets::{
AislingExt, AislingPalette, FlickerPanel, GlyphRain, NebulaGauge, NeonBorder, OrbField,
PulseRing, Radar, SignalPanel, WaveType, Waveform,
};
const TAB_TITLES: &[&str] = &["Glyphs", "Gauges", "Signals", "Aisling"];
const HELP: &str = " 1-4:tabs Tab/Shift-Tab:cycle O:overlay q:quit ─ scrin-widgets v0.2.4";
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 = 0_u8;
let mut show_overlay = false;
let mut overlay_tab = 0_u8;
loop {
terminal.draw(|frame| {
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2),
Constraint::Min(8),
Constraint::Length(1),
])
.split(frame.area());
let buffer = frame.buffer();
let palette = AislingPalette::cypherpunk();
let tab_style = scrin::style::Style::default().fg(palette.shadow);
let highlight = scrin::style::Style::default()
.fg(palette.high)
.add_modifier(scrin::style::Modifier::BOLD);
Tabs::new(TAB_TITLES)
.with_selected(usize::from(active_tab))
.with_style(tab_style)
.with_highlight_style(highlight)
.render(buffer, root[0]);
match active_tab {
0 => render_glyphs(buffer, root[1], tick, &palette),
1 => render_gauges(buffer, root[1], tick, &palette),
2 => render_signals(buffer, root[1], tick, &palette),
_ => render_aisling(buffer, root[1], tick, &palette),
}
if show_overlay {
render_overlay(buffer, root[1], tick, &palette, overlay_tab);
}
for (i, ch) in HELP.chars().enumerate() {
let x = root[2].x + i as u16;
if x < root[2].x + root[2].width {
scrin::core::buffer::Buffer::set(
buffer,
usize::from(x),
usize::from(root[2].y),
scrin::core::buffer::Cell::new(ch, palette.shadow, None),
);
}
}
})?;
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,
KeyCode::Char('4') => active_tab = 3,
KeyCode::Char('o') | KeyCode::Char('O') => show_overlay = !show_overlay,
KeyCode::Tab => {
if show_overlay {
overlay_tab = (overlay_tab + 1) % 4;
} else {
active_tab = (active_tab + 1) % 4;
}
}
KeyCode::BackTab => {
if show_overlay {
overlay_tab = overlay_tab.wrapping_sub(1) % 4;
} else {
active_tab = active_tab.wrapping_sub(1) % 4;
}
}
_ => {}
}
}
}
}
tick = tick.wrapping_add(1);
}
Ok(())
}
fn render_glyphs(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
tick: u64,
palette: &AislingPalette,
) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(area);
let top = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(layout[0]);
GlyphRain::new(tick)
.density(38)
.palette(AislingPalette::phosphor())
.block(block("glyph rain", palette.low))
.render(buf, top[0]);
OrbField::new(12)
.tick(tick)
.palette(palette.clone())
.block(block("orb field", palette.mid))
.render(buf, top[1]);
let bottom = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[1]);
NeonBorder::new(block("", palette.low))
.tick(tick)
.palette(palette.clone())
.render(buf, bottom[0]);
let neon_inner = bottom[0];
let content_area = scrin::Rect::new(
neon_inner.x + 1,
neon_inner.y + 1,
neon_inner.width.saturating_sub(2),
neon_inner.height.saturating_sub(2),
);
Paragraph::new("neon border cycles\ncolors around edges")
.with_word_wrap(true)
.render(buf, content_area);
FlickerPanel::new("SCRIN WIDGETS")
.tick(tick)
.intensity(4)
.palette(palette.clone())
.block(block("flicker", palette.high))
.render(buf, bottom[1]);
}
fn render_gauges(
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(6),
])
.split(area);
let wave1 = ((tick as f64 * 0.04).sin() * 0.5 + 0.5) as f64;
NebulaGauge::new(wave1)
.tick(tick)
.label(format!("signal {:>3}%", (wave1 * 100.0) as u16))
.block(block("gauge · cyan", palette.low))
.render(buf, layout[0]);
let wave2 = ((tick as f64 * 0.03).cos() * 0.5 + 0.5) as f64;
NebulaGauge::new(wave2)
.tick(tick + 50)
.label(format!("temp {:>3}%", (wave2 * 100.0) as u16))
.palette(AislingPalette::flare())
.block(block("gauge · amber", palette.pulse))
.render(buf, layout[1]);
let wave3 = ((tick as f64 * 0.025).sin() * 0.5 + 0.5) as f64;
NebulaGauge::new(wave3)
.tick(tick + 100)
.label(format!("mem {:>3}%", (wave3 * 100.0) as u16))
.palette(AislingPalette::phosphor())
.block(block("gauge · green", palette.mid))
.render(buf, layout[2]);
let bottom = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(65), Constraint::Percentage(35)])
.split(layout[3]);
Waveform::new(4.0, 0.6)
.tick(tick)
.wave_type(WaveType::Sine)
.palette(palette.clone())
.block(block("waveform · sine", palette.low))
.render(buf, bottom[0]);
Radar::new(5)
.tick(tick)
.palette(palette.clone())
.render(buf, bottom[1]);
}
fn render_signals(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
tick: u64,
palette: &AislingPalette,
) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
.split(area);
let left = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(8), Constraint::Length(9)])
.split(layout[0]);
SignalPanel::new("relay")
.line("phase: lucid")
.line("carrier: 8.13 THz")
.line("noise: below horizon")
.line("mode: exotic TUI")
.tick(tick)
.palette(palette.clone())
.render(buf, left[0]);
let pulse_area = left[1];
let pulse_inner = scrin::Rect::new(
pulse_area.x + 1,
pulse_area.y + 1,
pulse_area.width.saturating_sub(2),
pulse_area.height.saturating_sub(2),
);
PulseRing::new(3)
.tick(tick)
.palette(palette.clone())
.render(buf, pulse_inner);
block("pulse", palette.low).render(buf, pulse_area);
let right = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[1]);
SignalPanel::new("alpha")
.line("status: active")
.line("latency: 12ms")
.line("packets: 4,201")
.tick(tick)
.palette(palette.clone())
.render(buf, right[0]);
SignalPanel::new("beta")
.line("status: standby")
.line("latency: 89ms")
.line("packets: 1,087")
.tick(tick + 30)
.palette(AislingPalette::phosphor())
.render(buf, right[1]);
}
fn render_aisling(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
tick: u64,
palette: &AislingPalette,
) {
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
let left = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[0]);
let b1 = block("aisling · cyan", palette.low);
let inner1 = b1.inner(left[0]);
b1.render(buf, left[0]);
Paragraph::new("Cyan cyber shimmer. Scanlines and edge glow on any Scrin widget. Use .intensity() to tune 0..=10.")
.with_word_wrap(true)
.aisling()
.tick(tick)
.palette(palette.clone())
.intensity(6)
.render(buf, inner1);
let b2 = block("aisling · phosphor", palette.mid);
let inner2 = b2.inner(left[1]);
b2.render(buf, left[1]);
Paragraph::new("Green phosphor surveillance. CRT monitor aesthetic for terminal-core themes and data readouts.")
.with_word_wrap(true)
.aisling()
.tick(tick + 20)
.palette(AislingPalette::phosphor())
.intensity(7)
.render(buf, inner2);
let right = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[1]);
let b3 = block("aisling · flare", palette.pulse);
let inner3 = b3.inner(right[0]);
b3.render(buf, right[0]);
Paragraph::new("Amber warning flare. Max shimmer for alert states and warning panels.")
.with_word_wrap(true)
.aisling()
.tick(tick + 40)
.palette(AislingPalette::flare())
.intensity(9)
.render(buf, inner3);
let b4 = block("aisling · glyphs", palette.high);
let inner4 = b4.inner(right[1]);
b4.render(buf, right[1]);
GlyphRain::new(tick)
.density(25)
.palette(palette.clone())
.aisling()
.tick(tick)
.intensity(4)
.render(buf, inner4);
}
fn render_overlay(
buf: &mut scrin::core::buffer::Buffer,
area: scrin::Rect,
tick: u64,
palette: &AislingPalette,
overlay_tab: u8,
) {
let popup_w = (area.width * 70 / 100).max(20);
let popup_h = (area.height * 60 / 100).max(8);
let popup_x = area.x + (area.width.saturating_sub(popup_w)) / 2;
let popup_y = area.y + (area.height.saturating_sub(popup_h)) / 2;
let popup = scrin::Rect::new(popup_x, popup_y, popup_w, popup_h);
Clear::with_bg(Color::rgb(6, 10, 18)).render(buf, popup);
let overlay_titles = &["Radar", "Wave", "Flicker", "Gauge"];
let highlight = scrin::style::Style::default()
.fg(palette.high)
.add_modifier(scrin::style::Modifier::BOLD);
let tab_style = scrin::style::Style::default().fg(palette.shadow);
let tab_area = scrin::Rect::new(popup.x, popup.y, popup_w, 1);
Tabs::new(overlay_titles)
.with_selected(usize::from(overlay_tab))
.with_style(tab_style)
.with_highlight_style(highlight)
.render(buf, tab_area);
let content = scrin::Rect::new(popup.x, popup.y + 1, popup_w, popup_h.saturating_sub(2));
match overlay_tab {
0 => {
Radar::new(4)
.tick(tick)
.palette(palette.clone())
.render(buf, content);
}
1 => {
Waveform::new(3.0, 0.8)
.tick(tick)
.wave_type(WaveType::Sawtooth)
.palette(palette.clone())
.render(buf, content);
}
2 => {
FlickerPanel::new("OVERLAY ACTIVE")
.tick(tick)
.intensity(7)
.palette(palette.clone())
.render(buf, content);
}
_ => {
let ratio = ((tick as f64 * 0.03).sin() * 0.5 + 0.5) as f64;
NebulaGauge::new(ratio)
.tick(tick)
.label(format!("load {:>3}%", (ratio * 100.0) as u16))
.palette(palette.clone())
.render(buf, content);
}
}
let ol_help = " Tab:switch O:close ";
let ol_help_x = popup.x + (popup_w.saturating_sub(ol_help.len() as u16)) / 2;
let ol_help_y = popup.y + popup_h.saturating_sub(1);
if ol_help_y >= popup.y && ol_help_x + ol_help.len() as u16 <= popup.x + popup_w {
Paragraph::new(ol_help).render(
buf,
scrin::Rect::new(ol_help_x, ol_help_y, ol_help.len() as u16, 1),
);
}
}
fn block(title: &str, color: Color) -> Block<'_> {
Block::new(title)
.with_borders(BorderStyle::Plain)
.with_border_color(color)
.with_inner_margin(scrin::Rect::ZERO)
}