use crossterm::{
cursor, execute,
terminal::{self, ClearType},
};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
io::{self, BufWriter, Write},
sync::{Arc, Mutex},
};
static BUFFER_POOL: once_cell::sync::Lazy<Arc<Mutex<Vec<String>>>> =
once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(Vec::new())));
#[inline(always)]
fn get_pooled_buffer() -> String {
if let Ok(mut pool) = BUFFER_POOL.lock() {
pool.pop().unwrap_or_else(|| String::with_capacity(4096))
} else {
String::with_capacity(4096)
}
}
#[inline(always)]
fn return_to_pool(mut buffer: String) {
buffer.clear();
if buffer.capacity() <= 16384 {
if let Ok(mut pool) = BUFFER_POOL.lock() {
if pool.len() < 8 {
pool.push(buffer);
}
}
}
}
#[inline(always)]
fn fast_hash(content: &str) -> u64 {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
pub struct StringRenderer {
previous_content: String,
previous_lines: Vec<(usize, usize)>, width: u16,
height: u16,
previous_hash: u64,
buf_writer: Option<BufWriter<Box<dyn Write + Send>>>,
work_buffer: String,
}
impl StringRenderer {
#[inline]
pub fn new() -> Self {
let (width, height) = terminal::size().unwrap_or((80, 24));
Self {
previous_content: String::with_capacity(4096),
previous_lines: Vec::with_capacity(64),
width,
height,
previous_hash: 0,
buf_writer: None,
work_buffer: String::with_capacity(1024),
}
}
#[inline]
pub fn with_output(output: Box<dyn Write + Send>) -> Self {
let (width, height) = terminal::size().unwrap_or((80, 24));
Self {
previous_content: String::with_capacity(4096),
previous_lines: Vec::with_capacity(64),
width,
height,
previous_hash: 0,
buf_writer: Some(BufWriter::with_capacity(8192, output)),
work_buffer: String::with_capacity(1024),
}
}
pub fn render(&mut self, content: String) -> io::Result<()> {
let content_hash = fast_hash(&content);
if content_hash == self.previous_hash && content == self.previous_content {
return Ok(()); }
let mut work_buf = get_pooled_buffer();
let new_lines: Vec<&str> = content.lines().collect();
let (width, height) = terminal::size().unwrap_or((80, 24));
if width != self.width || height != self.height {
self.width = width;
self.height = height;
self.full_render_content(&new_lines, &mut work_buf)?;
} else {
self.diff_render_content(&new_lines, &mut work_buf)?;
}
self.update_line_cache(&content);
self.previous_content = content;
self.previous_hash = content_hash;
return_to_pool(work_buf);
if let Some(ref mut buf_writer) = self.buf_writer {
buf_writer.flush()?;
} else {
io::stdout().flush()?;
}
Ok(())
}
#[inline(always)]
fn update_line_cache(&mut self, content: &str) {
self.previous_lines.clear();
let est_lines = content.len() / 80 + 1; if self.previous_lines.capacity() < est_lines {
self.previous_lines
.reserve(est_lines - self.previous_lines.capacity());
}
let mut start = 0;
for line in content.lines() {
let end = start + line.len();
self.previous_lines.push((start, end));
start = end + 1; }
}
#[inline]
fn full_render_content(&mut self, lines: &[&str], work_buf: &mut String) -> io::Result<()> {
work_buf.clear();
work_buf.reserve(lines.len() * 80);
for (i, line) in lines.iter().enumerate() {
if i > 0 {
work_buf.push('\n');
}
work_buf.push_str(line);
}
if let Some(ref mut buf_writer) = self.buf_writer {
execute!(buf_writer, cursor::MoveTo(0, 0))?;
execute!(buf_writer, terminal::Clear(ClearType::All))?;
write!(buf_writer, "{}", work_buf)?;
} else {
let mut stdout = io::stdout();
execute!(stdout, cursor::MoveTo(0, 0))?;
execute!(stdout, terminal::Clear(ClearType::All))?;
write!(stdout, "{}", work_buf)?;
}
Ok(())
}
fn diff_render_content(&mut self, new_lines: &[&str], work_buf: &mut String) -> io::Result<()> {
let max_lines = new_lines.len().max(self.previous_lines.len());
work_buf.clear();
let mut pending_updates = Vec::with_capacity(max_lines);
for i in 0..max_lines {
let new_line = new_lines.get(i).copied().unwrap_or("");
let old_line = self
.previous_lines
.get(i)
.and_then(|(start, end)| self.previous_content.get(*start..*end))
.unwrap_or("");
if new_line.len() != old_line.len() || !self.lines_equal(new_line, old_line) {
pending_updates.push((i, new_line));
}
}
if !pending_updates.is_empty() {
self.apply_line_updates_content(&pending_updates, work_buf)?;
}
if self.previous_lines.len() > new_lines.len() {
if let Some(ref mut buf_writer) = self.buf_writer {
for i in new_lines.len()..self.previous_lines.len() {
execute!(buf_writer, cursor::MoveTo(0, i as u16))?;
execute!(buf_writer, terminal::Clear(ClearType::CurrentLine))?;
}
} else {
let mut stdout = io::stdout();
for i in new_lines.len()..self.previous_lines.len() {
execute!(stdout, cursor::MoveTo(0, i as u16))?;
execute!(stdout, terminal::Clear(ClearType::CurrentLine))?;
}
}
}
Ok(())
}
#[inline(always)]
fn lines_equal(&self, a: &str, b: &str) -> bool {
a == b
}
#[inline]
fn apply_line_updates_content(
&mut self,
updates: &[(usize, &str)],
work_buf: &mut String,
) -> io::Result<()> {
work_buf.clear();
work_buf.reserve(updates.len() * 100);
for &(line_idx, content) in updates {
use std::fmt::Write;
write!(work_buf, "\x1B[{};1H\x1B[2K{}", line_idx + 1, content)
.map_err(|_| io::Error::other("Format error"))?;
}
if let Some(ref mut buf_writer) = self.buf_writer {
buf_writer.write_all(work_buf.as_bytes())?;
} else {
io::stdout().write_all(work_buf.as_bytes())?;
}
Ok(())
}
pub fn clear(&mut self) -> io::Result<()> {
if let Some(ref mut buf_writer) = self.buf_writer {
execute!(buf_writer, terminal::Clear(ClearType::All))?;
execute!(buf_writer, cursor::MoveTo(0, 0))?;
buf_writer.flush()?;
} else {
let mut stdout = io::stdout();
execute!(stdout, terminal::Clear(ClearType::All))?;
execute!(stdout, cursor::MoveTo(0, 0))?;
stdout.flush()?;
}
self.previous_content.clear();
self.previous_lines.clear();
self.previous_hash = 0;
Ok(())
}
#[inline]
pub fn stats(&self) -> RenderStats {
RenderStats {
previous_lines: self.previous_lines.len(),
content_size: self.previous_content.len(),
terminal_size: (self.width, self.height),
content_hash: self.previous_hash,
buffer_capacity: self.work_buffer.capacity(),
}
}
pub fn reserve_capacity(&mut self, content_size: usize) {
if self.previous_content.capacity() < content_size {
self.previous_content
.reserve(content_size - self.previous_content.capacity());
}
if self.work_buffer.capacity() < content_size / 2 {
self.work_buffer.reserve(content_size / 2);
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RenderStats {
pub previous_lines: usize,
pub content_size: usize,
pub terminal_size: (u16, u16),
pub content_hash: u64,
pub buffer_capacity: usize,
}
impl Default for StringRenderer {
fn default() -> Self {
Self::new()
}
}