1use backend::WindowSize;
2
3use ratatui::backend::Backend;
4use ratatui::prelude::*;
5
6use minitel_stum::{
7 videotex::{GrayScale, SIChar, C0, C1, G0, G1},
8 Minitel, MinitelRead, MinitelWrite,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CharKind {
15 None,
16 Alphabet(SIChar),
18 SemiGraphic(G1),
20}
21
22impl CharKind {
23 pub fn escape_code(&self) -> C0 {
24 match self {
25 CharKind::None => C0::NUL,
26 CharKind::Alphabet(_) => C0::SI,
27 CharKind::SemiGraphic(_) => C0::SO,
28 }
29 }
30}
31
32pub struct MinitelBackend<S: MinitelRead + MinitelWrite> {
34 pub minitel: Minitel<S>,
35
36 cursor_position: (u16, u16),
37 last_char_kind: CharKind,
38 char_attributes: Vec<C1>,
39 zone_attributes: Vec<C1>,
40}
41
42impl<S: MinitelRead + MinitelWrite> MinitelBackend<S> {
43 pub fn new(minitel: Minitel<S>) -> Self {
44 Self {
45 minitel,
46 cursor_position: (255, 255),
47 last_char_kind: CharKind::None,
48 char_attributes: Vec::new(),
49 zone_attributes: Vec::new(),
50 }
51 }
52}
53
54impl<S: MinitelRead + MinitelWrite> Backend for MinitelBackend<S> {
55 #[inline(always)]
56 fn draw<'a, I>(&mut self, content: I) -> std::io::Result<()>
57 where
58 I: Iterator<Item = (u16, u16, &'a ratatui::buffer::Cell)>,
59 {
60 for (x, y, cell) in content {
61 self.cursor_position.0 += 1;
62
63 let mut zone_attributes = vec![match cell.bg {
65 Color::Black => C1::BgBlack,
66 Color::Red => C1::BgRed,
67 Color::Green => C1::BgGreen,
68 Color::Yellow => C1::BgYellow,
69 Color::Blue => C1::BgBlue,
70 Color::Magenta => C1::BgMagenta,
71 Color::Cyan => C1::BgCyan,
72 Color::Gray => GrayScale::Gray50.char(),
73 Color::DarkGray => GrayScale::Gray40.char(),
74 Color::LightRed => C1::BgRed,
75 Color::LightGreen => C1::BgGreen,
76 Color::LightYellow => C1::BgYellow,
77 Color::LightBlue => C1::BgBlue,
78 Color::LightMagenta => C1::BgMagenta,
79 Color::LightCyan => C1::BgCyan,
80 Color::White => C1::BgWhite,
81 _ => C1::BgBlack,
82 }];
83 zone_attributes.push(match cell.modifier.contains(Modifier::UNDERLINED) {
84 true => C1::BeginUnderline,
85 false => C1::EndUnderline,
86 });
87 zone_attributes.push(match cell.modifier.contains(Modifier::REVERSED) {
88 true => C1::InvertBg,
89 false => C1::NormalBg,
90 });
91
92 let mut char_attributes = Vec::new();
94 char_attributes.push(match cell.fg {
95 Color::Black => C1::CharBlack,
96 Color::Red => C1::CharRed,
97 Color::Green => C1::CharGreen,
98 Color::Yellow => C1::CharYellow,
99 Color::Blue => C1::CharBlue,
100 Color::Magenta => C1::CharMagenta,
101 Color::Cyan => C1::CharCyan,
102 Color::Gray => GrayScale::Gray50.char(),
103 Color::DarkGray => GrayScale::Gray40.char(),
104 Color::LightRed => C1::CharRed,
105 Color::LightGreen => C1::CharGreen,
106 Color::LightYellow => C1::CharYellow,
107 Color::LightBlue => C1::CharBlue,
108 Color::LightMagenta => C1::CharMagenta,
109 Color::LightCyan => C1::CharCyan,
110 Color::White => C1::CharWhite,
111 _ => C1::CharWhite,
112 });
113
114 if cell.modifier.contains(Modifier::RAPID_BLINK)
115 || cell.modifier.contains(Modifier::SLOW_BLINK)
116 {
117 char_attributes.push(C1::Blink);
118 }
119
120 let c = cell.symbol().chars().next().unwrap();
123 let char_kind = if cell.modifier.contains(Modifier::CROSSED_OUT) {
124 G1::approximate_char(c)
125 .map(CharKind::SemiGraphic)
126 .unwrap_or_else(|| {
127 SIChar::try_from(c)
128 .map(CharKind::Alphabet)
129 .unwrap_or(CharKind::None)
130 })
131 } else {
132 SIChar::try_from(c)
133 .map(CharKind::Alphabet)
134 .unwrap_or_else(|_| {
135 G1::approximate_char(c)
136 .map(CharKind::SemiGraphic)
137 .unwrap_or(CharKind::None)
138 })
139 };
140
141 if self.cursor_position != (x, y)
143 || std::mem::discriminant(&self.last_char_kind)
144 != std::mem::discriminant(&char_kind)
145 {
146 self.cursor_position = (x, y);
148 self.char_attributes = Vec::new();
149 self.zone_attributes = Vec::new();
150 self.last_char_kind = char_kind;
151
152 self.minitel.set_pos(x as u8, y as u8)?;
154 self.minitel.write_byte(char_kind.escape_code() as u8)?;
155 }
156
157 match char_kind {
158 CharKind::Alphabet(SIChar::G0(G0(0x20))) => {
159 if self.zone_attributes != zone_attributes {
161 for attr in &zone_attributes {
162 self.minitel.c1(*attr)?;
163 }
164 self.zone_attributes.clone_from(&zone_attributes);
165 }
166 self.minitel.write_byte(0x20)?;
167 }
168 CharKind::Alphabet(c) => {
169 if self.char_attributes != char_attributes {
171 for attr in &char_attributes {
172 self.minitel.c1(*attr)?;
173 }
174 self.char_attributes.clone_from(&char_attributes);
175 }
176
177 self.minitel.si_char(c)?;
178 }
179 CharKind::SemiGraphic(c) => {
180 if self.zone_attributes != zone_attributes {
182 for attr in &zone_attributes {
183 self.minitel.c1(*attr)?;
184 }
185 self.zone_attributes.clone_from(&zone_attributes);
186 }
187 if self.char_attributes != char_attributes {
188 for attr in &char_attributes {
189 self.minitel.c1(*attr)?;
190 }
191 self.char_attributes.clone_from(&char_attributes);
192 }
193 self.minitel.write_byte(c)?;
195 }
196 _ => {}
197 }
198 }
199 Ok(())
200 }
201
202 fn hide_cursor(&mut self) -> std::io::Result<()> {
203 self.minitel.hide_cursor()?;
204 Ok(())
205 }
206
207 fn show_cursor(&mut self) -> std::io::Result<()> {
208 self.minitel.show_cursor()?;
209 Ok(())
210 }
211
212 fn get_cursor_position(&mut self) -> std::io::Result<ratatui::prelude::Position> {
213 let (x, y) = self.minitel.get_pos()?;
214 Ok(Position::new(x as u16, y as u16))
215 }
216
217 fn set_cursor_position<P: Into<ratatui::prelude::Position>>(
218 &mut self,
219 position: P,
220 ) -> std::io::Result<()> {
221 let position: Position = position.into();
222 self.minitel.set_pos(position.x as u8, position.y as u8)?;
223 Ok(())
224 }
225
226 fn clear(&mut self) -> std::io::Result<()> {
227 self.minitel.clear_screen()?;
228 Ok(())
229 }
230
231 fn size(&self) -> std::io::Result<ratatui::prelude::Size> {
232 Ok(Size::new(40, 24))
233 }
234
235 fn window_size(&mut self) -> std::io::Result<ratatui::backend::WindowSize> {
236 Ok(WindowSize {
237 columns_rows: self.size()?,
238 pixels: self.size()?,
239 })
240 }
241
242 fn flush(&mut self) -> std::io::Result<()> {
243 self.minitel.flush()?;
244 Ok(())
245 }
246}
247
248pub mod border {
249 use ratatui::symbols::border;
250
251 pub const ONE_EIGHTH_WIDE_OFFSET: border::Set = border::Set {
254 top_right: "▁",
255 top_left: " ",
256 bottom_right: "▔",
257 bottom_left: " ",
258 vertical_left: "▕",
259 vertical_right: "▕",
260 horizontal_top: "▁",
261 horizontal_bottom: "▔",
262 };
263
264 pub const ONE_EIGHTH_WIDE_BEVEL: border::Set = border::Set {
265 top_right: "\\",
266 top_left: "/",
267 bottom_right: "/",
268 bottom_left: "\\",
269 vertical_left: "▏",
270 vertical_right: "▕",
271 horizontal_top: "▔",
272 horizontal_bottom: "▁",
273 };
274}