minitel_ratatui/
lib.rs

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/// Keep track of the contextual data
12///
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CharKind {
15    None,
16    /// Last char was a normal char
17    Alphabet(SIChar),
18    /// Last char was a semi-graphic char
19    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
32/// Ratatui minitel backend
33pub 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            // Zone attributes: background color, invert, ...
64            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            // Char attributes: foreground color, blink, ...
93            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            // Chose between a char or a semi graphic
121            // The crossed out modifier is taken as prefering a semi graphic char
122            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            // Check if the previous context is invalidated
142            if self.cursor_position != (x, y)
143                || std::mem::discriminant(&self.last_char_kind)
144                    != std::mem::discriminant(&char_kind)
145            {
146                // Invalidated, we can start from scratch
147                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                // Move the cursor to the right position, select the char set
153                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                    // Empty char, update the zone attributes if necessary
160                    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                    // Alphabetic char, update the char attributes if necessary
170                    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                    // Semigraphic char, update both the zone and char attributes if necessary
181                    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                    // Write the semi graphic char
194                    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    /// Variation on ONE_EIGHTH_WIDE offsetting it on the right to allow
252    /// a consistent background transition in videotex mode.
253    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}