use std::io;
use super::sgr::{SgrState, emit_cup, emit_sgr_transition};
use super::{Cell, Rect};
pub trait Backend: io::Write {
fn size(&self) -> io::Result<Rect>;
fn clear(&mut self) -> io::Result<()>;
fn hide_cursor(&mut self) -> io::Result<()>;
fn show_cursor(&mut self) -> io::Result<()>;
fn set_cursor_position(&mut self, x: u16, y: u16) -> io::Result<()>;
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
fn reset_style_cache(&mut self);
}
#[derive(Debug, Clone, Default)]
pub(crate) struct BackendState {
pub sgr: SgrState,
pub cursor: Option<(u16, u16)>,
pub link: Option<String>,
}
pub(crate) fn draw_iter<'a, W, I>(
writer: &mut W,
state: &mut BackendState,
iter: I,
) -> io::Result<()>
where
W: io::Write,
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in iter {
let need_cup = match state.cursor {
Some((lx, ly)) => !(ly == y && lx == x),
None => true,
};
if need_cup {
emit_cup(writer, x, y)?;
}
let new_sgr = SgrState {
fg: cell.fg,
bg: cell.bg,
modifier: cell.modifier,
};
state.sgr = emit_sgr_transition(writer, state.sgr, new_sgr)?;
let new_link = cell.link();
let matches_current = match (&state.link, new_link) {
(Some(cur), Some(next)) => cur == next,
(None, None) => true,
_ => false,
};
if !matches_current {
if state.link.is_some() {
writer.write_all(b"\x1b]8;;\x1b\\")?;
}
if let Some(url) = new_link {
writer.write_all(b"\x1b]8;;")?;
writer.write_all(url.as_bytes())?;
writer.write_all(b"\x1b\\")?;
state.link = Some(url.to_string());
} else {
state.link = None;
}
}
let symbol = cell.symbol();
writer.write_all(symbol.as_bytes())?;
let advance = cell.cell_width().max(1);
let new_x = x.saturating_add(advance);
state.cursor = Some((new_x, y));
}
if state.link.is_some() {
writer.write_all(b"\x1b]8;;\x1b\\")?;
state.link = None;
}
Ok(())
}
pub struct TestBackend {
size: Rect,
buffer: Vec<u8>,
state: BackendState,
cursor_visible: bool,
}
impl TestBackend {
pub fn new(width: u16, height: u16) -> Self {
Self {
size: Rect::new(0, 0, width, height),
buffer: Vec::new(),
state: BackendState::default(),
cursor_visible: true,
}
}
pub fn bytes(&self) -> &[u8] {
&self.buffer
}
pub fn take_bytes(&mut self) -> Vec<u8> {
std::mem::take(&mut self.buffer)
}
pub fn clear_bytes(&mut self) {
self.buffer.clear();
}
pub fn cursor_visible(&self) -> bool {
self.cursor_visible
}
pub fn resize(&mut self, width: u16, height: u16) {
self.size = Rect::new(0, 0, width, height);
}
#[allow(dead_code)]
pub(crate) fn state(&self) -> &BackendState {
&self.state
}
}
impl io::Write for TestBackend {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Backend for TestBackend {
fn size(&self) -> io::Result<Rect> {
Ok(self.size)
}
fn clear(&mut self) -> io::Result<()> {
self.buffer.extend_from_slice(b"\x1b[2J\x1b[0m\x1b[1;1H");
self.state = BackendState::default();
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.buffer.extend_from_slice(b"\x1b[?25l");
self.cursor_visible = false;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.buffer.extend_from_slice(b"\x1b[?25h");
self.cursor_visible = true;
Ok(())
}
fn set_cursor_position(&mut self, x: u16, y: u16) -> io::Result<()> {
emit_cup(&mut self.buffer, x, y)?;
self.state.cursor = Some((x, y));
Ok(())
}
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
let Self { buffer, state, .. } = self;
draw_iter(buffer, state, content)
}
fn reset_style_cache(&mut self) {
self.state = BackendState::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{Buffer, Color, Modifier, Style};
fn paint<F: FnOnce(&mut Buffer)>(f: F) -> Buffer {
let mut b = Buffer::empty(Rect::new(0, 0, 10, 3));
f(&mut b);
b
}
#[test]
fn new_test_backend() {
let tb = TestBackend::new(80, 24);
assert_eq!(tb.size().unwrap(), Rect::new(0, 0, 80, 24));
assert!(tb.cursor_visible());
assert!(tb.bytes().is_empty());
}
#[test]
fn clear_emits_expected_bytes() {
let mut tb = TestBackend::new(10, 3);
tb.clear().unwrap();
assert_eq!(tb.bytes(), b"\x1b[2J\x1b[0m\x1b[1;1H");
}
#[test]
fn hide_show_cursor() {
let mut tb = TestBackend::new(10, 3);
tb.hide_cursor().unwrap();
assert_eq!(tb.bytes(), b"\x1b[?25l");
assert!(!tb.cursor_visible());
tb.clear_bytes();
tb.show_cursor().unwrap();
assert_eq!(tb.bytes(), b"\x1b[?25h");
assert!(tb.cursor_visible());
}
#[test]
fn set_cursor_position_emits_cup() {
let mut tb = TestBackend::new(10, 3);
tb.set_cursor_position(3, 1).unwrap();
assert_eq!(tb.bytes(), b"\x1b[2;4H"); }
#[test]
fn draw_empty_iter_emits_nothing() {
let mut tb = TestBackend::new(10, 3);
tb.draw(std::iter::empty()).unwrap();
assert!(tb.bytes().is_empty());
}
#[test]
fn draw_single_cell() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(2, 1, "A", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[2;3H\x1b[38;2;255;0;0mA");
}
#[test]
fn draw_adjacent_cells_skip_cup() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "abc", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[1;1H\x1b[38;2;255;0;0mabc");
}
#[test]
fn draw_non_adjacent_cells_emit_cup_each() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "A", Style::new());
next.set_string(5, 1, "B", Style::new());
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[1;1HA\x1b[2;6HB");
}
#[test]
fn draw_preserves_sgr_state_across_adjacent_cells() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "abc", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[1;1H\x1b[38;2;255;0;0mabc");
}
#[test]
fn draw_emits_sgr_on_change() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "A", Style::new().fg(Color::Rgb(255, 0, 0)));
next.set_string(1, 0, "B", Style::new().fg(Color::Rgb(0, 0, 255)));
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
assert_eq!(
tb.bytes(),
b"\x1b[1;1H\x1b[38;2;255;0;0mA\x1b[38;2;0;0;255mB",
);
}
#[test]
fn draw_style_cache_persists_across_draws() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "A", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
tb.clear_bytes();
let mut next2 = prev.clone();
next2.set_string(2, 0, "B", Style::new().fg(Color::Rgb(255, 0, 0)));
tb.draw(next2.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[1;3HB");
}
#[test]
fn reset_style_cache_forces_next_frame_to_reemit() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "A", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
tb.clear_bytes();
tb.reset_style_cache();
let mut next2 = prev.clone();
next2.set_string(0, 0, "B", Style::new().fg(Color::Rgb(255, 0, 0)));
tb.draw(next2.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[1;1H\x1b[38;2;255;0;0mB");
}
#[test]
fn draw_wide_glyph_advances_cursor_by_two() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "中X", Style::new());
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
let s = std::str::from_utf8(tb.bytes()).unwrap();
assert!(s.starts_with("\x1b[1;1H中X"), "got: {:?}", s);
}
#[test]
fn clear_resets_style_and_cursor_cache() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(0, 0, "A", Style::new().fg(Color::Rgb(255, 0, 0)));
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
tb.clear().unwrap();
tb.clear_bytes();
let mut next2 = prev.clone();
next2.set_string(0, 0, "B", Style::new().fg(Color::Rgb(255, 0, 0)));
tb.draw(next2.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[1;1H\x1b[38;2;255;0;0mB");
}
#[test]
fn modifier_composition() {
let prev = paint(|_| {});
let mut next = prev.clone();
next.set_string(
0,
0,
"X",
Style::new()
.fg(Color::Rgb(255, 255, 255))
.add_modifier(Modifier::BOLD),
);
let mut tb = TestBackend::new(10, 3);
tb.draw(next.diff_iter(&prev)).unwrap();
assert_eq!(tb.bytes(), b"\x1b[1;1H\x1b[1m\x1b[38;2;255;255;255mX");
}
}