hanbun/
lib.rs

1// Some trivia:
2// terminal = text input/output environment
3// console = physical terminal
4
5//! Welcome to the top of the hanbun crate.
6//! [`Buffer`] should be of interest to you.
7
8use crossterm::{
9    queue,
10    style::{ResetColor, SetBackgroundColor, SetForegroundColor},
11};
12use std::{
13    fmt,
14    io::{self, stdout, BufWriter, Write},
15};
16
17/// Returned by [`size`] if querying the terminal size failed.
18#[derive(Debug)]
19pub struct TerminalSizeError;
20
21impl fmt::Display for TerminalSizeError {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(formatter, "Failed to query terminal size")
24    }
25}
26
27impl std::error::Error for TerminalSizeError {}
28
29/// Returns the terminal's width and height.
30///
31/// # Examples
32///
33/// ```
34/// if let Ok((width, height)) = hanbun::size() {
35///     println!("Your terminal has {} cells!", width*height);
36/// } else {
37///     eprintln!("Failed to get terminal size!");
38/// }
39/// ```
40///
41/// # Errors
42///
43/// May return [`TerminalSizeError`] if the operation failed.
44pub fn size() -> Result<(usize, usize), TerminalSizeError> {
45    if let Ok((width, height)) = crossterm::terminal::size() {
46        Ok((width as usize, height as usize))
47    } else {
48        Err(TerminalSizeError)
49    }
50}
51
52/// See [this list](https://docs.rs/crossterm/0.19.0/crossterm/style/enum.Color.html) for all available colors.
53pub type Color = crossterm::style::Color;
54
55/// Represents a terminal cell. Every cell has two blocks.
56#[derive(Debug, Clone)]
57pub struct Cell {
58    /// The upper block. It can be modified using [`Buffer::set`] and [`Buffer::color`].
59    pub upper_block: Option<Option<Color>>,
60    /// The lower block. It can be modified using [`Buffer::set`] and [`Buffer::color`].
61    pub lower_block: Option<Option<Color>>,
62    /// The character used if both [`Cell::upper_block`] and [`Cell::lower_block`] are [`None`].
63    ///
64    /// This character occupies the whole cell.
65    pub char: Option<char>,
66    /// A color for [`Cell::char`].
67    pub char_color: Option<Color>,
68}
69
70/// A buffer for storing the state of the cells.
71/// You can see it as a drawing canvas.
72///
73/// # Examples
74///
75/// ```
76/// let mut buffer;
77///
78/// if let Ok((width, height)) = hanbun::size() {
79///     buffer = hanbun::Buffer::new(width, height, ' ');
80/// } else {
81///     return;
82/// }
83///
84/// buffer.set(3, 3);
85/// buffer.draw();
86/// ```
87pub struct Buffer {
88    pub cells: Vec<Cell>,
89    pub width: usize,
90    pub height: usize,
91    writer: BufWriter<io::Stdout>,
92}
93
94impl Buffer {
95    /// Creates a new buffer of `width * height` cells filled with `char`.
96    pub fn new(width: usize, height: usize, char: char) -> Buffer {
97        Buffer {
98            cells: vec![
99                Cell {
100                    upper_block: None,
101                    lower_block: None,
102                    char: Some(char),
103                    char_color: None
104                };
105                width * height
106            ],
107            writer: BufWriter::with_capacity(width * height, stdout()),
108            width,
109            height,
110        }
111    }
112
113    /// Draws the buffer to the screen.
114    ///
115    /// # Panics
116    ///
117    /// Panics if an internal write operation operation failed.
118    pub fn draw(&mut self) {
119        let writer = &mut self.writer;
120        let mut x = 0;
121        let mut y = 1;
122        for cell in &self.cells {
123            // NOTE: This can be improved after https://github.com/rust-lang/rust/issues/53667
124            if cell.upper_block.is_some() && cell.lower_block.is_some() {
125                if let Some(Some(upper_color)) = cell.upper_block {
126                    if let Some(Some(lower_color)) = cell.lower_block {
127                        queue!(writer, SetForegroundColor(upper_color)).unwrap();
128                        queue!(writer, SetBackgroundColor(lower_color)).unwrap();
129                        writer.write_all("▀".as_bytes()).unwrap();
130                    } else {
131                        queue!(writer, SetBackgroundColor(upper_color)).unwrap();
132                        writer.write_all("▄".as_bytes()).unwrap();
133                    }
134                    queue!(writer, ResetColor).unwrap();
135                } else if let Some(Some(lower_color)) = cell.lower_block {
136                    if let Some(Some(upper_color)) = cell.upper_block {
137                        queue!(writer, SetForegroundColor(upper_color)).unwrap();
138                        queue!(writer, SetBackgroundColor(lower_color)).unwrap();
139                        writer.write_all("▀".as_bytes()).unwrap();
140                    } else {
141                        queue!(writer, SetBackgroundColor(lower_color)).unwrap();
142                        writer.write_all("▀".as_bytes()).unwrap();
143                    }
144                    queue!(writer, ResetColor).unwrap();
145                } else {
146                    writer.write_all("█".as_bytes()).unwrap();
147                }
148            } else if let Some(upper_block) = cell.upper_block {
149                if let Some(color) = upper_block {
150                    queue!(writer, SetForegroundColor(color)).unwrap();
151                }
152                writer.write_all("▀".as_bytes()).unwrap();
153                if upper_block.is_some() {
154                    queue!(writer, ResetColor).unwrap();
155                }
156            } else if let Some(lower_block) = cell.lower_block {
157                if let Some(color) = lower_block {
158                    queue!(writer, SetForegroundColor(color)).unwrap();
159                }
160                writer.write_all("▄".as_bytes()).unwrap();
161                if lower_block.is_some() {
162                    queue!(writer, ResetColor).unwrap();
163                }
164            } else if let Some(char) = &cell.char {
165                if let Some(color) = cell.char_color {
166                    queue!(writer, SetForegroundColor(color)).unwrap();
167                }
168
169                write!(writer, "{}", char).unwrap();
170                if cell.char_color.is_some() {
171                    queue!(writer, ResetColor).unwrap();
172                }
173            } else {
174                unreachable!();
175            }
176
177            x += 1;
178            if y != self.height && x == self.width {
179                writer.write_all(b"\n").unwrap();
180                x = 0;
181                y += 1;
182            }
183        }
184        self.writer.flush().unwrap();
185    }
186
187    /// Clears the buffer using `char`.
188    pub fn clear(&mut self, char: char) {
189        self.cells.fill(Cell {
190            upper_block: None,
191            lower_block: None,
192            char: Some(char),
193            char_color: None,
194        })
195    }
196
197    /// Clears the buffer using `char` colored with `color`.
198    pub fn colored_clear(&mut self, char: char, color: Color) {
199        self.cells.fill(Cell {
200            upper_block: None,
201            lower_block: None,
202            char: Some(char),
203            char_color: Some(color),
204        })
205    }
206
207    /// Sets the cell at (`x`, `y`) to a half block.
208    ///
209    /// # Panics
210    ///
211    /// Panics if (`x`, `y`) is out of the buffer's range.
212    pub fn set(&mut self, x: usize, y: usize) {
213        let position = x + self.width * (y / 2);
214        let current_cell = &self
215            .cells
216            .get(position)
217            .unwrap_or_else(|| panic!("setting block at ({}, {}) (out of range)", x, y));
218
219        if y % 2 == 0 {
220            self.cells[position] = Cell {
221                upper_block: Some(None),
222                lower_block: current_cell.lower_block,
223                char: None,
224                char_color: None,
225            };
226        } else {
227            self.cells[position] = Cell {
228                upper_block: current_cell.upper_block,
229                lower_block: Some(None),
230                char: None,
231                char_color: None,
232            };
233        }
234    }
235
236    /// Colors the cell at (`x`, `y`) with `color`.
237    ///
238    /// # Panics
239    ///
240    /// Panics if (`x`, `y`) is out of the buffer's range.
241    pub fn color(&mut self, x: usize, y: usize, color: Color) {
242        let position = x + self.width * (y / 2);
243        let current_cell = &self
244            .cells
245            .get(position)
246            .unwrap_or_else(|| panic!("coloring block at ({}, {}) (out of range)", x, y));
247
248        if y % 2 == 0 {
249            self.cells[position] = Cell {
250                upper_block: Some(Some(color)),
251                lower_block: current_cell.lower_block,
252                char: None,
253                char_color: None,
254            };
255        } else {
256            self.cells[position] = Cell {
257                upper_block: current_cell.upper_block,
258                lower_block: Some(Some(color)),
259                char: None,
260                char_color: None,
261            };
262        }
263    }
264
265    /// Prints `string` to (`x`, `y`).
266    ///
267    /// # Panics
268    ///
269    /// Panics if (`x`, `y`) is out of the buffer's range.
270    pub fn print(&mut self, x: usize, y: usize, string: &str) {
271        let position = x + self.width * (y / 2);
272
273        for (index, char) in string.chars().enumerate() {
274            let cell = self
275                .cells
276                .get_mut(index + position)
277                .unwrap_or_else(|| panic!("printing at ({}, {}) (out of range)", x, y));
278
279            *cell = Cell {
280                upper_block: None,
281                lower_block: None,
282                char: Some(char),
283                char_color: None,
284            };
285        }
286    }
287
288    /// Prints a colored `string` to (`x`, `y`) with `color`.
289    ///
290    /// # Panics
291    ///
292    /// Panics if (`x`, `y`) is out of the buffer's range.
293    pub fn colored_print(&mut self, x: usize, y: usize, string: &str, color: Color) {
294        let position = x + self.width * (y / 2);
295
296        for (index, char) in string.chars().enumerate() {
297            let cell = self
298                .cells
299                .get_mut(index + position)
300                .unwrap_or_else(|| panic!("printing at ({}, {}) (out of range)", x, y));
301
302            *cell = Cell {
303                upper_block: None,
304                lower_block: None,
305                char: Some(char),
306                char_color: Some(color),
307            };
308        }
309    }
310}