use super::{
image::{
Image,
printer::{PrintImage, PrintImageError, PrintOptions},
protocols::ascii::AsciiPrinter,
},
printer::{TerminalError, TerminalIo},
};
use crate::{
WindowSize,
markdown::{
elements::Text,
text_style::{Color, Colors, TextStyle},
},
terminal::printer::TerminalCommand,
};
use core::fmt;
use std::{collections::HashMap, io};
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct PrintedImage {
pub(crate) image: Image,
pub(crate) width_columns: u16,
}
pub(crate) struct TerminalRowIterator<'a> {
row: &'a [StyledChar],
}
impl<'a> TerminalRowIterator<'a> {
pub(crate) fn new(row: &'a [StyledChar]) -> Self {
Self { row }
}
}
impl Iterator for TerminalRowIterator<'_> {
type Item = Text;
fn next(&mut self) -> Option<Self::Item> {
let style = self.row.first()?.style;
let mut output = String::new();
while let Some(c) = self.row.first() {
if c.style != style {
break;
}
output.push(c.character);
self.row = &self.row[1..];
}
Some(Text::new(output, style))
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TerminalGrid {
pub(crate) rows: Vec<Vec<StyledChar>>,
pub(crate) background_color: Option<Color>,
pub(crate) images: HashMap<(u16, u16), PrintedImage>,
}
pub(crate) struct VirtualTerminal {
row: u16,
column: u16,
colors: Colors,
rows: Vec<Vec<StyledChar>>,
background_color: Option<Color>,
images: HashMap<(u16, u16), PrintedImage>,
row_heights: Vec<u16>,
image_behavior: ImageBehavior,
}
impl VirtualTerminal {
pub(crate) fn new(dimensions: WindowSize, image_behavior: ImageBehavior) -> Self {
let rows = vec![vec![StyledChar::default(); dimensions.columns as usize]; dimensions.rows as usize];
let row_heights = vec![1; dimensions.rows as usize];
Self {
row: 0,
column: 0,
colors: Default::default(),
rows,
background_color: None,
images: Default::default(),
row_heights,
image_behavior,
}
}
pub(crate) fn into_contents(self) -> TerminalGrid {
TerminalGrid { rows: self.rows, background_color: self.background_color, images: self.images }
}
fn current_cell_mut(&mut self) -> Option<&mut StyledChar> {
self.rows.get_mut(self.row as usize).and_then(|row| row.get_mut(self.column as usize))
}
fn set_current_row_height(&mut self, height: u16) {
if let Some(current) = self.row_heights.get_mut(self.row as usize) {
*current = height;
}
}
fn current_row_height(&self) -> u16 {
*self.row_heights.get(self.row as usize).unwrap_or(&1)
}
fn move_to(&mut self, column: u16, row: u16) -> io::Result<()> {
self.column = column;
self.row = row;
Ok(())
}
fn move_to_row(&mut self, row: u16) -> io::Result<()> {
self.row = row;
self.set_current_row_height(1);
Ok(())
}
fn move_to_column(&mut self, column: u16) -> io::Result<()> {
self.column = column;
Ok(())
}
fn move_down(&mut self, amount: u16) -> io::Result<()> {
self.row += amount;
Ok(())
}
fn move_right(&mut self, amount: u16) -> io::Result<()> {
self.column += amount;
Ok(())
}
fn move_left(&mut self, amount: u16) -> io::Result<()> {
self.column = self.column.saturating_sub(amount);
Ok(())
}
fn move_to_next_line(&mut self) -> io::Result<()> {
let amount = self.current_row_height();
self.row += amount;
self.column = 0;
self.set_current_row_height(1);
Ok(())
}
fn print_text(&mut self, content: &str, style: &TextStyle) -> io::Result<()> {
let style = style.merged(&TextStyle::default().colors(self.colors));
for c in content.chars() {
let Some(cell) = self.current_cell_mut() else {
continue;
};
cell.character = c;
cell.style = style;
self.column += style.size as u16;
}
let height = self.current_row_height().max(style.size as u16);
self.set_current_row_height(height);
Ok(())
}
fn clear_screen(&mut self) -> io::Result<()> {
for row in &mut self.rows {
for cell in row {
cell.character = ' ';
}
}
self.background_color = self.colors.background;
Ok(())
}
fn set_colors(&mut self, colors: crate::markdown::text_style::Colors) -> io::Result<()> {
self.colors = colors;
Ok(())
}
fn set_background_color(&mut self, color: Color) -> io::Result<()> {
self.colors.background = Some(color);
Ok(())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
fn print_image(&mut self, image: &Image, options: &PrintOptions) -> Result<(), PrintImageError> {
match &self.image_behavior {
ImageBehavior::Store => {
let key = (self.row, self.column);
let image = PrintedImage { image: image.clone(), width_columns: options.columns };
self.images.insert(key, image);
}
ImageBehavior::PrintAscii => {
let image = image.to_ascii();
let image_printer = AsciiPrinter;
image_printer.print(&image, options, self)?
}
};
Ok(())
}
}
impl fmt::Debug for VirtualTerminal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VirtualTerminal")
.field("row", &self.row)
.field("column", &self.column)
.field("colors", &self.colors)
.field("background_color", &self.background_color)
.field("images", &self.images)
.field("row_heights", &self.row_heights)
.field("image_behavior", &self.image_behavior)
.finish()
}
}
impl TerminalIo for VirtualTerminal {
fn execute(&mut self, command: &TerminalCommand<'_>) -> Result<(), TerminalError> {
use TerminalCommand::*;
match command {
BeginUpdate | EndUpdate => (),
MoveTo { column, row } => self.move_to(*column, *row)?,
MoveToRow(row) => self.move_to_row(*row)?,
MoveToColumn(column) => self.move_to_column(*column)?,
MoveDown(amount) => self.move_down(*amount)?,
MoveRight(amount) => self.move_right(*amount)?,
MoveLeft(amount) => self.move_left(*amount)?,
MoveToNextLine => self.move_to_next_line()?,
PrintText { content, style } => self.print_text(content, style)?,
ClearScreen => self.clear_screen()?,
SetColors(colors) => self.set_colors(*colors)?,
SetBackgroundColor(color) => self.set_background_color(*color)?,
SetCursorBoundaries { .. } => (),
Flush => self.flush()?,
PrintImage { image, options } => self.print_image(image, options)?,
};
Ok(())
}
fn cursor_row(&self) -> u16 {
self.row
}
}
#[derive(Clone, Debug, Default)]
pub(crate) enum ImageBehavior {
#[default]
Store,
PrintAscii,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct StyledChar {
pub(crate) character: char,
pub(crate) style: TextStyle,
}
impl StyledChar {
#[cfg(test)]
pub(crate) fn new(character: char, style: TextStyle) -> Self {
Self { character, style }
}
}
impl From<char> for StyledChar {
fn from(character: char) -> Self {
Self { character, style: Default::default() }
}
}
impl Default for StyledChar {
fn default() -> Self {
Self { character: ' ', style: Default::default() }
}
}
#[cfg(test)]
mod tests {
use super::*;
trait TerminalGridExt {
fn assert_contents(&self, lines: &[&str]);
}
impl TerminalGridExt for TerminalGrid {
fn assert_contents(&self, lines: &[&str]) {
assert_eq!(self.rows.len(), lines.len());
for (line, expected) in self.rows.iter().zip(lines) {
let line: String = line.iter().map(|c| c.character).collect();
assert_eq!(line, *expected);
}
}
}
#[test]
fn text() {
let dimensions = WindowSize { rows: 2, columns: 3, height: 0, width: 0 };
let mut term = VirtualTerminal::new(dimensions, Default::default());
for c in "abc".chars() {
term.print_text(&c.to_string(), &Default::default()).expect("print failed");
}
term.move_to_next_line().unwrap();
term.print_text("A", &Default::default()).expect("print failed");
let grid = term.into_contents();
grid.assert_contents(&["abc", "A "]);
}
#[test]
fn movement() {
let dimensions = WindowSize { rows: 2, columns: 3, height: 0, width: 0 };
let mut term = VirtualTerminal::new(dimensions, Default::default());
term.print_text("A", &Default::default()).unwrap();
term.move_down(1).unwrap();
term.print_text("B", &Default::default()).unwrap();
term.move_to(2, 0).unwrap();
term.print_text("C", &Default::default()).unwrap();
term.move_to_row(1).unwrap();
term.move_to_column(2).unwrap();
term.print_text("D", &Default::default()).unwrap();
let grid = term.into_contents();
grid.assert_contents(&["A C", " BD"]);
}
#[test]
fn iterator() {
let row = &[
StyledChar { character: ' ', style: TextStyle::default() },
StyledChar { character: 'A', style: TextStyle::default() },
StyledChar { character: 'B', style: TextStyle::default().bold() },
StyledChar { character: 'C', style: TextStyle::default().bold() },
StyledChar { character: 'D', style: TextStyle::default() },
];
let texts: Vec<_> = TerminalRowIterator::new(row).collect();
assert_eq!(texts, &[Text::from(" A"), Text::new("BC", TextStyle::default().bold()), Text::from("D")]);
}
}