mod core;
mod helper;
mod render;
mod types;
pub use helper::stdout_terminal;
pub use types::Terminal;
#[cfg(test)]
mod tests {
use crate::layout::Rect;
use crate::render::cell::{Cell, Modifier};
use crate::render::Buffer;
use crate::style::Color;
use crossterm::style::Color as CrosstermColor;
use std::io::{self, Write};
fn to_crossterm_color(color: Color) -> CrosstermColor {
let Color { r, g, b, a: _ } = color;
CrosstermColor::Rgb { r, g, b }
}
struct MockWriter {
buffer: Vec<u8>,
}
impl MockWriter {
fn new() -> Self {
Self { buffer: Vec::new() }
}
fn contents(&self) -> &[u8] {
&self.buffer
}
fn as_string(&self) -> String {
String::from_utf8_lossy(&self.buffer).to_string()
}
}
impl Write for MockWriter {
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(())
}
}
#[derive(Default)]
struct RenderState {
fg: Option<Color>,
bg: Option<Color>,
modifier: Modifier,
hyperlink_id: Option<u16>,
cursor: Option<(u16, u16)>,
}
#[test]
fn test_render_state_default() {
let state = RenderState::default();
assert!(state.fg.is_none());
assert!(state.bg.is_none());
assert!(state.modifier.is_empty());
assert!(state.hyperlink_id.is_none());
}
#[test]
fn test_to_crossterm_color() {
let color = Color::rgb(255, 128, 64);
let ct_color = to_crossterm_color(color);
match ct_color {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 255);
assert_eq!(g, 128);
assert_eq!(b, 64);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_constants_conversion() {
let red = to_crossterm_color(Color::RED);
match red {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 0);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_green_conversion() {
let green = to_crossterm_color(Color::GREEN);
match green {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 0);
assert_eq!(g, 255);
assert_eq!(b, 0);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_blue_conversion() {
let blue = to_crossterm_color(Color::BLUE);
match blue {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 0);
assert_eq!(g, 0);
assert_eq!(b, 255);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_white_conversion() {
let white = to_crossterm_color(Color::WHITE);
match white {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 255);
assert_eq!(g, 255);
assert_eq!(b, 255);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_black_conversion() {
let black = to_crossterm_color(Color::BLACK);
match black {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 0);
assert_eq!(g, 0);
assert_eq!(b, 0);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_cyan_conversion() {
let cyan = to_crossterm_color(Color::CYAN);
match cyan {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 0);
assert_eq!(g, 255);
assert_eq!(b, 255);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_magenta_conversion() {
let magenta = to_crossterm_color(Color::MAGENTA);
match magenta {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 255);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_yellow_conversion() {
let yellow = to_crossterm_color(Color::YELLOW);
match yellow {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 255);
assert_eq!(g, 255);
assert_eq!(b, 0);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_color_gray_conversion() {
let gray = to_crossterm_color(Color::rgb(128, 128, 128));
match gray {
CrosstermColor::Rgb { r, g, b } => {
assert_eq!(r, 128);
assert_eq!(g, 128);
assert_eq!(b, 128);
}
_ => panic!("Expected RGB color"),
}
}
#[test]
fn test_hyperlink_start_escape() {
let mut writer = MockWriter::new();
let url = "https://example.com";
write!(writer, "\x1b]8;;{}\x1b\\", url).unwrap();
let output = writer.as_string();
assert!(output.contains("8;;"));
assert!(output.contains("https://example.com"));
}
#[test]
fn test_hyperlink_end_escape() {
let mut writer = MockWriter::new();
write!(writer, "\x1b]8;;\x1b\\").unwrap();
let output = writer.as_string();
assert!(output.contains("8;;"));
}
#[test]
fn test_mock_writer_write() {
let mut writer = MockWriter::new();
let bytes_written = writer.write(b"hello").unwrap();
assert_eq!(bytes_written, 5);
assert_eq!(writer.contents(), b"hello");
}
#[test]
fn test_mock_writer_multiple_writes() {
let mut writer = MockWriter::new();
writer.write(b"hello").unwrap();
writer.write(b" ").unwrap();
writer.write(b"world").unwrap();
assert_eq!(writer.as_string(), "hello world");
}
#[test]
fn test_mock_writer_flush() {
let mut writer = MockWriter::new();
assert!(writer.flush().is_ok());
}
#[test]
fn test_modifier_empty() {
let modifier = Modifier::empty();
assert!(modifier.is_empty());
assert!(!modifier.contains(Modifier::BOLD));
assert!(!modifier.contains(Modifier::ITALIC));
}
#[test]
fn test_modifier_bold() {
let modifier = Modifier::BOLD;
assert!(!modifier.is_empty());
assert!(modifier.contains(Modifier::BOLD));
}
#[test]
fn test_modifier_combined() {
let modifier = Modifier::BOLD | Modifier::ITALIC;
assert!(modifier.contains(Modifier::BOLD));
assert!(modifier.contains(Modifier::ITALIC));
assert!(!modifier.contains(Modifier::UNDERLINE));
}
#[test]
fn test_render_state_cursor_default() {
let state = RenderState::default();
assert!(state.cursor.is_none());
}
#[test]
fn test_render_state_cursor_tracking() {
let mut state = RenderState::default();
assert!(state.cursor.is_none());
state.cursor = Some((5, 10));
assert_eq!(state.cursor, Some((5, 10)));
state.cursor = Some((6, 10));
assert_eq!(state.cursor, Some((6, 10)));
}
#[test]
fn test_cursor_position_after_normal_char() {
let ch = 'A';
let width = crate::utils::unicode::char_width(ch) as u16;
assert_eq!(width, 1);
let x: u16 = 5;
let new_x = x.saturating_add(width);
assert_eq!(new_x, 6);
}
#[test]
fn test_cursor_position_after_wide_char() {
let ch = '한'; let width = crate::utils::unicode::char_width(ch) as u16;
assert_eq!(width, 2);
let x: u16 = 5;
let new_x = x.saturating_add(width);
assert_eq!(new_x, 7);
}
#[test]
fn test_cursor_position_saturating_add() {
let x: u16 = u16::MAX - 1;
let width: u16 = 2;
let new_x = x.saturating_add(width);
assert_eq!(new_x, u16::MAX);
}
#[test]
fn test_cursor_skip_moveto_same_position() {
let state = RenderState {
cursor: Some((5, 10)),
..RenderState::default()
};
let target = (5u16, 10u16);
let should_skip = state.cursor == Some(target);
assert!(should_skip);
}
#[test]
fn test_cursor_emit_moveto_different_position() {
let state = RenderState {
cursor: Some((5, 10)),
..RenderState::default()
};
let target = (6u16, 10u16);
let should_skip = state.cursor == Some(target);
assert!(!should_skip);
let target_diff_row = (5u16, 11u16);
let should_skip_row = state.cursor == Some(target_diff_row);
assert!(!should_skip_row);
}
#[test]
fn test_cursor_emit_moveto_when_none() {
let state = RenderState::default();
let target = (5u16, 10u16);
let should_skip = state.cursor == Some(target);
assert!(!should_skip);
}
#[test]
fn test_contiguous_cells_cursor_tracking() {
let mut state = RenderState::default();
assert!(state.cursor != Some((0, 0)));
state.cursor = Some((1, 0));
assert!(state.cursor == Some((1, 0)));
state.cursor = Some((2, 0));
assert!(state.cursor == Some((2, 0)));
}
#[test]
fn test_render_dirty_only_diffs_dirty_regions() {
let buf1 = Buffer::new(20, 20);
let mut buf2 = Buffer::new(20, 20);
buf2.set(5, 5, Cell::new('X')); buf2.set(15, 15, Cell::new('Y'));
let dirty_rects = vec![Rect::new(0, 0, 10, 10)];
let changes = crate::render::diff::diff(&buf1, &buf2, &dirty_rects);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].x, 5);
assert_eq!(changes[0].y, 5);
}
#[test]
fn test_render_dirty_multiple_regions() {
let buf1 = Buffer::new(20, 20);
let mut buf2 = Buffer::new(20, 20);
buf2.set(2, 2, Cell::new('X'));
buf2.set(15, 15, Cell::new('Y'));
let dirty_rects = vec![
Rect::new(0, 0, 5, 5), Rect::new(14, 14, 5, 5), ];
let changes = crate::render::diff::diff(&buf1, &buf2, &dirty_rects);
assert_eq!(changes.len(), 2);
}
#[test]
fn test_render_dirty_no_changes_in_dirty_region() {
let buf1 = Buffer::new(20, 20);
let mut buf2 = Buffer::new(20, 20);
buf2.set(15, 15, Cell::new('Z'));
let dirty_rects = vec![Rect::new(0, 0, 10, 10)];
let changes = crate::render::diff::diff(&buf1, &buf2, &dirty_rects);
assert!(changes.is_empty());
}
#[test]
fn test_render_dirty_empty_dirty_rects_fallback() {
let buf1 = Buffer::new(10, 10);
let mut buf2 = Buffer::new(10, 10);
buf2.set(5, 5, Cell::new('X'));
let changes = crate::render::diff::diff(&buf1, &buf2, &[]);
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].x, 5);
assert_eq!(changes[0].y, 5);
}
#[test]
fn test_render_dirty_overlapping_regions() {
let buf1 = Buffer::new(20, 20);
let mut buf2 = Buffer::new(20, 20);
buf2.set(5, 5, Cell::new('X'));
let dirty_rects = vec![Rect::new(0, 0, 10, 10), Rect::new(3, 3, 10, 10)];
let changes = crate::render::diff::diff(&buf1, &buf2, &dirty_rects);
assert_eq!(changes.len(), 1);
}
}