use std::io;
use super::backend::Backend;
use super::{Buffer, Rect};
pub struct Terminal<B: Backend> {
backend: B,
front: Buffer,
back: Buffer,
force_full_redraw: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct CompletedFrame {
pub area: Rect,
pub cells_emitted: usize,
pub was_full_redraw: bool,
}
impl<B: Backend> Terminal<B> {
pub fn new(backend: B) -> io::Result<Self> {
let size = backend.size()?;
Ok(Self {
backend,
front: Buffer::empty(size),
back: Buffer::empty(size),
force_full_redraw: true,
})
}
pub fn backend(&self) -> &B {
&self.backend
}
pub fn backend_mut(&mut self) -> &mut B {
&mut self.backend
}
pub fn size(&self) -> Rect {
self.back.area
}
pub fn queue_full_redraw(&mut self) {
self.force_full_redraw = true;
}
pub fn clear(&mut self) -> io::Result<()> {
self.backend.clear()?;
self.front.clear();
self.back.clear();
self.force_full_redraw = true;
Ok(())
}
pub fn autoresize(&mut self) -> io::Result<()> {
let actual = self.backend.size()?;
if actual != self.back.area {
self.back.resize(actual);
self.front.resize(actual);
self.force_full_redraw = true;
self.backend.clear()?;
}
Ok(())
}
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
where
F: FnOnce(&mut Buffer) -> io::Result<()>,
{
self.autoresize()?;
self.back.clear();
f(&mut self.back)?;
let mut cells_emitted = 0usize;
let was_full_redraw = self.force_full_redraw;
if self.force_full_redraw {
let blank = Buffer::empty(self.back.area);
for (x, y, cell) in self.back.diff_iter(&blank) {
cells_emitted += 1;
self.backend.draw(std::iter::once((x, y, cell)))?;
}
self.force_full_redraw = false;
} else {
let count = self.back.diff_iter(&self.front).count();
cells_emitted = count;
self.backend.draw(self.back.diff_iter(&self.front))?;
}
self.backend.flush()?;
std::mem::swap(&mut self.front, &mut self.back);
Ok(CompletedFrame {
area: self.front.area,
cells_emitted,
was_full_redraw,
})
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()
}
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
self.backend.set_cursor_position(x, y)
}
pub fn into_backend(self) -> B {
self.backend
}
}
pub struct TerminalGuard {
active: bool,
}
impl TerminalGuard {
pub fn new() -> Self {
Self { active: true }
}
pub fn disarm(&mut self) {
self.active = false;
}
}
impl Default for TerminalGuard {
fn default() -> Self {
Self::new()
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
if self.active {
let _ = super::backend_crossterm::leave_tui_mode(&mut io::stdout());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::backend::TestBackend;
use crate::render::{Color, Style};
#[test]
fn construct_initial_full_redraw() {
let tb = TestBackend::new(10, 3);
let term = Terminal::new(tb).unwrap();
assert_eq!(term.size(), Rect::new(0, 0, 10, 3));
}
#[test]
fn first_draw_is_full_redraw() {
let tb = TestBackend::new(10, 3);
let mut term = Terminal::new(tb).unwrap();
let frame = term
.draw(|buf| {
buf.set_string(0, 0, "hi", Style::new().fg(Color::Rgb(255, 0, 0)));
Ok(())
})
.unwrap();
assert!(frame.was_full_redraw);
assert_eq!(frame.cells_emitted, 2); }
#[test]
fn second_draw_is_incremental() {
let tb = TestBackend::new(10, 3);
let mut term = Terminal::new(tb).unwrap();
term.draw(|buf| {
buf.set_string(0, 0, "hi", Style::new());
Ok(())
})
.unwrap();
let f2 = term
.draw(|buf| {
buf.set_string(0, 0, "hi", Style::new());
Ok(())
})
.unwrap();
assert!(!f2.was_full_redraw);
assert_eq!(f2.cells_emitted, 0); }
#[test]
fn changed_cells_are_emitted() {
let tb = TestBackend::new(10, 3);
let mut term = Terminal::new(tb).unwrap();
term.draw(|buf| {
buf.set_string(0, 0, "hello", Style::new());
Ok(())
})
.unwrap();
let frame = term
.draw(|buf| {
buf.set_string(0, 0, "hELLo", Style::new());
Ok(())
})
.unwrap();
assert_eq!(frame.cells_emitted, 3); }
#[test]
fn draw_does_not_emit_synchronized_output_sequences() {
let tb = TestBackend::new(5, 1);
let mut term = Terminal::new(tb).unwrap();
term.draw(|buf| {
buf.set_string(0, 0, "X", Style::new());
Ok(())
})
.unwrap();
let bytes = term.backend().bytes();
assert!(
!bytes.windows(8).any(|w| w == b"\x1b[?2026h"),
"BSU (?2026h) must NOT appear in draw output — see iTerm2 \
motion-tracking interaction documented in `terminal.rs::draw`"
);
assert!(
!bytes.windows(8).any(|w| w == b"\x1b[?2026l"),
"ESU (?2026l) must NOT appear in draw output"
);
}
#[test]
fn autoresize_resizes_both_buffers() {
let tb = TestBackend::new(5, 3);
let mut term = Terminal::new(tb).unwrap();
term.draw(|buf| {
buf.set_string(0, 0, "ab", Style::new());
Ok(())
})
.unwrap();
term.backend_mut().resize(10, 3);
assert_eq!(term.backend().size().unwrap(), Rect::new(0, 0, 10, 3));
let frame = term
.draw(|buf| {
buf.set_string(0, 0, "ab", Style::new());
Ok(())
})
.unwrap();
assert!(frame.was_full_redraw);
assert_eq!(term.size(), Rect::new(0, 0, 10, 3));
}
#[test]
fn clear_forces_full_redraw_next_frame() {
let tb = TestBackend::new(5, 2);
let mut term = Terminal::new(tb).unwrap();
term.draw(|buf| {
buf.set_string(0, 0, "X", Style::new());
Ok(())
})
.unwrap();
term.clear().unwrap();
let frame = term
.draw(|buf| {
buf.set_string(0, 0, "X", Style::new());
Ok(())
})
.unwrap();
assert!(frame.was_full_redraw);
}
#[test]
fn hide_show_cursor_passes_through() {
let tb = TestBackend::new(5, 2);
let mut term = Terminal::new(tb).unwrap();
term.hide_cursor().unwrap();
assert!(term.backend().bytes().contains(&b'l'));
term.show_cursor().unwrap();
assert!(term.backend().bytes().contains(&b'h'));
}
#[test]
fn end_to_end_dom_to_ansi() {
use crate::prelude::*;
let mut dom = TuiDom::new();
let root = dom.root();
let span = dom.create_element("span");
let t = dom.create_text_node("hi");
dom.append_child(span, t).unwrap();
dom.append_child(root, span).unwrap();
let sheet =
Stylesheet::bare().rule_unchecked("span", TuiStyle::new().fg(Color::Rgb(255, 0, 0)));
dom.cascade(&sheet);
let tb = TestBackend::new(10, 1);
let mut term = Terminal::new(tb).unwrap();
let viewport = term.size();
term.draw(|buf| {
dom.layout_dom(viewport);
dom.paint_dom(buf, viewport);
Ok(())
})
.unwrap();
let bytes = term.backend().bytes();
let s = std::str::from_utf8(bytes).unwrap();
assert!(s.contains("hi"), "got: {:?}", s);
assert!(
s.contains("\x1b[38;2;255;0;0m"),
"expected Red fg truecolor SGR in: {:?}",
s,
);
}
#[test]
fn unchanged_frames_emit_minimal_bytes() {
let tb = TestBackend::new(10, 1);
let mut term = Terminal::new(tb).unwrap();
term.draw(|buf| {
buf.set_string(0, 0, "stable", Style::new().fg(Color::Rgb(255, 0, 0)));
Ok(())
})
.unwrap();
term.backend_mut().take_bytes();
for _ in 0..5 {
term.draw(|buf| {
buf.set_string(0, 0, "stable", Style::new().fg(Color::Rgb(255, 0, 0)));
Ok(())
})
.unwrap();
}
let bytes = term.backend().bytes();
#[cfg(not(feature = "no-synchronized-output"))]
assert!(
bytes.len() <= 100,
"expected tiny steady-state emit, got {} bytes",
bytes.len()
);
}
}