use crate::attr::Attr;
use crate::color::Color;
use crate::error::{Error, Result};
use smallvec::SmallVec;
use std::fmt::Write;
use std::io;
pub struct Surface {
height: u16,
width: u16,
begin_y: u16,
begin_x: u16,
cursor_x: u16,
cursor_y: u16,
current_attr: Attr,
current_fg: Color,
current_bg: Color,
buffer: String,
scroll_enabled: bool,
last_emitted_attr: Attr,
last_emitted_fg: Color,
last_emitted_bg: Color,
style_sequence_buf: SmallVec<[u8; 64]>,
}
impl Surface {
pub(crate) fn new(height: u16, width: u16, y: u16, x: u16) -> Result<Self> {
let estimated_capacity = (height as usize * width as usize * 10).min(65536);
Ok(Self {
height,
width,
begin_y: y,
begin_x: x,
cursor_x: 0,
cursor_y: 0,
current_attr: Attr::NORMAL,
current_fg: Color::RESET,
current_bg: Color::RESET,
buffer: String::with_capacity(estimated_capacity),
scroll_enabled: false,
last_emitted_attr: Attr::NORMAL,
last_emitted_fg: Color::RESET,
last_emitted_bg: Color::RESET,
style_sequence_buf: SmallVec::new(), })
}
pub fn get_size(&self) -> (u16, u16) {
(self.height, self.width)
}
pub fn get_position(&self) -> (u16, u16) {
(self.begin_y, self.begin_x)
}
pub fn move_cursor(&mut self, y: u16, x: u16) -> Result<()> {
if y >= self.height || x >= self.width {
return Err(Error::InvalidCoordinates { y, x });
}
let dy = (y as i32 - self.cursor_y as i32).abs();
let dx = (x as i32 - self.cursor_x as i32).abs();
let abs_y = self.begin_y + y;
let abs_x = self.begin_x + x;
if dy == 0 && dx > 0 && dx < 4 {
if x > self.cursor_x {
write!(self.buffer, "\x1b[{}C", dx)?; } else {
write!(self.buffer, "\x1b[{}D", dx)?; }
} else if dx == 0 && dy > 0 && dy < 4 {
if y > self.cursor_y {
write!(self.buffer, "\x1b[{}B", dy)?; } else {
write!(self.buffer, "\x1b[{}A", dy)?; }
} else {
write!(self.buffer, "\x1b[{};{}H", abs_y + 1, abs_x + 1)?; }
self.cursor_y = y;
self.cursor_x = x;
Ok(())
}
pub fn print(&mut self, text: &str) -> Result<()> {
let remaining = (self.width - self.cursor_x) as usize;
let text_to_print = if text.len() > remaining {
&text[..remaining]
} else {
text
};
if text_to_print.len() >= 8 && text_to_print.chars().all(|c| c == ' ') {
write!(self.buffer, "\x1b[{}X", text_to_print.len())?;
self.cursor_x += text_to_print.len() as u16;
return Ok(());
}
self.apply_style()?;
write!(self.buffer, "{}", text_to_print)?;
self.cursor_x += text_to_print.len() as u16;
Ok(())
}
pub fn move_print(&mut self, y: u16, x: u16, text: &str) -> Result<()> {
self.move_cursor(y, x)?;
self.print(text)
}
pub fn add_char(&mut self, ch: char) -> Result<()> {
if self.cursor_x >= self.width {
return Ok(());
}
self.apply_style()?;
write!(self.buffer, "{}", ch)?;
self.cursor_x += 1;
Ok(())
}
pub fn move_add_char(&mut self, y: u16, x: u16, ch: char) -> Result<()> {
self.move_cursor(y, x)?;
self.add_char(ch)
}
pub fn add_attribute(&mut self, attr: Attr) -> Result<()> {
self.current_attr = self.current_attr | attr;
Ok(())
}
pub fn remove_attribute(&mut self, attr: Attr) -> Result<()> {
self.current_attr = self.current_attr & !attr;
Ok(())
}
pub fn set_foreground(&mut self, color: Color) -> Result<()> {
self.current_fg = color;
Ok(())
}
pub fn set_background(&mut self, color: Color) -> Result<()> {
self.current_bg = color;
Ok(())
}
pub fn clear(&mut self) -> Result<()> {
self.move_cursor(0, 0)?;
for y in 0..self.height {
if y > 0 {
self.move_cursor(y, 0)?;
}
write!(self.buffer, "\x1b[K")?;
}
self.move_cursor(0, 0)?;
Ok(())
}
pub fn border(
&mut self,
ls: char,
rs: char,
ts: char,
bs: char,
tl: char,
tr: char,
bl: char,
br: char,
) -> Result<()> {
self.move_add_char(0, 0, tl)?;
for _ in 1..self.width - 1 {
self.add_char(ts)?;
}
self.add_char(tr)?;
for y in 1..self.height - 1 {
self.move_add_char(y, 0, ls)?;
self.move_add_char(y, self.width - 1, rs)?;
}
self.move_add_char(self.height - 1, 0, bl)?;
for _ in 1..self.width - 1 {
self.add_char(bs)?;
}
self.add_char(br)?;
Ok(())
}
pub fn draw_box(&mut self) -> Result<()> {
use crate::acs::*;
self.border(
ACS_VLINE.as_char(),
ACS_VLINE.as_char(),
ACS_HLINE.as_char(),
ACS_HLINE.as_char(),
ACS_ULCORNER.as_char(),
ACS_URCORNER.as_char(),
ACS_LLCORNER.as_char(),
ACS_LRCORNER.as_char(),
)
}
pub fn refresh(&mut self) -> Result<()> {
use std::io::Write as IoWrite;
io::stdout().write_all(self.buffer.as_bytes())?;
io::stdout().flush()?;
self.buffer.clear();
Ok(())
}
pub fn wnoutrefresh(&mut self) -> Result<()> {
use crate::backend::Backend;
Backend::add_to_update_buffer(&self.buffer)?;
self.buffer.clear();
Ok(())
}
pub fn scrollok(&mut self, enabled: bool) -> Result<()> {
self.scroll_enabled = enabled;
Ok(())
}
pub fn scroll(&mut self, lines: i16) -> Result<()> {
if !self.scroll_enabled {
return Ok(());
}
if lines > 0 {
for _ in 0..lines {
write!(
self.buffer,
"\x1b[{};{}r",
self.begin_y + 1,
self.begin_y + self.height
)?;
write!(self.buffer, "\x1b[{}H\n", self.begin_y + self.height)?;
write!(self.buffer, "\x1b[r")?;
}
} else if lines < 0 {
for _ in 0..(-lines) {
write!(
self.buffer,
"\x1b[{};{}r",
self.begin_y + 1,
self.begin_y + self.height
)?;
write!(self.buffer, "\x1b[{}H\x1bM", self.begin_y + 1)?;
write!(self.buffer, "\x1b[r")?;
}
}
Ok(())
}
fn apply_style(&mut self) -> Result<()> {
let style_changed = self.current_attr != self.last_emitted_attr
|| self.current_fg != self.last_emitted_fg
|| self.current_bg != self.last_emitted_bg;
if !style_changed {
return Ok(());
}
self.style_sequence_buf.clear();
let mut needs_separator = false;
if self.current_attr != self.last_emitted_attr {
if self.last_emitted_attr != Attr::NORMAL {
self.style_sequence_buf.push(b'0');
needs_separator = true;
}
if !self.current_attr.is_empty() {
for code in self.current_attr.to_ansi_codes() {
if needs_separator {
self.style_sequence_buf.push(b';');
}
self.style_sequence_buf.extend_from_slice(code.as_bytes());
needs_separator = true;
}
}
}
let mut color_buf = String::with_capacity(20);
if self.current_fg != self.last_emitted_fg {
if needs_separator {
self.style_sequence_buf.push(b';');
}
color_buf.clear();
self.current_fg.write_ansi_fg(&mut color_buf);
self.style_sequence_buf
.extend_from_slice(color_buf.as_bytes());
needs_separator = true;
}
if self.current_bg != self.last_emitted_bg {
if needs_separator {
self.style_sequence_buf.push(b';');
}
color_buf.clear();
self.current_bg.write_ansi_bg(&mut color_buf);
self.style_sequence_buf
.extend_from_slice(color_buf.as_bytes());
}
if !self.style_sequence_buf.is_empty() {
self.buffer.push_str("\x1b[");
self.buffer
.push_str(std::str::from_utf8(&self.style_sequence_buf).unwrap());
self.buffer.push('m');
}
self.last_emitted_attr = self.current_attr;
self.last_emitted_fg = self.current_fg;
self.last_emitted_bg = self.current_bg;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_surface_creation() {
let win = Surface::new(10, 20, 5, 5).unwrap();
assert_eq!(win.get_size(), (10, 20));
assert_eq!(win.get_position(), (5, 5));
}
#[test]
fn test_surface_cursor_movement() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.move_cursor(5, 10).unwrap();
assert_eq!(win.cursor_y, 5);
assert_eq!(win.cursor_x, 10);
}
#[test]
fn test_surface_invalid_cursor() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
let result = win.move_cursor(15, 10);
assert!(matches!(result, Err(Error::InvalidCoordinates { .. })));
}
#[test]
fn test_surface_print() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.print("Hello").unwrap();
assert_eq!(win.cursor_x, 5);
}
#[test]
fn test_surface_print_truncation() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.move_cursor(0, 15).unwrap();
win.print("HelloWorld").unwrap();
assert_eq!(win.cursor_x, 20);
}
#[test]
fn test_surface_attributes() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.add_attribute(Attr::BOLD).unwrap();
assert!(win.current_attr.contains(Attr::BOLD));
win.remove_attribute(Attr::BOLD).unwrap();
assert!(!win.current_attr.contains(Attr::BOLD));
}
#[test]
fn test_surface_colors() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.set_foreground(Color::RED).unwrap();
win.set_background(Color::BLUE).unwrap();
assert_eq!(win.current_fg, Color::RED);
assert_eq!(win.current_bg, Color::BLUE);
}
#[test]
fn test_surface_clear() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.cursor_x = 5;
win.cursor_y = 5;
win.clear().unwrap();
assert_eq!(win.cursor_x, 0);
assert_eq!(win.cursor_y, 0);
}
#[test]
fn test_surface_border_buffer() {
let mut win = Surface::new(5, 10, 0, 0).unwrap();
win.border('|', '|', '-', '-', '+', '+', '+', '+').unwrap();
assert!(!win.buffer.is_empty());
}
#[test]
fn test_scrollok() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
assert!(!win.scroll_enabled);
win.scrollok(true).unwrap();
assert!(win.scroll_enabled);
win.scrollok(false).unwrap();
assert!(!win.scroll_enabled);
}
#[test]
fn test_scroll_disabled() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
assert!(!win.scroll_enabled);
win.scroll(5).unwrap();
assert!(win.buffer.is_empty());
win.scroll(-3).unwrap();
assert!(win.buffer.is_empty());
}
#[test]
fn test_scroll_up() {
let mut win = Surface::new(10, 20, 5, 5).unwrap();
win.scrollok(true).unwrap();
win.scroll(1).unwrap();
assert!(!win.buffer.is_empty());
assert!(win.buffer.contains("\x1b[")); }
#[test]
fn test_scroll_down() {
let mut win = Surface::new(10, 20, 5, 5).unwrap();
win.scrollok(true).unwrap();
win.scroll(-2).unwrap();
assert!(!win.buffer.is_empty());
assert!(win.buffer.contains("\x1b[")); }
#[test]
fn test_scroll_zero() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.scrollok(true).unwrap();
win.scroll(0).unwrap();
assert!(win.buffer.is_empty());
}
#[test]
fn test_scroll_multiple_lines() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.scrollok(true).unwrap();
win.scroll(3).unwrap();
let output = win.buffer.clone();
assert!(!output.is_empty());
win.buffer.clear();
win.scroll(-4).unwrap();
assert!(!win.buffer.is_empty());
}
#[test]
fn test_surface_style_caching_no_redundant_codes() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.print("Hello").unwrap();
win.buffer.clear();
win.print("World").unwrap();
let second_output = win.buffer.clone();
assert!(!second_output.contains("\x1b["));
assert_eq!(second_output, "World");
}
#[test]
fn test_surface_style_caching_emits_on_change() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.print("Normal").unwrap();
win.buffer.clear();
win.add_attribute(Attr::BOLD).unwrap();
win.print("Bold").unwrap();
assert!(win.buffer.contains("\x1b[1m"));
}
#[test]
fn test_surface_style_caching_color_change() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.set_foreground(Color::RED).unwrap();
win.print("Red").unwrap();
win.buffer.clear();
win.print("AlsoRed").unwrap();
assert!(!win.buffer.contains("\x1b["));
win.buffer.clear();
win.set_foreground(Color::BLUE).unwrap();
win.print("Blue").unwrap();
assert!(win.buffer.contains("\x1b["));
}
#[test]
fn test_surface_style_caching_attr_reset() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.add_attribute(Attr::BOLD).unwrap();
win.print("Bold").unwrap();
win.buffer.clear();
win.remove_attribute(Attr::BOLD).unwrap();
win.print("Normal").unwrap();
assert!(win.buffer.contains("\x1b[0m"));
}
#[test]
fn test_surface_style_caching_multiple_attrs() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.add_attribute(Attr::BOLD | Attr::UNDERLINE).unwrap();
win.print("Styled").unwrap();
win.buffer.clear();
win.print("AlsoStyled").unwrap();
assert!(!win.buffer.contains("\x1b["));
assert_eq!(win.buffer, "AlsoStyled");
}
#[test]
fn test_surface_buffer_preallocation() {
let win = Surface::new(10, 20, 0, 0).unwrap();
assert!(win.buffer.capacity() > 0);
assert!(win.buffer.capacity() >= 2000);
}
#[test]
fn test_surface_buffer_capacity_capped() {
let win = Surface::new(1000, 1000, 0, 0).unwrap();
assert_eq!(win.buffer.capacity(), 65536);
}
#[test]
fn test_surface_buffer_no_reallocation_on_typical_use() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
let initial_capacity = win.buffer.capacity();
for i in 0..5 {
win.move_print(i, 0, "Test line").unwrap();
}
assert_eq!(win.buffer.capacity(), initial_capacity);
}
#[test]
fn test_surface_cursor_movement_short_horizontal() {
let mut win = Surface::new(10, 20, 5, 5).unwrap();
win.cursor_x = 5;
win.cursor_y = 3;
win.move_cursor(3, 7).unwrap();
assert!(win.buffer.contains("\x1b[2C")); assert_eq!(win.cursor_x, 7);
assert_eq!(win.cursor_y, 3);
}
#[test]
fn test_surface_cursor_movement_short_vertical() {
let mut win = Surface::new(10, 20, 5, 5).unwrap();
win.cursor_x = 5;
win.cursor_y = 3;
win.move_cursor(5, 5).unwrap();
assert!(win.buffer.contains("\x1b[2B")); assert_eq!(win.cursor_x, 5);
assert_eq!(win.cursor_y, 5);
}
#[test]
fn test_surface_cursor_movement_long_distance() {
let mut win = Surface::new(10, 20, 5, 5).unwrap();
win.cursor_x = 2;
win.cursor_y = 1;
win.move_cursor(1, 12).unwrap();
assert!(win.buffer.contains("\x1b[7;18H")); assert_eq!(win.cursor_x, 12);
assert_eq!(win.cursor_y, 1);
}
#[test]
fn test_surface_cursor_movement_diagonal() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.cursor_x = 5;
win.cursor_y = 3;
win.move_cursor(5, 8).unwrap();
assert!(win.buffer.contains("\x1b[6;9H")); assert_eq!(win.cursor_x, 8);
assert_eq!(win.cursor_y, 5);
}
#[test]
fn test_surface_rle_long_blank_run() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.print(" ").unwrap();
assert!(win.buffer.contains("\x1b[15X")); assert_eq!(win.cursor_x, 15);
}
#[test]
fn test_surface_rle_short_blank_run() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.print(" ").unwrap();
assert!(!win.buffer.contains("\x1b[")); assert_eq!(win.buffer, " ");
assert_eq!(win.cursor_x, 5);
}
#[test]
fn test_surface_rle_threshold_8_spaces() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.print(" ").unwrap();
assert!(win.buffer.contains("\x1b[8X"));
assert_eq!(win.cursor_x, 8);
}
#[test]
fn test_surface_rle_with_truncation() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.cursor_x = 15;
win.print(" ").unwrap();
assert!(!win.buffer.contains("\x1b[")); assert_eq!(win.cursor_x, 20);
}
#[test]
fn test_surface_rle_non_blank_text() {
let mut win = Surface::new(10, 20, 0, 0).unwrap();
win.print("Hello").unwrap();
assert!(!win.buffer.contains("\x1b[")); assert_eq!(win.buffer, "Hello");
assert_eq!(win.cursor_x, 5);
}
}