use crossterm::{
cursor, queue,
style::Print,
terminal::{size, ClearType},
};
use std::io::{stdout, Write};
pub struct BufferWriter {
buffer: String,
line_count: usize,
}
impl Default for BufferWriter {
fn default() -> Self {
Self::new()
}
}
impl BufferWriter {
pub fn new() -> Self {
Self {
buffer: String::with_capacity(64 * 1024),
line_count: 0,
}
}
#[allow(dead_code)] pub fn reset(&mut self) {
self.buffer.clear();
self.line_count = 0;
}
pub fn get_buffer(&self) -> &str {
&self.buffer
}
pub fn line_count(&self) -> usize {
self.line_count
}
}
impl Write for BufferWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let s = std::str::from_utf8(buf)
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid UTF-8"))?;
self.line_count += s.matches('\n').count();
self.buffer.push_str(s);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub struct DifferentialRenderer {
previous_lines: Vec<String>,
screen_height: usize,
screen_width: usize,
}
impl DifferentialRenderer {
pub fn new() -> std::io::Result<Self> {
let (width, height) = size().unwrap_or((80, 24));
Ok(Self {
previous_lines: Vec::new(),
screen_height: height as usize,
screen_width: width as usize,
})
}
pub fn update_dimensions(&mut self, width: u16, height: u16) {
let w = width as usize;
let h = height as usize;
if w != self.screen_width || h != self.screen_height {
self.screen_width = w;
self.screen_height = h;
self.previous_lines.resize(h, String::new());
}
}
pub fn render_differential(
&mut self,
content: &str,
cols: u16,
rows: u16,
) -> std::io::Result<()> {
self.update_dimensions(cols, rows);
if self.previous_lines.is_empty() {
self.previous_lines = vec![String::new(); self.screen_height];
}
let mut stdout = stdout();
let mut current_line_count = 0;
let mut any_changes = false;
for (line_num, current_line) in content.lines().enumerate() {
if line_num >= self.screen_height {
break;
}
current_line_count = line_num + 1;
if self.previous_lines[line_num] != current_line {
queue!(
stdout,
cursor::MoveTo(0, line_num as u16),
crossterm::terminal::Clear(ClearType::UntilNewLine),
Print(current_line)
)?;
self.previous_lines[line_num].clear();
self.previous_lines[line_num].push_str(current_line);
any_changes = true;
}
}
for line_num in current_line_count..self.screen_height {
if !self.previous_lines[line_num].is_empty() {
queue!(
stdout,
cursor::MoveTo(0, line_num as u16),
crossterm::terminal::Clear(ClearType::CurrentLine)
)?;
self.previous_lines[line_num].clear();
any_changes = true;
}
}
if any_changes {
stdout.flush()?;
}
Ok(())
}
pub fn force_clear(&mut self) -> std::io::Result<()> {
let mut stdout = stdout();
queue!(stdout, crossterm::terminal::Clear(ClearType::All))?;
stdout.flush()?;
self.previous_lines.clear();
self.previous_lines
.resize(self.screen_height, String::new());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buffer_writer_basic() {
let mut bw = BufferWriter::new();
write!(bw, "hello\nworld\n").unwrap();
assert_eq!(bw.get_buffer(), "hello\nworld\n");
assert_eq!(bw.line_count(), 2);
}
#[test]
fn test_buffer_writer_reset_preserves_capacity() {
let mut bw = BufferWriter::new();
write!(bw, "some content\n").unwrap();
let cap_before = bw.buffer.capacity();
bw.reset();
assert!(bw.get_buffer().is_empty());
assert_eq!(bw.line_count(), 0);
assert_eq!(bw.buffer.capacity(), cap_before);
}
#[test]
fn test_buffer_writer_preallocated_capacity() {
let bw = BufferWriter::new();
assert!(bw.buffer.capacity() >= 64 * 1024);
}
#[test]
fn test_differential_renderer_update_dimensions() {
let mut dr = DifferentialRenderer {
previous_lines: Vec::new(),
screen_height: 24,
screen_width: 80,
};
dr.update_dimensions(120, 40);
assert_eq!(dr.screen_width, 120);
assert_eq!(dr.screen_height, 40);
assert_eq!(dr.previous_lines.len(), 40);
}
#[test]
fn test_differential_renderer_update_dimensions_noop() {
let mut dr = DifferentialRenderer {
previous_lines: vec![String::new(); 24],
screen_height: 24,
screen_width: 80,
};
dr.update_dimensions(80, 24);
assert_eq!(dr.screen_width, 80);
assert_eq!(dr.screen_height, 24);
}
#[test]
fn test_force_clear_resets_state() {
let mut dr = DifferentialRenderer {
previous_lines: vec!["old content".to_string(); 24],
screen_height: 24,
screen_width: 80,
};
let _ = dr.force_clear();
assert_eq!(dr.previous_lines.len(), 24);
assert!(dr.previous_lines.iter().all(|l| l.is_empty()));
}
#[test]
fn test_buffer_writer_throughput() {
let mut bw = BufferWriter::new();
let line = "x".repeat(120);
let start = std::time::Instant::now();
for _ in 0..1000 {
bw.reset();
for _ in 0..40 {
write!(bw, "{line}\n").unwrap();
}
let _ = bw.get_buffer();
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 1000,
"BufferWriter throughput too slow: {elapsed:?}"
);
}
}