use std::io::Write;
pub type Result<T> = std::io::Result<T>;
pub struct LogUpdate<W: Write> {
writer: W,
previous_line_count: usize,
previous_output: String,
cursor_visible: bool,
}
impl<W: Write> LogUpdate<W> {
pub fn new(writer: W) -> Self {
Self {
writer,
previous_line_count: 0,
previous_output: String::new(),
cursor_visible: true,
}
}
pub fn previous_line_count(&self) -> usize {
self.previous_line_count
}
pub fn set_cursor_visible(&mut self, visible: bool) {
self.cursor_visible = visible;
}
pub fn render(&mut self, content: &str) -> Result<()> {
let output = format!("{}\r\n", content);
if output == self.previous_output {
return Ok(());
}
let mut buffer = String::new();
buffer.push_str("\x1b[?2026h");
buffer.push_str("\x1b[?25l");
if self.previous_line_count > 0 {
buffer.push_str(&format!("\x1b[{}A", self.previous_line_count));
for i in 0..self.previous_line_count {
buffer.push_str("\x1b[2K");
if i < self.previous_line_count - 1 {
buffer.push_str("\x1b[1B");
}
}
if self.previous_line_count > 1 {
buffer.push_str(&format!("\x1b[{}A", self.previous_line_count - 1));
}
buffer.push_str("\x1b[0G");
}
buffer.push_str(&output);
if self.cursor_visible {
buffer.push_str("\x1b[?25h");
}
buffer.push_str("\x1b[?2026l");
write!(self.writer, "{}", buffer)?;
self.writer.flush()?;
self.previous_output = output;
self.previous_line_count = self.previous_output.matches('\n').count().max(1);
Ok(())
}
pub fn clear(&mut self) -> Result<()> {
self.erase_lines(self.previous_line_count)?;
self.writer.flush()?;
self.previous_output.clear();
self.previous_line_count = 0;
Ok(())
}
pub fn handle_resize(&mut self) -> Result<()> {
if self.previous_line_count > 0 {
write!(self.writer, "\x1b[{}A", self.previous_line_count)?;
write!(self.writer, "\x1b[0G")?;
write!(self.writer, "\x1b[J")?;
self.writer.flush()?;
}
self.previous_output.clear();
self.previous_line_count = 0;
Ok(())
}
pub fn done(&mut self) -> Result<()> {
self.previous_output.clear();
self.previous_line_count = 0;
Ok(())
}
fn erase_lines(&mut self, count: usize) -> Result<()> {
if count == 0 {
return Ok(());
}
if count > 0 {
write!(self.writer, "\x1b[{}A", count)?;
}
for i in 0..count {
write!(self.writer, "\x1b[2K")?;
if i < count - 1 {
write!(self.writer, "\x1b[1B")?; }
}
if count > 1 {
write!(self.writer, "\x1b[{}A", count - 1)?;
}
write!(self.writer, "\x1b[0G")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_update_new() {
let buf = Vec::new();
let lu = LogUpdate::new(buf);
assert_eq!(lu.previous_line_count(), 0);
}
#[test]
fn test_log_update_render() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("Hello\nWorld").unwrap();
assert_eq!(lu.previous_line_count(), 2);
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("Hello"));
assert!(output.contains("World"));
}
#[test]
fn test_log_update_rerender_erases() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("First").unwrap();
lu.render("Second").unwrap();
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("\x1b["));
assert!(output.contains("Second"));
}
#[test]
fn test_log_update_clear() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("Content").unwrap();
lu.clear().unwrap();
assert_eq!(lu.previous_line_count(), 0);
}
}
#[test]
fn test_log_update_done() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("Final").unwrap();
lu.done().unwrap();
assert_eq!(lu.previous_line_count(), 0);
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("Final"));
}
#[test]
fn test_log_update_same_content_no_rerender() {
use std::cell::RefCell;
use std::rc::Rc;
struct CountingWriter {
data: Rc<RefCell<Vec<u8>>>,
write_count: Rc<RefCell<usize>>,
}
impl Write for CountingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
*self.write_count.borrow_mut() += 1;
self.data.borrow_mut().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let data = Rc::new(RefCell::new(Vec::new()));
let write_count = Rc::new(RefCell::new(0usize));
let writer = CountingWriter {
data: data.clone(),
write_count: write_count.clone(),
};
let mut lu = LogUpdate::new(writer);
lu.render("Same").unwrap();
let count_after_first = *write_count.borrow();
lu.render("Same").unwrap();
let count_after_second = *write_count.borrow();
assert_eq!(
count_after_first, count_after_second,
"Should not write more data if content unchanged"
);
}
#[test]
fn test_log_update_height_decrease() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("Line1\nLine2\nLine3").unwrap();
assert_eq!(lu.previous_line_count(), 3);
lu.render("Line1").unwrap();
assert_eq!(lu.previous_line_count(), 1);
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("\x1b["));
}
#[test]
fn test_log_update_empty_render() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("").unwrap();
assert_eq!(lu.previous_line_count(), 1);
}
}
#[test]
fn test_log_update_multiple_renders() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("A").unwrap();
lu.render("B").unwrap();
lu.render("C").unwrap();
assert_eq!(lu.previous_line_count(), 1);
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("C"));
}
#[test]
fn test_log_update_after_done() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("First").unwrap();
lu.done().unwrap();
lu.render("Second").unwrap();
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("First"));
assert!(output.contains("Second"));
}
#[test]
fn test_log_update_clear_then_render() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("First").unwrap();
lu.clear().unwrap();
lu.render("After clear").unwrap();
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("After clear"));
}
#[test]
fn test_log_update_multiline_with_styles() {
let mut buf = Vec::new();
{
let mut lu = LogUpdate::new(&mut buf);
lu.render("\x1b[31mRed\x1b[0m\n\x1b[32mGreen\x1b[0m")
.unwrap();
assert_eq!(lu.previous_line_count(), 2);
}
let output = String::from_utf8(buf).unwrap();
assert!(output.contains("Red"));
assert!(output.contains("Green"));
}
}