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,
}
}
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,
previous_content_hash: u64,
}
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,
previous_content_hash: 0,
})
}
fn hash_content(content: &str) -> u64 {
const FNV_OFFSET: u64 = 14695981039346656037;
const FNV_PRIME: u64 = 1099511628211;
let mut hash = FNV_OFFSET;
for byte in content.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
pub fn render_differential(&mut self, content: &str) -> std::io::Result<()> {
let content_hash = Self::hash_content(content);
if content_hash == self.previous_content_hash && !self.previous_lines.is_empty() {
return Ok(());
}
self.previous_content_hash = content_hash;
let (width, height) = size().unwrap_or((80, 24));
if width as usize != self.screen_width || height as usize != self.screen_height {
self.screen_width = width as usize;
self.screen_height = height as usize;
self.previous_lines
.resize(self.screen_height, String::new());
}
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;
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);
}
}
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();
}
}
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());
self.previous_content_hash = 0;
Ok(())
}
}