use std::io::Write;
use backend::WindowSize;
use ratatui::prelude::*;
use ratatui::style::Styled;
use ratatui::{backend::Backend, buffer::Cell};
use crate::{
stum::videotex::{GrayScale, Repeat, SIChar, SetPosition, C0, C1, G0, G1},
MinitelMessage,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CharKind {
None,
Alphabet(SIChar),
SemiGraphic(G1),
}
impl CharKind {
pub fn escape_code(&self) -> C0 {
match self {
CharKind::None => C0::NUL,
CharKind::Alphabet(_) => C0::SI,
CharKind::SemiGraphic(_) => C0::SO,
}
}
}
pub struct MinitelBackend<S: Write> {
pub stream: S,
cursor_position: (u16, u16),
last_char_kind: CharKind,
char_attributes: Vec<C1>,
zone_attributes: Vec<C1>,
repeat: u8,
last_cell: Option<Cell>,
}
impl<S: Write> MinitelBackend<S> {
pub fn new(stream: S) -> Self {
Self {
stream,
cursor_position: (255, 255),
last_char_kind: CharKind::None,
char_attributes: Vec::new(),
zone_attributes: Vec::new(),
repeat: 0,
last_cell: None,
}
}
fn send<T>(&mut self, message: T) -> std::io::Result<()>
where
T: MinitelMessage,
{
self.stream.write_all(&message.message())
}
}
impl<S: Write> Backend for MinitelBackend<S> {
#[inline(always)]
fn draw<'a, I>(&mut self, content: I) -> std::io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, cell) in content {
self.cursor_position.0 += 1;
if (self.cursor_position.0, self.cursor_position.1) == (x, y)
&& Some(cell.to_owned()) == self.last_cell
{
self.repeat += 1;
continue;
} else if self.repeat > 0 {
self.send(Repeat(self.repeat))?;
self.repeat = 0;
}
self.last_cell = Some(cell.to_owned());
let mut zone_attributes = vec![match cell.bg {
Color::Black => C1::BgBlack,
Color::Red => C1::BgRed,
Color::Green => C1::BgGreen,
Color::Yellow => C1::BgYellow,
Color::Blue => C1::BgBlue,
Color::Magenta => C1::BgMagenta,
Color::Cyan => C1::BgCyan,
Color::Gray => GrayScale::Gray50.char(),
Color::DarkGray => GrayScale::Gray40.char(),
Color::LightRed => C1::BgRed,
Color::LightGreen => C1::BgGreen,
Color::LightYellow => C1::BgYellow,
Color::LightBlue => C1::BgBlue,
Color::LightMagenta => C1::BgMagenta,
Color::LightCyan => C1::BgCyan,
Color::White => C1::BgWhite,
_ => C1::BgBlack,
}];
zone_attributes.push(match cell.modifier.contains(Modifier::UNDERLINED) {
true => C1::BeginUnderline,
false => C1::EndUnderline,
});
zone_attributes.push(match cell.modifier.contains(Modifier::REVERSED) {
true => C1::InvertBg,
false => C1::NormalBg,
});
let mut char_attributes = Vec::new();
char_attributes.push(match cell.fg {
Color::Black => C1::CharBlack,
Color::Red => C1::CharRed,
Color::Green => C1::CharGreen,
Color::Yellow => C1::CharYellow,
Color::Blue => C1::CharBlue,
Color::Magenta => C1::CharMagenta,
Color::Cyan => C1::CharCyan,
Color::Gray => GrayScale::Gray50.char(),
Color::DarkGray => GrayScale::Gray40.char(),
Color::LightRed => C1::CharRed,
Color::LightGreen => C1::CharGreen,
Color::LightYellow => C1::CharYellow,
Color::LightBlue => C1::CharBlue,
Color::LightMagenta => C1::CharMagenta,
Color::LightCyan => C1::CharCyan,
Color::White => C1::CharWhite,
_ => C1::CharWhite,
});
if cell.modifier.contains(Modifier::RAPID_BLINK)
|| cell.modifier.contains(Modifier::SLOW_BLINK)
{
char_attributes.push(C1::Blink);
} else {
char_attributes.push(C1::Fixed);
}
let c = cell.symbol().chars().next().unwrap();
let char_kind = if cell.modifier.contains(Modifier::CROSSED_OUT) {
G1::approximate_char(c)
.map(CharKind::SemiGraphic)
.unwrap_or_else(|| {
SIChar::try_from(c)
.map(CharKind::Alphabet)
.unwrap_or(CharKind::None)
})
} else {
SIChar::try_from(c)
.map(CharKind::Alphabet)
.unwrap_or_else(|_| {
G1::approximate_char(c)
.map(CharKind::SemiGraphic)
.unwrap_or(CharKind::None)
})
};
if self.cursor_position != (x, y)
|| std::mem::discriminant(&self.last_char_kind)
!= std::mem::discriminant(&char_kind)
{
self.cursor_position = (x, y);
self.char_attributes = Vec::new();
self.zone_attributes = Vec::new();
self.last_char_kind = char_kind;
self.stream
.write_all(&SetPosition(x as u8, y as u8).message())?;
self.send(char_kind.escape_code())?;
}
match char_kind {
CharKind::Alphabet(SIChar::G0(G0(0x20))) => {
if self.zone_attributes != zone_attributes {
for attr in &zone_attributes {
self.send(*attr)?;
}
self.zone_attributes.clone_from(&zone_attributes);
}
self.send(SIChar::G0(G0(0x20)))?;
}
CharKind::Alphabet(c) => {
if self.char_attributes != char_attributes {
for attr in &char_attributes {
self.send(*attr)?;
}
self.char_attributes.clone_from(&char_attributes);
}
self.send(c)?;
}
CharKind::SemiGraphic(c) => {
if self.zone_attributes != zone_attributes {
for attr in &zone_attributes {
self.send(*attr)?;
}
self.zone_attributes.clone_from(&zone_attributes);
}
if self.char_attributes != char_attributes {
for attr in &char_attributes {
self.send(*attr)?;
}
self.char_attributes.clone_from(&char_attributes);
}
self.send(c)?;
}
_ => {}
}
}
if self.repeat > 0 {
self.send(Repeat(self.repeat))?;
self.repeat = 0;
}
Ok(())
}
fn hide_cursor(&mut self) -> std::io::Result<()> {
self.send(C0::Coff)?;
Ok(())
}
fn show_cursor(&mut self) -> std::io::Result<()> {
self.send(C0::Con)?;
Ok(())
}
fn get_cursor_position(&mut self) -> std::io::Result<ratatui::prelude::Position> {
Ok(self.cursor_position.into())
}
fn set_cursor_position<P: Into<ratatui::prelude::Position>>(
&mut self,
position: P,
) -> std::io::Result<()> {
let position: Position = position.into();
self.send(SetPosition(position.x as u8, position.y as u8))?;
Ok(())
}
fn clear(&mut self) -> std::io::Result<()> {
self.send(C0::FF)?;
Ok(())
}
fn size(&self) -> std::io::Result<ratatui::prelude::Size> {
Ok(Size::new(40, 25))
}
fn window_size(&mut self) -> std::io::Result<ratatui::backend::WindowSize> {
Ok(WindowSize {
columns_rows: self.size()?,
pixels: self.size()?,
})
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub mod border {
use ratatui::symbols::border;
pub const ONE_EIGHTH_WIDE_OFFSET: border::Set = border::Set {
top_right: "▁",
top_left: " ",
bottom_right: "▔",
bottom_left: " ",
vertical_left: "▕",
vertical_right: "▕",
horizontal_top: "▁",
horizontal_bottom: "▔",
};
pub const ONE_EIGHTH_WIDE_BEVEL: border::Set = border::Set {
top_right: "\\",
top_left: "/",
bottom_right: "/",
bottom_left: "\\",
vertical_left: "▏",
vertical_right: "▕",
horizontal_top: "▔",
horizontal_bottom: "▁",
};
}
pub trait StyledMinitelExt {
type Item;
#[cfg(feature = "invalidation-group")]
fn invalidation_group(self, group: u8) -> Self::Item;
}
impl<T> StyledMinitelExt for T
where
T: Styled<Item = T>,
{
type Item = Self;
#[cfg(feature = "invalidation-group")]
fn invalidation_group(self, group: u8) -> Self::Item {
let style = self.style().underline_color(Color::Indexed(group));
self.set_style(style)
}
}
pub mod widgets {
use ratatui::{prelude::*, style::Styled};
pub struct Fill {
pub char: char,
pub style: Style,
}
impl Default for Fill {
fn default() -> Self {
Self {
char: '█',
style: Style::default(),
}
}
}
impl Fill {
pub fn with_char(self, char: char) -> Self {
Self { char, ..self }
}
}
impl Styled for Fill {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
Self {
style: style.into(),
..self
}
}
}
impl Widget for Fill {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.set_symbol(&self.char.to_string());
}
}
}
}
}
}