use crate::terminal::writer::DrawTarget;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct RenderState {
pub last_line_len: usize,
pub lines_above: usize,
}
#[derive(Debug)]
pub struct Renderer {
pub(crate) target: DrawTarget,
pub(crate) render_state: RenderState,
}
impl Renderer {
pub fn new(target: DrawTarget) -> Renderer {
Renderer {
target,
render_state: RenderState::default(),
}
}
pub fn draw(&mut self, line: &str) {
if self.target.is_hidden() {
return;
}
let line_count = line.lines().count().max(1);
if self.target.is_tty() {
let mut out = String::new();
if self.render_state.lines_above > 1 {
out.push_str(&format!("\x1b[{}A", self.render_state.lines_above - 1));
}
out.push('\r');
for (idx, rendered_line) in line.lines().enumerate() {
if idx > 0 {
out.push('\n');
}
out.push_str("\x1b[K");
out.push_str(rendered_line);
}
if line.is_empty() {
out.push_str("\x1b[K");
}
self.target.write_str(&out);
} else {
self.target.write_str(line);
self.target.write_str("\n");
}
self.target.flush();
self.render_state.last_line_len = line.lines().last().map(str::len).unwrap_or(0);
self.render_state.lines_above = line_count;
}
pub fn clear(&mut self) {
if self.target.is_hidden() {
return;
}
if self.target.is_tty() {
let mut out = String::new();
if self.render_state.lines_above > 1 {
out.push_str(&format!("\x1b[{}A", self.render_state.lines_above - 1));
}
for idx in 0..self.render_state.lines_above.max(1) {
if idx > 0 {
out.push('\n');
}
out.push_str("\r\x1b[K");
}
self.target.write_str(&out);
}
self.target.flush();
self.render_state = RenderState::default();
}
pub fn println(&mut self, msg: &str) {
if self.target.is_hidden() {
return;
}
if self.target.is_tty() {
self.target.write_str(&format!("\r\x1b[K{msg}\n"));
} else {
self.target.write_str(msg);
self.target.write_str("\n");
}
self.target.flush();
}
pub fn finish(&mut self, line: &str) {
if self.target.is_hidden() {
return;
}
if self.target.is_tty() {
self.draw(line);
self.target.write_str("\n");
} else {
self.target.write_str(line);
self.target.write_str("\n");
}
self.target.flush();
self.render_state = RenderState::default();
}
pub(crate) fn set_target(&mut self, target: DrawTarget) {
self.target = target;
self.render_state = RenderState::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
impl SharedBuffer {
fn contents(&self) -> String {
let bytes = match self.0.lock() {
Ok(inner) => inner.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
};
String::from_utf8_lossy(&bytes).into_owned()
}
}
impl Write for SharedBuffer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.0.lock() {
Ok(mut inner) => inner.write(buf),
Err(poisoned) => poisoned.into_inner().write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_renderer_draw_hidden_target_no_output() {
let mut renderer = Renderer::new(DrawTarget::hidden());
renderer.draw("hello");
assert_eq!(renderer.render_state.last_line_len, 0);
}
#[test]
fn test_renderer_draw_produces_output() {
let buffer = SharedBuffer::default();
let mirror = buffer.clone();
let mut renderer = Renderer::new(DrawTarget::to_writer(buffer));
renderer.draw("hello");
assert!(mirror.contents().contains("hello"));
}
#[test]
fn test_renderer_clear() {
let buffer = SharedBuffer::default();
let mut renderer = Renderer::new(DrawTarget::to_writer(buffer));
renderer.draw("hello");
renderer.clear();
assert_eq!(renderer.render_state.last_line_len, 0);
}
#[test]
fn test_println_above_bar() {
let buffer = SharedBuffer::default();
let mirror = buffer.clone();
let mut renderer = Renderer::new(DrawTarget::to_writer(buffer));
renderer.println("message");
assert!(mirror.contents().contains("message"));
}
}