use std::{
fmt,
sync::{Arc, Mutex},
};
use super::emulator::Emulator;
use super::prediction::{OverlayCell, OverlayCursor, PredictionEngine};
pub struct Renderer {
displayed: vt100::Parser,
initialized: bool,
}
impl fmt::Debug for Renderer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Renderer")
.field("initialized", &self.initialized)
.finish_non_exhaustive()
}
}
impl Renderer {
#[must_use]
pub fn new(rows: u16, cols: u16) -> Self {
Self {
displayed: vt100::Parser::new(rows, cols, 0),
initialized: false,
}
}
pub fn set_size(&mut self, rows: u16, cols: u16) {
self.displayed.screen_mut().set_size(rows, cols);
self.initialized = false;
}
#[must_use]
pub fn render(
&mut self,
screen: &vt100::Screen,
overlays: &[OverlayCell],
cursor: Option<OverlayCursor>,
) -> Vec<u8> {
let new_alt = screen.alternate_screen();
let old_alt = self.displayed.screen().alternate_screen();
let alt_changed = new_alt != old_alt;
let (rows, cols) = screen.size();
let mut frame = vt100::Parser::new(rows, cols, 0);
frame.process(&screen.contents_formatted());
if !overlays.is_empty() {
let mut paint: Vec<u8> = Vec::with_capacity(overlays.len() * 12);
for cell in overlays {
write_to_vec(
&mut paint,
format_args!("\x1b[{};{}H", cell.row + 1, cell.col + 1),
);
if cell.flagged {
paint.extend_from_slice(b"\x1b[4m");
}
let mut char_buf = [0u8; 4];
paint.extend_from_slice(cell.ch.encode_utf8(&mut char_buf).as_bytes());
if cell.flagged {
paint.extend_from_slice(b"\x1b[24m");
}
}
frame.process(&paint);
}
let (cur_row, cur_col) = if let Some(oc) = cursor {
(oc.row, oc.col)
} else {
screen.cursor_position()
};
let mut mv: Vec<u8> = Vec::with_capacity(12);
write_to_vec(
&mut mv,
format_args!("\x1b[{};{}H", cur_row + 1, cur_col + 1),
);
frame.process(&mv);
let mut out: Vec<u8> = Vec::with_capacity(4096);
if new_alt && !old_alt {
out.extend_from_slice(b"\x1b[?1049h");
} else if !new_alt && old_alt {
out.extend_from_slice(b"\x1b[?1049l");
}
if self.initialized && !alt_changed {
out.extend_from_slice(&frame.screen().contents_diff(self.displayed.screen()));
} else {
out.extend_from_slice(&frame.screen().contents_formatted());
self.initialized = true;
}
self.displayed.process(&out);
out
}
pub fn invalidate(&mut self) {
self.initialized = false;
}
}
#[must_use]
pub fn paint_overlays_to_ansi(overlays: &[OverlayCell], cursor: Option<OverlayCursor>) -> Vec<u8> {
if overlays.is_empty() && cursor.is_none() {
return Vec::new();
}
let mut out: Vec<u8> = Vec::with_capacity(256);
if !overlays.is_empty() {
out.extend_from_slice(b"\x1b[s"); for cell in overlays {
write_to_vec(
&mut out,
format_args!("\x1b[{};{}H", cell.row + 1, cell.col + 1),
);
if cell.flagged {
out.extend_from_slice(b"\x1b[4m");
}
let mut char_buf = [0u8; 4];
let s = cell.ch.encode_utf8(&mut char_buf);
out.extend_from_slice(s.as_bytes());
if cell.flagged {
out.extend_from_slice(b"\x1b[24m");
}
}
out.extend_from_slice(b"\x1b[m"); out.extend_from_slice(b"\x1b[u"); }
if let Some(oc) = cursor {
write_to_vec(
&mut out,
format_args!("\x1b[{};{}H", oc.row + 1, oc.col + 1),
);
}
out
}
#[must_use]
pub fn render_server_update(
emulator: &Arc<Mutex<Emulator>>,
prediction: &Arc<Mutex<PredictionEngine>>,
renderer: &Arc<Mutex<Renderer>>,
with_predictions: bool,
) -> Vec<u8> {
let emu = emulator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let screen = emu.screen();
let (overlays, cursor) = if with_predictions {
let mut pred = prediction
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
pred.cull(screen);
pred.apply(screen)
} else {
(Vec::new(), None)
};
let mut rend = renderer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
rend.render(screen, &overlays, cursor)
}
#[must_use]
pub fn render_prediction_update(
emulator: &Arc<Mutex<Emulator>>,
prediction: &Arc<Mutex<PredictionEngine>>,
renderer: &Arc<Mutex<Renderer>>,
new_bytes: &[u8],
) -> Vec<u8> {
let emu = emulator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let screen = emu.screen();
let (overlays, cursor) = {
let mut pred = prediction
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for &byte in new_bytes {
pred.new_user_byte(byte, screen);
}
pred.apply(screen)
};
let mut rend = renderer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
rend.render(screen, &overlays, cursor)
}
fn write_to_vec(buf: &mut Vec<u8>, args: fmt::Arguments<'_>) {
use std::io::Write as _;
drop(buf.write_fmt(args));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_size_resets_renderer_and_updates_dimensions() {
let mut renderer = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
drop(renderer.render(parser.screen(), &[], None));
assert!(renderer.initialized);
renderer.set_size(30, 100);
assert!(!renderer.initialized);
}
#[test]
fn renderer_new_is_not_initialized() {
let r = Renderer::new(24, 80);
assert!(!r.initialized);
}
#[test]
fn renderer_first_render_sets_initialized() {
let mut r = Renderer::new(24, 80);
let parser = vt100::Parser::new(24, 80, 0);
let out = r.render(parser.screen(), &[], None);
assert!(r.initialized);
assert!(!out.is_empty());
}
#[test]
fn renderer_invalidate_clears_initialized() {
let mut r = Renderer::new(24, 80);
let parser = vt100::Parser::new(24, 80, 0);
drop(r.render(parser.screen(), &[], None));
assert!(r.initialized);
r.invalidate();
assert!(!r.initialized);
}
#[test]
fn paint_overlays_to_ansi_empty_returns_empty() {
let out = paint_overlays_to_ansi(&[], None);
assert!(out.is_empty());
}
#[test]
fn paint_overlays_to_ansi_with_cell_contains_escape_sequences() {
use super::super::prediction::OverlayCell;
let cells = vec![OverlayCell {
row: 0,
col: 0,
ch: 'a',
flagged: false,
}];
let out = paint_overlays_to_ansi(&cells, None);
let s = String::from_utf8_lossy(&out);
assert!(s.contains("\x1b[s"));
assert!(s.contains("\x1b[1;1H"));
assert!(s.contains('a'));
assert!(s.contains("\x1b[u"));
}
#[test]
fn render_with_content_produces_nonempty_output() {
let mut r = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
let out = r.render(parser.screen(), &[], None);
assert!(!out.is_empty());
let s = String::from_utf8_lossy(&out);
assert!(s.contains("hello"));
}
#[test]
fn render_second_call_with_no_change_produces_minimal_output() {
let mut r = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
let first = r.render(parser.screen(), &[], None);
assert!(!first.is_empty());
let second = r.render(parser.screen(), &[], None);
assert!(
second.len() < first.len(),
"second render with no changes should be smaller"
);
}
fn apply(term: &mut vt100::Parser, bytes: &[u8]) {
term.process(bytes);
}
#[test]
fn render_with_overlay_cell_paints_overlay_character() {
use super::super::prediction::OverlayCell;
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let parser = vt100::Parser::new(24, 80, 0);
let overlays = vec![OverlayCell {
row: 0,
col: 0,
ch: 'Z',
flagged: false,
}];
apply(&mut term, &r.render(parser.screen(), &overlays, None));
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("Z"),
"overlay character 'Z' must be painted at (0,0)"
);
}
#[test]
fn render_with_flagged_overlay_underlines_the_cell() {
use super::super::prediction::OverlayCell;
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let parser = vt100::Parser::new(24, 80, 0);
let overlays = vec![OverlayCell {
row: 2,
col: 5,
ch: 'F',
flagged: true,
}];
apply(&mut term, &r.render(parser.screen(), &overlays, None));
let cell = term.screen().cell(2, 5).expect("cell exists");
assert_eq!(cell.contents(), "F");
assert!(cell.underline(), "flagged overlay cell must be underlined");
}
#[test]
fn render_with_cursor_override_positions_cursor_at_override() {
use super::super::prediction::OverlayCursor;
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let parser = vt100::Parser::new(24, 80, 0);
let cursor_override = Some(OverlayCursor { row: 5, col: 10 });
apply(&mut term, &r.render(parser.screen(), &[], cursor_override));
assert_eq!(
term.screen().cursor_position(),
(5, 10),
"cursor override must place the cursor at (5,10)"
);
}
#[test]
fn render_culled_prediction_self_heals() {
use super::super::prediction::OverlayCell;
let mut server = vt100::Parser::new(24, 80, 0);
server.process(b"a");
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let overlay = vec![OverlayCell {
row: 0,
col: 0,
ch: 'X',
flagged: false,
}];
apply(&mut term, &r.render(server.screen(), &overlay, None));
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("X"),
"prediction must be visible in frame 1"
);
apply(&mut term, &r.render(server.screen(), &[], None));
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("a"),
"culled prediction must self-heal back to the real cell"
);
}
#[test]
fn render_no_sgr_bleed_after_flagged_overlay() {
use super::super::prediction::OverlayCell;
let mut server = vt100::Parser::new(24, 80, 0);
server.process(b"hi");
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let overlay = vec![OverlayCell {
row: 0,
col: 5,
ch: 'P',
flagged: true,
}];
apply(&mut term, &r.render(server.screen(), &overlay, None));
assert!(
!term.screen().cell(0, 0).expect("cell").underline(),
"plain server cell must not inherit the overlay's underline"
);
assert!(
term.screen().cell(0, 5).expect("cell").underline(),
"flagged overlay cell should be underlined"
);
}
#[test]
fn paint_overlays_to_ansi_with_cursor_positions_cursor() {
use super::super::prediction::OverlayCursor;
let out = paint_overlays_to_ansi(&[], Some(OverlayCursor { row: 3, col: 7 }));
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("\x1b[4;8H"),
"cursor overlay must produce ESC[4;8H: {s:?}"
);
}
#[test]
fn render_emits_alt_screen_enter_on_transition() {
let mut r = Renderer::new(24, 80);
let mut p1 = vt100::Parser::new(24, 80, 0);
p1.process(b"hello");
drop(r.render(p1.screen(), &[], None));
let mut p2 = vt100::Parser::new(24, 80, 0);
p2.process(b"\x1b[?1049h");
let out = r.render(p2.screen(), &[], None);
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("\x1b[?1049h"),
"alt-screen enter must appear in output when transitioning to alt-screen: {s:?}"
);
}
#[test]
fn render_emits_alt_screen_exit_on_transition() {
let mut r = Renderer::new(24, 80);
let mut p1 = vt100::Parser::new(24, 80, 0);
p1.process(b"\x1b[?1049h");
drop(r.render(p1.screen(), &[], None));
let mut p2 = vt100::Parser::new(24, 80, 0);
p2.process(b"\x1b[?1049h\x1b[?1049l");
let out = r.render(p2.screen(), &[], None);
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("\x1b[?1049l"),
"alt-screen exit must appear in output when transitioning to main screen: {s:?}"
);
}
#[test]
fn render_size_change_triggers_full_refresh() {
let mut r = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
let first = r.render(parser.screen(), &[], None);
assert!(!first.is_empty());
r.set_size(30, 100);
assert!(!r.initialized);
let mut parser2 = vt100::Parser::new(30, 100, 0);
parser2.process(b"world");
let after_resize = r.render(parser2.screen(), &[], None);
assert!(r.initialized);
assert!(!after_resize.is_empty());
}
use super::super::prediction::DisplayPreference;
type RenderTrio = (
Arc<Mutex<Emulator>>,
Arc<Mutex<PredictionEngine>>,
Arc<Mutex<Renderer>>,
);
fn render_trio() -> RenderTrio {
(
Arc::new(Mutex::new(Emulator::new(24, 80))),
Arc::new(Mutex::new(PredictionEngine::new(DisplayPreference::Always))),
Arc::new(Mutex::new(Renderer::new(24, 80))),
)
}
#[test]
fn render_server_update_paints_emulator_screen() {
let (emulator, prediction, renderer) = render_trio();
emulator.lock().unwrap().process(b"server text");
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("s"),
"server screen content must be rendered to the terminal"
);
}
#[test]
fn render_server_update_culls_stale_predictions() {
let (emulator, prediction, renderer) = render_trio();
emulator.lock().unwrap().process(b"a");
{
let emu = emulator.lock().unwrap();
let mut pred = prediction.lock().unwrap();
pred.new_user_byte(b'Z', emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("a"),
"a prediction the server screen contradicts must be culled, leaving the real cell"
);
}
#[test]
fn render_server_update_without_predictions_skips_overlays() {
let (emulator, prediction, renderer) = render_trio();
emulator.lock().unwrap().process(b"hello");
{
let emu = emulator.lock().unwrap();
let mut pred = prediction.lock().unwrap();
pred.new_user_byte(b'X', emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, false),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("h"),
"with_predictions=false must render only the raw server screen"
);
}
#[test]
fn render_prediction_update_paints_keystroke_once_confirmed() {
let (emulator, prediction, renderer) = render_trio();
{
let emu = emulator.lock().unwrap();
prediction.lock().unwrap().cull(emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_prediction_update(&emulator, &prediction, &renderer, b"k"),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some(""),
"a fresh (tentative) prediction must not be painted yet"
);
emulator.lock().unwrap().process(b"\x1b[1;2H");
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("k"),
"once the epoch is confirmed the predicted keystroke must be painted"
);
}
#[test]
fn render_prediction_update_does_not_cull_between_keystrokes() {
let (emulator, prediction, renderer) = render_trio();
{
let emu = emulator.lock().unwrap();
prediction.lock().unwrap().cull(emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_prediction_update(&emulator, &prediction, &renderer, b"a"),
);
apply(
&mut term,
&render_prediction_update(&emulator, &prediction, &renderer, b"b"),
);
emulator.lock().unwrap().process(b"\x1b[1;3H");
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("a"),
"first prediction must survive the second keystroke"
);
assert_eq!(
term.screen().cell(0, 1).map(vt100::Cell::contents),
Some("b"),
"second prediction must be appended alongside the first"
);
}
}