use std::{io, time::Duration};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use scrin::{
Frame, FrameTiming, PresentStrategy, Rect, Terminal, TerminalOptions,
layout::{Constraint, Direction, Layout},
widgets::Clear,
};
use scrin_widgets::{
AislingExt, AislingPalette, FrameStats, Gauge, List, Paragraph, SignalPanel, StatusBar,
StreamPanel,
};
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 selected = 0_usize;
let mut last_timing: Option<FrameTiming> = None;
let stream_lines: Vec<String> = (0..120)
.map(|i| {
format!(
"[{i:03}] diagnostic pane emitted workload slice {:02x}",
i * 19 % 255
)
})
.collect();
let stages: Vec<String> = [
"collect diagnostics",
"diff presenter",
"dirty regions",
"hit metadata",
"selectable spans",
"scroll rows",
"frame stats",
]
.into_iter()
.map(String::from)
.collect();
loop {
let timing = last_timing;
terminal.draw_with_present_strategy(PresentStrategy::MarkedDirty, |frame| {
let palette = match (tick / 100) % 3 {
0 => AislingPalette::cypherpunk(),
1 => AislingPalette::phosphor(),
_ => AislingPalette::flare(),
};
frame.render_widget_timed("clear", Clear::with_bg(palette.shadow), frame.area());
frame.mark_dirty(frame.area());
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5),
Constraint::Min(10),
Constraint::Length(9),
Constraint::Length(1),
])
.split(frame.area());
render_header(frame, root[0], tick, palette, timing);
frame.time_pane("workload", root[1], |frame| {
render_workload(
frame,
root[1],
tick,
palette,
selected,
&stream_lines,
&stages,
);
});
frame.render_widget_timed(
"frame stats",
FrameStats::from_frame(frame, timing)
.palette(palette)
.max_diagnostics(8)
.block(palette.block("FrameStats::from_frame")),
root[2],
);
frame.mark_dirty(root[2]);
frame.render_widget_timed(
"status",
StatusBar::new()
.left(format!("frame {}", frame.frame_count()))
.center("j/k move selection | q quits")
.right(format!("diagnostics {}", frame.diagnostics().len()))
.palette(palette),
root[3],
);
frame.mark_dirty(root[3]);
})?;
last_timing = terminal.last_frame_timing();
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('k') | KeyCode::Up => selected = selected.saturating_sub(1),
KeyCode::Char('j') | KeyCode::Down => {
selected = (selected + 1).min(stages.len().saturating_sub(1));
}
_ => {}
}
}
}
}
tick = tick.wrapping_add(1);
}
Ok(())
}
fn render_header(
frame: &mut Frame<'_>,
area: Rect,
tick: u64,
palette: AislingPalette,
timing: Option<FrameTiming>,
) {
let timing = timing
.map(|timing| {
format!(
"last frame: {}us, {} bytes, {} dirty cells",
timing.elapsed.as_micros(),
timing.bytes_written,
timing.dirty_cells
)
})
.unwrap_or_else(|| "last frame: warming presenter".to_string());
let block = palette.block("Scrin frame diagnostics flow");
let inner = block.inner(area);
frame.render_widget_timed("header:block", block, area);
frame.render_widget_timed(
"header:text",
Paragraph::new(format!(
"FrameStats combines Terminal::last_frame_timing, Frame::diagnostics, dirty regions, and interaction counts. tick {tick}\n{timing}"
))
.palette(palette)
.aisling()
.tick(tick)
.intensity(4),
inner,
);
frame.mark_dirty(area);
}
fn render_workload(
frame: &mut Frame<'_>,
area: Rect,
tick: u64,
palette: AislingPalette,
selected: usize,
stream_lines: &[String],
stages: &[String],
) {
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
.split(area);
let right = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(42),
Constraint::Length(4),
Constraint::Min(4),
])
.split(columns[1]);
let visible = stream_lines.len().min(20 + tick as usize % 50);
StreamPanel::new()
.lines(stream_lines[..visible].iter().cloned())
.show_line_numbers(true)
.follow_tail(true)
.tick(tick)
.palette(palette)
.block(palette.block("timed stream"))
.render_with_interaction(frame, "stats:stream", columns[0]);
List::new()
.items(stages.iter().cloned())
.selected(Some(selected))
.tick(tick)
.palette(palette)
.block(palette.block("interaction metadata"))
.render_with_interaction(frame, "stats:stages", right[0]);
frame.render_widget_timed(
"dirty gauge",
Gauge::new(wave_ratio(tick, 140, 0))
.label("dirty budget")
.palette(palette)
.block(palette.block("presenter")),
right[1],
);
frame.mark_dirty(right[1]);
frame.render_widget_timed(
"signal",
SignalPanel::new("diagnostics")
.line("time_pane -> NamedFrameTiming")
.line("render_widget_timed -> FrameDiagnostic")
.line("MarkedDirty -> dirty region count")
.tick(tick)
.palette(palette),
right[2],
);
frame.mark_dirty(right[2]);
}
fn wave_ratio(tick: u64, period: u64, offset: u64) -> f64 {
let phase = ((tick + offset) % period) as f64 / period as f64;
1.0 - (phase - 0.5).abs() * 2.0
}