use std::{io, time::Duration};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use scrin::{
Frame, PresentStrategy, Rect, Terminal, TerminalOptions,
core::buffer::Cell,
layout::{Constraint, Direction, Layout},
widgets::Clear,
};
use scrin_widgets::{AislingExt, AislingPalette, List, Paragraph, StatusBar, StreamPanel, Table};
fn main() -> io::Result<()> {
let mut terminal = Terminal::init_with(TerminalOptions {
mouse_capture: true,
bracketed_paste: true,
..TerminalOptions::default()
})?;
let result = run(&mut terminal);
terminal.restore()?;
result
}
fn run(terminal: &mut Terminal) -> io::Result<()> {
let mut tick = 0_u64;
let mut probe = (4_u16, 5_u16);
let mut selected = 0_usize;
let mut stream_scroll = 0_u16;
let stream_lines: Vec<String> = (0..90)
.map(|i| {
format!(
"[{i:03}] selectable transcript row -> payload {:02x}",
i * 17 % 255
)
})
.collect();
let list_items: Vec<String> = [
"hit region",
"selectable span",
"scroll row",
"selection group",
"logical row",
"probe cursor",
"dirty rect",
]
.into_iter()
.map(String::from)
.collect();
let rows: Vec<[String; 3]> = (0..18)
.map(|i| {
[
format!("row_{i:02}"),
["warm", "focus", "copy", "route"][i % 4].to_string(),
format!("span:{}", i + 1),
]
})
.collect();
loop {
terminal.draw_with_present_strategy(PresentStrategy::MarkedDirty, |frame| {
let palette = AislingPalette::cypherpunk();
let area = frame.area();
probe.0 = probe.0.min(area.width.saturating_sub(1));
probe.1 = probe.1.min(area.height.saturating_sub(1));
frame.render_widget_timed("clear", Clear::with_bg(palette.shadow), area);
frame.mark_dirty(area);
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5),
Constraint::Min(10),
Constraint::Length(1),
])
.split(area);
render_header(frame, root[0], tick, palette);
let body = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(root[1]);
let right = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(36),
Constraint::Percentage(34),
Constraint::Percentage(30),
])
.split(body[1]);
let visible = stream_lines.len().min(18 + tick as usize % 24);
StreamPanel::new()
.lines(stream_lines[..visible].iter().cloned())
.show_line_numbers(true)
.follow_tail(stream_scroll == 0)
.scroll_offset(stream_scroll)
.tick(tick)
.palette(palette)
.block(palette.block("stream: hit + span + scroll"))
.render_with_interaction(frame, "meta:stream", body[0]);
List::new()
.items(list_items.iter().cloned())
.selected(Some(selected.min(list_items.len().saturating_sub(1))))
.tick(tick)
.palette(palette)
.block(palette.block("list metadata"))
.render_with_interaction(frame, "meta:list", right[0]);
Table::new(["id", "state", "copy"])
.rows(rows.iter().cloned())
.selected(Some(selected.min(rows.len().saturating_sub(1))))
.tick(tick)
.palette(palette)
.block(palette.block("table metadata"))
.render_with_interaction(frame, "meta:table", right[1]);
let report = metadata_report(frame, probe);
frame.render_widget_timed(
"metadata report",
Paragraph::new(report)
.palette(palette)
.block(palette.block("probe report")),
right[2],
);
frame.mark_dirty(right[2]);
draw_probe(frame, probe, palette);
frame.render_widget_timed(
"status",
StatusBar::new()
.left("arrows move probe")
.center("j/k select rows | [/] scroll stream")
.right("q quits")
.palette(palette),
root[2],
);
frame.mark_dirty(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::Left => probe.0 = probe.0.saturating_sub(1),
KeyCode::Right => probe.0 = probe.0.saturating_add(1),
KeyCode::Up => probe.1 = probe.1.saturating_sub(1),
KeyCode::Down => probe.1 = probe.1.saturating_add(1),
KeyCode::Char('k') => selected = selected.saturating_sub(1),
KeyCode::Char('j') => {
let max = list_items.len().max(rows.len()).saturating_sub(1);
selected = (selected + 1).min(max);
}
KeyCode::Char('[') => stream_scroll = stream_scroll.saturating_add(1),
KeyCode::Char(']') => stream_scroll = stream_scroll.saturating_sub(1),
_ => {}
}
}
}
}
tick = tick.wrapping_add(1);
}
Ok(())
}
fn render_header(frame: &mut Frame<'_>, area: Rect, tick: u64, palette: AislingPalette) {
let block = palette.block("Scrin interaction metadata flow");
let inner = block.inner(area);
frame.render_widget_timed("header:block", block, area);
frame.render_widget_timed(
"header:text",
Paragraph::new(format!(
"StreamPanel, List, and Table now register HitRegion, SelectableSpan, and ScrollRowHit metadata.\nMove the probe over rows to see logical mappings. tick {tick}"
))
.palette(palette)
.aisling()
.tick(tick)
.intensity(4),
inner,
);
frame.mark_dirty(area);
}
fn metadata_report(frame: &Frame<'_>, probe: (u16, u16)) -> String {
let layer = frame.interaction_layer();
let hit = layer
.hit_test(probe.0, probe.1)
.map(|region| format!("{} [{:?}] row {:?}", region.label, region.role, region.row))
.unwrap_or_else(|| "none".to_string());
let scroll = layer
.scroll_hit_test(probe.0, probe.1)
.map(|hit| format!("{} -> logical row {}", hit.region_id, hit.logical_row))
.unwrap_or_else(|| "none".to_string());
let selectable = layer
.selectable_at(probe.0, probe.1)
.map(|(span, point)| format!("{} @ line {} col {}", span.text, point.line, point.column))
.unwrap_or_else(|| "none".to_string());
format!(
"probe: {}, {}\nregions: {}\nspans: {}\nscroll regions: {}\nhit: {hit}\nscroll: {scroll}\nselectable: {selectable}",
probe.0,
probe.1,
layer.regions.len(),
layer.selectable_spans.len(),
layer.scroll_regions.len(),
)
}
fn draw_probe(frame: &mut Frame<'_>, probe: (u16, u16), palette: AislingPalette) {
frame.buffer().set(
usize::from(probe.0),
usize::from(probe.1),
Cell::new('@', palette.pulse, Some(palette.shadow)).with_bold(true),
);
frame.mark_dirty(Rect::new(probe.0, probe.1, 1, 1));
}