use crate::cell::Cell;
use crate::surface::Surface;
use std::io::{self, Write};
struct Sgr {
bold: bool,
italic: bool,
underline: bool,
strikethrough: bool,
#[allow(dead_code)]
reversed: bool,
fg: Option<crate::cell::Color>,
bg: Option<crate::cell::Color>,
}
impl PartialEq for Sgr {
fn eq(&self, other: &Self) -> bool {
self.bold == other.bold
&& self.italic == other.italic
&& self.underline == other.underline
&& self.strikethrough == other.strikethrough
&& self.reversed == other.reversed
&& self.fg == other.fg
&& self.bg == other.bg
}
}
impl Sgr {
fn new() -> Self {
Self {
bold: false,
italic: false,
underline: false,
strikethrough: false,
reversed: false,
fg: None,
bg: None,
}
}
#[allow(dead_code)]
fn reset() -> Self {
Self {
bold: false,
italic: false,
underline: false,
strikethrough: false,
reversed: false,
fg: None,
bg: None,
}
}
fn to_sgr(&self) -> String {
use crate::cell::Color;
let mut codes = Vec::new();
codes.push(0);
if self.bold {
codes.push(1);
}
if self.italic {
codes.push(3);
}
if self.underline {
codes.push(4);
}
if self.strikethrough {
codes.push(9);
}
if let Some(fg) = &self.fg {
match fg {
Color::Default => codes.extend_from_slice(&[39]),
Color::Black => codes.push(30),
Color::Red => codes.push(31),
Color::Green => codes.push(32),
Color::Yellow => codes.push(33),
Color::Blue => codes.push(34),
Color::Magenta => codes.push(35),
Color::Cyan => codes.push(36),
Color::White => codes.push(37),
Color::Indexed(n) => codes.extend_from_slice(&[38, 5, (*n)]),
Color::Rgb(r, g, b) => {
codes.extend_from_slice(&[38, 2, (*r), (*g), (*b)])
}
}
}
if let Some(bg) = &self.bg {
match bg {
Color::Default => codes.extend_from_slice(&[49]),
Color::Black => codes.push(40),
Color::Red => codes.push(41),
Color::Green => codes.push(42),
Color::Yellow => codes.push(43),
Color::Blue => codes.push(44),
Color::Magenta => codes.push(45),
Color::Cyan => codes.push(46),
Color::White => codes.push(47),
Color::Indexed(n) => codes.extend_from_slice(&[48, 5, (*n)]),
Color::Rgb(r, g, b) => {
codes.extend_from_slice(&[48, 2, (*r), (*g), (*b)])
}
}
}
codes
.iter()
.map(|c| format!("{}", c))
.collect::<Vec<_>>()
.join(";")
}
}
pub struct Renderer {
current_sgr: Sgr,
buf: Vec<u8>,
cursor_position: Option<(u16, u16)>,
}
impl Renderer {
pub fn new() -> Self {
Self {
current_sgr: Sgr::new(),
buf: Vec::with_capacity(16384),
cursor_position: None,
}
}
pub fn reset(&mut self) {
self.current_sgr = Sgr::new();
self.buf.clear();
self.cursor_position = None;
}
pub fn set_cursor_position(&mut self, pos: Option<(u16, u16)>) {
self.cursor_position = pos;
}
#[allow(dead_code)]
pub fn cursor_position(&self) -> Option<(u16, u16)> {
self.cursor_position
}
#[allow(dead_code)]
#[inline]
fn buf_write(&mut self, bytes: &[u8]) {
self.buf.extend_from_slice(bytes);
}
#[allow(dead_code)]
#[inline]
fn write_str(&mut self, s: &str) {
self.buf.extend_from_slice(s.as_bytes());
}
pub fn flush(&mut self) -> io::Result<()> {
if !self.buf.is_empty() {
let mut stdout = io::stdout();
stdout.write_all(&self.buf)?;
if let Some((row, col)) = self.cursor_position.take() {
write!(stdout, "\x1b[{};{}H", row + 1, col + 1)?;
stdout.write_all(b"\x1b[?25h")?;
}
stdout.flush()?;
self.buf.clear();
}
Ok(())
}
pub fn begin_sync(&mut self) {
self.buf.extend_from_slice(b"\x1b[?2026h");
}
pub fn end_sync(&mut self) -> io::Result<()> {
self.buf.extend_from_slice(b"\x1b[?2026l");
self.flush()
}
fn move_cursor(&mut self, row: u16, col: u16) {
write!(self.buf, "\x1b[{};{}H", row + 1, col + 1).unwrap();
}
fn apply_sgr(&mut self, cell: &Cell) -> bool {
use crate::cell::Color;
let new_sgr = Sgr {
bold: cell.attrs.bold,
italic: cell.attrs.italic,
underline: cell.attrs.underline,
strikethrough: cell.attrs.strikethrough,
reversed: cell.attrs.reversed,
fg: Some(cell.fg),
bg: Some(cell.bg),
};
if new_sgr == self.current_sgr {
return false; }
let mut codes = Vec::new();
if new_sgr.bold != self.current_sgr.bold {
codes.push(if new_sgr.bold { 1 } else { 22 });
}
if new_sgr.italic != self.current_sgr.italic {
codes.push(if new_sgr.italic { 3 } else { 23 });
}
if new_sgr.underline != self.current_sgr.underline {
codes.push(if new_sgr.underline { 4 } else { 24 });
}
if new_sgr.strikethrough != self.current_sgr.strikethrough {
codes.push(if new_sgr.strikethrough { 9 } else { 29 });
}
if new_sgr.fg != self.current_sgr.fg {
match &new_sgr.fg {
Some(Color::Default) | None => codes.push(39),
Some(Color::Black) => codes.push(30),
Some(Color::Red) => codes.push(31),
Some(Color::Green) => codes.push(32),
Some(Color::Yellow) => codes.push(33),
Some(Color::Blue) => codes.push(34),
Some(Color::Magenta) => codes.push(35),
Some(Color::Cyan) => codes.push(36),
Some(Color::White) => codes.push(37),
Some(Color::Indexed(n)) => codes.extend_from_slice(&[38, 5, (*n)]),
Some(Color::Rgb(r, g, b)) => {
codes.extend_from_slice(&[38, 2, (*r), (*g), (*b)])
}
}
}
if new_sgr.bg != self.current_sgr.bg {
match &new_sgr.bg {
Some(Color::Default) | None => codes.push(49),
Some(Color::Black) => codes.push(40),
Some(Color::Red) => codes.push(41),
Some(Color::Green) => codes.push(42),
Some(Color::Yellow) => codes.push(43),
Some(Color::Blue) => codes.push(44),
Some(Color::Magenta) => codes.push(45),
Some(Color::Cyan) => codes.push(46),
Some(Color::White) => codes.push(47),
Some(Color::Indexed(n)) => codes.extend_from_slice(&[48, 5, (*n)]),
Some(Color::Rgb(r, g, b)) => {
codes.extend_from_slice(&[48, 2, (*r), (*g), (*b)])
}
}
}
self.current_sgr = new_sgr;
if codes.is_empty() {
return false;
}
self.buf.extend_from_slice(b"\x1b[");
let mut first = true;
for code in &codes {
if !first {
self.buf.push(b';');
}
first = false;
write!(self.buf, "{}", code).unwrap();
}
self.buf.push(b'm');
true
}
fn clear_to_eol(&mut self) {
self.buf.extend_from_slice(b"\x1b[K");
}
pub fn clear_screen(&mut self) {
self.buf.extend_from_slice(b"\x1b[2J");
}
pub fn render_full(&mut self, surface: &Surface, use_sync: bool) -> io::Result<()> {
if use_sync {
self.begin_sync();
}
for row in 0..surface.height() {
for col in 0..surface.width() {
if let Some(cell) = surface.get(row, col) {
self.render_cell(row, col, cell);
}
}
}
self.move_cursor(0, 0);
if use_sync {
self.end_sync()?;
}
Ok(())
}
pub fn render_dirty(
&mut self,
surface: &Surface,
first_dirty: u16,
last_dirty: u16,
) -> io::Result<()> {
for row in first_dirty..=last_dirty {
for col in 0..surface.width() {
if surface.is_dirty(row, col) {
if let Some(cell) = surface.get(row, col) {
self.render_cell(row, col, cell);
}
}
}
}
Ok(())
}
pub fn render_cell(&mut self, row: u16, col: u16, cell: &Cell) {
self.move_cursor(row, col);
self.apply_sgr(cell);
let mut tmp = [0u8; 4];
let s = cell.char.encode_utf8(&mut tmp);
self.buf.extend_from_slice(s.as_bytes());
}
#[allow(dead_code)]
fn render_cell_at(&mut self, row: u16, col: u16, cell: &Cell) {
self.move_cursor(row, col);
self.apply_sgr(cell);
let mut tmp = [0u8; 4];
let s = cell.char.encode_utf8(&mut tmp);
self.buf.extend_from_slice(s.as_bytes());
self.clear_to_eol();
}
pub fn render_changed_lines(
&mut self,
surface: &Surface,
first_dirty: u16,
last_dirty: u16,
) -> io::Result<()> {
for row in first_dirty..=last_dirty {
self.move_cursor(row, 0);
let mut any_dirty = false;
for col in 0..surface.width() {
if surface.is_dirty(row, col) {
any_dirty = true;
break;
}
}
if !any_dirty {
continue;
}
for col in 0..surface.width() {
if let Some(cell) = surface.get(row, col) {
self.apply_sgr(cell);
let mut tmp = [0u8; 4];
let s = cell.char.encode_utf8(&mut tmp);
self.buf.extend_from_slice(s.as_bytes());
}
}
self.clear_to_eol();
}
if let Some(_cell) = surface.get(first_dirty, 0) {
self.move_cursor(first_dirty, 0);
}
Ok(())
}
}
impl Default for Renderer {
fn default() -> Self {
Self::new()
}
}
pub trait RenderToSurface {
fn to_ansi(&self) -> String;
}
impl RenderToSurface for Cell {
fn to_ansi(&self) -> String {
let sgr = Sgr {
bold: self.attrs.bold,
italic: self.attrs.italic,
underline: self.attrs.underline,
strikethrough: self.attrs.strikethrough,
reversed: self.attrs.reversed,
fg: Some(self.fg),
bg: Some(self.bg),
};
format!("\x1b[{}m{}\x1b[0m", sgr.to_sgr(), self.char)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cell::{Attributes, Cell, Color};
use crate::surface::Surface;
#[test]
fn sgr_diff_no_changes() {
let mut renderer = Renderer::new();
let cell = Cell::new('A');
let changed = renderer.apply_sgr(&cell);
assert!(changed); let changed2 = renderer.apply_sgr(&cell);
assert!(!changed2); }
#[test]
fn sgr_diff_attribute_changes_only() {
let mut renderer = Renderer::new();
let cell1 = Cell::new('A');
renderer.apply_sgr(&cell1);
let cell2 = Cell::new('B').with_bold();
let changed = renderer.apply_sgr(&cell2);
assert!(changed);
let buf_str = String::from_utf8_lossy(&renderer.buf);
assert!(buf_str.contains("\x1b["));
assert!(buf_str.contains("1"));
}
#[test]
fn sgr_diff_full_changes() {
let mut renderer = Renderer::new();
let cell1 = Cell::new('A');
renderer.apply_sgr(&cell1);
let attrs = Attributes::new().with_bold().with_italic().with_underline();
let cell2 = Cell::new('Z')
.with_fg(Color::Red)
.with_bg(Color::Blue)
.with_attrs(attrs);
let changed = renderer.apply_sgr(&cell2);
assert!(changed);
let buf_str = String::from_utf8_lossy(&renderer.buf);
assert!(buf_str.contains("1"));
assert!(buf_str.contains("3"));
assert!(buf_str.contains("4"));
assert!(buf_str.contains("31"));
assert!(buf_str.contains("44"));
}
#[test]
fn render_to_surface_known_content() {
let cell = Cell::new('X').with_fg(Color::Green).with_bg(Color::Black);
let ansi = cell.to_ansi();
assert!(ansi.starts_with("\x1b["));
assert!(ansi.ends_with("\x1b[0m"));
assert!(ansi.contains('X'));
assert!(ansi.contains("32"));
assert!(ansi.contains("40"));
}
#[test]
fn render_to_surface_default_cell() {
let cell = Cell::new(' ');
let ansi = cell.to_ansi();
assert!(ansi.contains(' '));
}
#[test]
fn flush_clears_buffer() {
let mut renderer = Renderer::new();
renderer.buf.extend_from_slice(b"test data");
assert!(!renderer.buf.is_empty());
let _ = renderer.flush();
assert!(renderer.buf.is_empty());
}
#[test]
fn flush_empty_buffer_is_ok() {
let mut renderer = Renderer::new();
let result = renderer.flush();
assert!(result.is_ok());
}
#[test]
fn ime_cursor_positioning_set_get() {
let mut renderer = Renderer::new();
assert!(renderer.cursor_position().is_none());
renderer.set_cursor_position(Some((5, 10)));
assert_eq!(renderer.cursor_position(), Some((5, 10)));
renderer.set_cursor_position(None);
assert!(renderer.cursor_position().is_none());
}
#[test]
fn ime_cursor_position_consumed_on_flush() {
let mut renderer = Renderer::new();
renderer.set_cursor_position(Some((3, 7)));
renderer.buf.extend_from_slice(b"data");
let _ = renderer.flush();
assert!(renderer.cursor_position().is_none());
}
#[test]
fn render_full_produces_output() {
let mut renderer = Renderer::new();
let mut surface = Surface::new(3, 2);
surface.set(0, 0, Cell::new('H'));
surface.set(0, 1, Cell::new('i'));
surface.set(1, 0, Cell::new('!'));
renderer.render_full(&surface, false).unwrap();
let buf_str = String::from_utf8_lossy(&renderer.buf);
assert!(buf_str.contains('H'));
assert!(buf_str.contains('i'));
assert!(buf_str.contains('!'));
}
#[test]
fn render_full_with_sync_includes_sync_codes() {
let mut renderer = Renderer::new();
let _surface = Surface::new(2, 1);
renderer.begin_sync();
assert!(renderer.buf.starts_with(b"\x1b[?2026h"));
renderer.buf.clear();
renderer.buf.extend_from_slice(b"\x1b[?2026l");
assert!(renderer.buf.ends_with(b"\x1b[?2026l"));
}
#[test]
fn render_dirty_only_touches_dirty_cells() {
let mut renderer = Renderer::new();
let mut surface = Surface::new(5, 2);
surface.set(0, 0, Cell::new('A'));
surface.set(0, 1, Cell::new('B'));
renderer.render_dirty(&surface, 0, 1).unwrap();
let buf_str = String::from_utf8_lossy(&renderer.buf);
assert!(buf_str.contains('A'));
assert!(buf_str.contains('B'));
}
#[test]
fn render_changed_lines_skips_clean_rows() {
let mut renderer = Renderer::new();
let mut surface = Surface::new(5, 3);
surface.set(1, 0, Cell::new('X'));
surface.clear_dirty();
surface.set(1, 2, Cell::new('Y'));
renderer
.render_changed_lines(&surface, 1, 1)
.unwrap();
let buf_str = String::from_utf8_lossy(&renderer.buf);
assert!(buf_str.contains('Y'));
}
#[test]
fn clear_screen_writes_escape() {
let mut renderer = Renderer::new();
renderer.clear_screen();
assert_eq!(&renderer.buf, b"\x1b[2J");
}
#[test]
fn reset_clears_state() {
let mut renderer = Renderer::new();
renderer.buf.extend_from_slice(b"data");
renderer.set_cursor_position(Some((1, 2)));
renderer.reset();
assert!(renderer.buf.is_empty());
assert!(renderer.cursor_position().is_none());
}
}