use std::{io, time::Duration};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use scrin::{
FrameTiming, PresentStats, Rect, Style, Terminal, TerminalOptions,
core::buffer::{Buffer, CellStyle},
layout::{Constraint, Direction, Layout},
widgets::Widget,
};
use scrin_widgets::{
AislingExt, AislingPalette, AislingTerminalExt, DiffRunSummary, Gauge, Paragraph, SignalPanel,
StatusBar, StreamPanel, collect_diff_run_summaries, write_diff_ansi_to,
};
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 area_mode = false;
let mut last_timing: Option<FrameTiming> = None;
let mut last_stats: Option<PresentStats> = None;
let mut last_runs: Vec<DiffRunSummary> = Vec::new();
loop {
let palette = match (tick / 120) % 3 {
0 => AislingPalette::cypherpunk(),
1 => AislingPalette::dream(),
_ => AislingPalette::flare(),
};
let area = terminal.size();
let dashboard = DirectPresentDashboard {
tick,
palette,
area_mode,
last_timing,
last_stats,
last_runs: &last_runs,
};
let buffer = terminal.render_widget_buffer(dashboard);
let areas = [area];
let area_filter = area_mode.then_some(areas.as_slice());
let mut streamed = Vec::new();
let stats = write_diff_ansi_to(
&mut streamed,
terminal.front_buffer(),
&buffer,
area.width,
area.height,
area_filter,
)?;
let runs =
collect_diff_run_summaries(terminal.front_buffer(), &buffer, area.width, area.height);
let timing = if area_mode {
terminal.present_buffer_areas_timed(&buffer, &areas)?
} else {
terminal.present_buffer_timed(&buffer)?
};
last_timing = Some(timing);
last_stats = Some(stats);
last_runs = runs.into_iter().take(8).collect();
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('a') => area_mode = !area_mode,
_ => {}
}
}
}
}
tick = tick.wrapping_add(1);
}
Ok(())
}
struct DirectPresentDashboard<'a> {
tick: u64,
palette: AislingPalette,
area_mode: bool,
last_timing: Option<FrameTiming>,
last_stats: Option<PresentStats>,
last_runs: &'a [DiffRunSummary],
}
impl Widget for DirectPresentDashboard<'_> {
fn render(&self, buf: &mut Buffer, area: Rect) {
let style = Style::new().fg(self.palette.high).bg(self.palette.shadow);
buf.fill_styled(area, ' ', style);
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5),
Constraint::Min(10),
Constraint::Length(8),
Constraint::Length(1),
])
.split(area);
self.render_header(buf, root[0]);
self.render_body(buf, root[1]);
self.render_diff_report(buf, root[2]);
StatusBar::new()
.left(format!(
"present: {}",
if self.area_mode { "areas" } else { "buffer" }
))
.center("A toggles present_buffer_areas_timed | q quits")
.right(format!("tick {}", self.tick))
.palette(self.palette)
.render(buf, root[3]);
}
}
impl DirectPresentDashboard<'_> {
fn render_header(&self, buf: &mut Buffer, area: Rect) {
let block = self.palette.block("Scrin 0.1.84 direct presentation flow");
let inner = block.inner(area);
block.render(buf, area);
Paragraph::new(format!(
"Standalone Buffer -> diff_runs/write_diff_to -> present_buffer_timed. tick {}\n{}",
self.tick,
timing_line(self.last_timing)
))
.palette(self.palette)
.aisling()
.tick(self.tick)
.intensity(4)
.render(buf, inner);
}
fn render_body(&self, buf: &mut Buffer, area: Rect) {
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(45),
Constraint::Length(4),
Constraint::Min(4),
])
.split(columns[1]);
let stream_lines: Vec<String> = (0..80)
.map(|i| format!("[{i:03}] styled bulk buffer cell run {:02x}", i * 23 % 255))
.collect();
let visible = stream_lines.len().min(18 + self.tick as usize % 36);
StreamPanel::new()
.lines(stream_lines[..visible].iter().cloned())
.show_line_numbers(true)
.follow_tail(true)
.tick(self.tick)
.palette(self.palette)
.block(self.palette.block("rendered into standalone buffer"))
.render(buf, columns[0]);
SignalPanel::new("0.1.84 paths")
.line("Buffer::fill_styled")
.line("Buffer::set_str_styled")
.line("diff_runs -> style runs")
.line("write_diff_to -> Vec<u8>")
.line("present_buffer*_timed")
.tick(self.tick)
.palette(self.palette)
.render(buf, right[0]);
Gauge::new(wave_ratio(self.tick, 160, 0))
.label("direct diff")
.palette(self.palette)
.block(self.palette.block("streamed ANSI"))
.render(buf, right[1]);
Paragraph::new(format!(
"wide glyph metadata: {}\nCellStyle sample: {}",
if buf.has_wide_glyphs() {
"present"
} else {
"clear"
},
style_summary(CellStyle::from_style(
Style::new()
.fg(self.palette.mid)
.bg(self.palette.shadow)
.bold()
))
))
.palette(self.palette)
.block(self.palette.block("buffer metadata"))
.render(buf, right[2]);
buf.set_str_styled(
usize::from(right[2].x.saturating_add(1)),
usize::from(right[2].y.saturating_add(3)),
"wide safe: δΈ -> ascii overwrite",
Style::new()
.fg(self.palette.pulse)
.bg(self.palette.shadow)
.bold(),
);
}
fn render_diff_report(&self, buf: &mut Buffer, area: Rect) {
let stats = self
.last_stats
.map(|stats| {
format!(
"bytes {} | dirty cells {} | areas {} | full {}",
stats.bytes_written, stats.dirty_cells, stats.areas, stats.full_repaint
)
})
.unwrap_or_else(|| "warming diff writer".to_string());
let runs = if self.last_runs.is_empty() {
"runs: warming".to_string()
} else {
self.last_runs
.iter()
.map(|run| {
format!(
"@{},{} w{} cells{} {} {}",
run.x,
run.y,
run.width,
run.visible_cells,
style_summary(run.style),
run.text
)
})
.collect::<Vec<_>>()
.join("\n")
};
Paragraph::new(format!("{stats}\n{runs}"))
.palette(self.palette)
.block(self.palette.block("previous diff run summaries"))
.render(buf, area);
}
}
fn timing_line(timing: Option<FrameTiming>) -> String {
timing
.map(|timing| {
format!(
"last present: {}us, {} bytes, {} dirty cells",
timing.elapsed.as_micros(),
timing.bytes_written,
timing.dirty_cells
)
})
.unwrap_or_else(|| "last present: warming".to_string())
}
fn style_summary(style: CellStyle) -> String {
let mut flags = String::new();
if style.bold {
flags.push('b');
}
if style.italic {
flags.push('i');
}
if style.underlined {
flags.push('u');
}
if flags.is_empty() {
flags.push('-');
}
format!("fg:{:?} bg:{:?} {flags}", style.fg, style.bg)
}
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
}