grux/
lib.rs

1//! A library for drawing grid-based user interfaces using ASCII characters.
2//!
3//! [`grux`][`crate`] provides:
4//! - A uniform interface for drawing to a 2D grid: [`GridWriter`].
5//! - A uniform interface for displaying a 2D grid: [`DisplayGrid`].
6//!
7//! The [`grux::art`][`crate::art`] module provides helper types for drawing ASCII art.
8//!
9//! # Examples
10//!
11//! ## Using a fixed-size nested array
12//!
13//! > 💡 **TIP**: Use a fixed-size nested array for a grid dimensions known ahead of time.
14//! >
15//! > - Nested arrays will be faster and more efficient than a growable grid
16//! > - Nested arrays support `Display` trait for cells, which means graphemes are supported and
17//! >   ANSI escape codes can be used for colors (see `examples/emojis.rs` and `examples/ansi.rs`).
18//!
19//! ```
20//! use grux::GridWriter;
21//!
22//! // Create a 2x2 array of zeros.
23//! let mut array = [[0; 2]; 2];
24//!
25//! // Set the element at (1, 1) to 1.
26//! array.set((1, 1), 1);
27//! assert_eq!(array, [[0, 0], [0, 1]]);
28//! ```
29//!
30//! ## Using a growable nested vector
31//!
32//! > 💡 **TIP**: Use a growable nested vector for a grid dimensions not known ahead of time.
33//! >
34//! > - Nested vectors will be slower and less efficient than a fixed-size grid
35//! > - Nested vectors support `Display` trait for cells, which means graphemes are supported
36//! > - A rectangular grid is not guaranteed
37//!
38//! ```
39//! use grux::GridWriter;
40//!
41//! // Create an empty vector (of vectors)
42//! let mut vec: Vec<Vec<i32>> = Vec::new();
43//!
44//! // Set the element at (1, 1) to 1.
45//! // This will grow the vector to fit the position, adding empty default vectors as needed.
46//! vec.set((1, 1), 1);
47//! assert_eq!(vec, vec![vec![], vec![0, 1]]);
48//! ```
49//!
50//! ## Using a growable string
51//!
52//! > ⚠️ **WARNING**: Only supports ASCII characters (`char`) and not graphemes or ANSI escape codes.
53//! >
54//! > - Strings are not as efficient or flexible as nested arrays or vectors
55//! > - Strings do not support graphemes or ANSI escape codes
56//! > - A rectangular grid is not guaranteed
57//! >
58//! > See [print any grid to a output stream](#print-any-grid-to-a-output-stream) for alternatives.
59//!
60//! ```
61//! use grux::GridWriter;
62//!
63//! // Create an empty string.
64//! let mut string = String::new();
65//!
66//! // Set the element at (1, 2) to '1'.
67//! // This will grow the string to fit the position, adding empty lines as needed.
68//! string.set((1, 2), '1');
69//! assert_eq!(string, "\n\n 1");
70//! ```
71//!
72//! ## Print any grid to a output stream
73//!
74//! Any type that implements [`DisplayGrid`] can be printed to a output stream _or_ a new string.
75//!
76//! ```
77//! use grux::DisplayGrid;
78//!
79//! // Create a 3x3 array of the letters 'A' - 'I'.
80//! let mut array = [['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']];
81//!
82//! // Convert the array to a string.
83//! // TIP: Use `print` instead if you want to print to a output stream.
84//! let string = array.to_string().unwrap();
85//!
86//! assert_eq!(string, "ABC\nDEF\nGHI\n");
87//! ```
88
89use std::{fmt::Display, string::FromUtf8Error};
90
91pub mod art;
92
93#[cfg(test)]
94mod tests;
95
96/// A trait for a grid-like writable buffer, typically with a fixed width and height.
97///
98/// The grid is indexed by `(x, y)` coordinates, where `x` is the column and `y` is the row.
99///
100/// # Examples
101///
102/// The provided structs and implementations are likely sufficient, but as an example:
103///
104/// ```
105/// # use grux::GridWriter;
106/// struct MyGrid {
107///     width: usize,
108///     height: usize,
109///     data: Vec<char>,
110/// }
111///
112/// impl GridWriter for MyGrid {
113///     type Element = char;
114///
115///     fn set(&mut self, position: (usize, usize), element: Self::Element) {
116///         let (x, y) = position;
117///         self.data[y * self.width + x] = element;
118///     }
119/// }
120/// ```
121pub trait GridWriter {
122    /// The type of the elements in the grid, e.g. `char`; must implement `Display`.
123    type Element: Display;
124
125    /// Sets the element at the given `(x, y)` position.
126    ///
127    /// How the position is interpreted is up to the implementor; for example, it could grow the
128    /// grid to fit the position, or it could panic if the position is out of bounds. See the
129    /// documentation for the implementor for more information.
130    fn set(&mut self, position: (usize, usize), element: Self::Element);
131}
132
133/// A trait that can be used to display a grid-like buffer to a output stream or a new string.
134pub trait DisplayGrid {
135    /// Returns a UTF-8 string representation of the grid.
136    ///
137    /// Each row is separated by a newline (`\n`), including the last row.
138    ///
139    /// # Performance
140    ///
141    /// Equivalent to calling `print` with a new vector, but is provided for convenience. If...
142    ///
143    /// - The grid is large
144    /// - The grid will be printed to an output stream (e.g. `stdout`)
145    /// - Memory is a concern
146    ///
147    /// ... then it is recommended to use `print` instead (or provide a custom `to_string`).
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if the grid contains invalid UTF-8.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// # use grux::DisplayGrid;
157    /// let mut grid = [['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']];
158    ///
159    /// assert_eq!(grid.to_string().unwrap(), "ABC\nDEF\nGHI\n");
160    /// ```
161    fn to_string(&self) -> Result<String, FromUtf8Error> {
162        let mut output = Vec::new();
163        self.write_to(&mut output).unwrap();
164        String::from_utf8(output)
165    }
166
167    /// Formats the grid into the given formatter.
168    ///
169    /// Each row is separated by a newline (`\n`), including the last row.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if the output stream returns an error.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// # use grux::DisplayGrid;
179    /// let mut grid = [['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']];
180    ///
181    /// // Print the grid to a vector (which can be substituted for say, stdout).
182    /// let mut output = Vec::new();
183    /// grid.write_to(&mut output).unwrap();
184    ///
185    /// assert_eq!(output, b"ABC\nDEF\nGHI\n");
186    /// ```
187    fn write_to(&self, stream: &mut impl std::io::Write) -> std::io::Result<()>;
188}
189
190/// Provides [`GridWriter`] for a fixed-size nested array of elements.
191///
192/// The outer array is assumed to be the rows, and the inner array is assumed to be the columns.
193///
194/// > ⓘ **NOTE**: While this doesn't seem like an intuitive way to index arrays (since it would be
195/// > more natural to index by `[y][x]`, this implementation allows nested arrays to be used the
196/// > same way as other data structures, i.e. the point of this library.
197///
198/// # Examples
199///
200/// ```
201/// # use grux::GridWriter;
202/// let mut array = [[0; 2]; 2];
203///
204/// // Set the element at (1, 1) to 1.
205/// array.set((1, 1), 1);
206///
207/// assert_eq!(array, [[0, 0], [0, 1]]);
208/// ```
209impl<const W: usize, const H: usize, T> GridWriter for [[T; W]; H]
210where
211    T: Display,
212{
213    type Element = T;
214
215    /// Sets the element at the given `(x, y)` position.
216    ///
217    /// # Panics
218    ///
219    /// If the position is out of bounds.
220    fn set(&mut self, position: (usize, usize), element: Self::Element) {
221        let (x, y) = position;
222        self[y][x] = element;
223    }
224}
225
226/// Provides [`DisplayGrid`] for a fixed-size nested array of elements.
227impl<const W: usize, const H: usize, T> DisplayGrid for [[T; W]; H]
228where
229    T: Display,
230{
231    fn write_to(&self, stream: &mut impl std::io::Write) -> std::io::Result<()> {
232        for row in self {
233            for element in row {
234                write!(stream, "{}", element)?;
235            }
236            writeln!(stream)?;
237        }
238        Ok(())
239    }
240}
241
242/// Provides [`GridWriter`] for a growable nested vector of elements.
243///
244/// The outer vector is assumed to be the rows, and the inner vector is assumed to be the columns.
245///
246/// Unlike fixed-size nested arrays, this implementation will grow the grid to fit the position;
247/// this is useful for drawing to a grid that is not known ahead of time. As such, the element is
248/// required to implement [`Default`] and [`Clone`].
249///
250/// > ⓘ **NOTE**: While this doesn't seem like an intuitive way to index vectors (since it would be
251/// > more natural to index by `[y][x]`, this implementation allows nested vectors to be used the
252/// > same way as other data structures, i.e. the point of this library.
253///
254/// # Limitations
255///
256/// A rectangular grid is not guaranteed. See the examples below for details.
257///
258/// # Examples
259///
260/// ```
261/// # use grux::GridWriter;
262/// let mut vec: Vec<Vec<i32>> = Vec::new();
263///
264/// // Set the element at (1, 1) to 1.
265/// // This will grow the vector to fit the position, adding empty default vectors as needed.
266/// vec.set((1, 1), 1);
267///
268/// assert_eq!(vec, vec![vec![], vec![0, 1]]);
269/// ```
270impl<T> GridWriter for Vec<Vec<T>>
271where
272    T: Display + Default + Clone,
273{
274    type Element = T;
275
276    /// Sets the element at the given `(x, y)` position.
277    ///
278    /// If the position is out of bounds, the grid will be resized to fit the position.
279    fn set(&mut self, position: (usize, usize), element: Self::Element) {
280        let (x, y) = position;
281
282        if y >= self.len() {
283            self.resize_with(y + 1, Vec::new);
284        }
285
286        let row = &mut self[y];
287
288        if x >= row.len() {
289            row.resize(x + 1, T::default());
290        }
291
292        row[x] = element;
293    }
294}
295
296/// Provides [`DisplayGrid`] for a growable nested vector of elements.
297impl<T> DisplayGrid for Vec<Vec<T>>
298where
299    T: Display + Default + Clone,
300{
301    fn write_to(&self, stream: &mut impl std::io::Write) -> std::io::Result<()> {
302        for row in self {
303            for element in row {
304                write!(stream, "{}", element)?;
305            }
306            writeln!(stream)?;
307        }
308        Ok(())
309    }
310}
311
312/// Provides [`GridWriter`] for a growable string of characters.
313///
314/// Unlike fixed-size nested arrays, this implementation will grow the grid to fit the position;
315/// this is useful for drawing to a grid that is not known ahead of time. "Empty" characters are
316/// assumed to be spaces (`' '`).
317///
318/// # Limitations
319///
320/// This implementation assumes that the string is a grid of characters, where each line is a row
321/// and each character is a column. This means that the string must be a valid UTF-8 string, and
322/// that the string cannot contain multi-byte characters (i.e. graphemes or ANSI escape sequences).
323///
324/// Additionally, a rectangular grid is not guaranteed. See the examples below for details.
325///
326/// # Performance
327///
328/// This implementation is not optimized for performance, and is intended for use in small grids
329/// (e.g. a 10x10 grid) or for prototyping. For larger grids, consider using a fixed-size nested
330/// array or a growable vector.
331///
332/// # Examples
333///
334/// ```
335/// # use grux::GridWriter;
336/// let mut string = String::new();
337///
338/// // Set the element at (1, 1) to 'X'.
339/// // This will grow the string to fit the position, adding empty spaces as needed.
340/// string.set((1, 1), 'X');
341///
342/// assert_eq!(string, "\n X");
343/// ```
344impl GridWriter for String {
345    type Element = char;
346
347    /// Sets the element at the given `(x, y)` position.
348    ///
349    /// If the position is out of bounds, the grid will be resized to fit the position.
350    fn set(&mut self, position: (usize, usize), element: Self::Element) {
351        let (x, y) = position;
352
353        // Create a vector of the rows (i.e lines) in the string.
354        let mut rows: Vec<&str> = self.lines().collect();
355
356        // Grow the rows if necessary.
357        while rows.len() <= y {
358            rows.push("");
359        }
360
361        // Replace the y-th row with a new row that is the same as the old row, but with the element
362        // at the x-th position replaced with the new element.
363        let mut row = rows[y].to_string();
364
365        // Grow the row if necessary, using spaces for the new characters.
366        while row.len() <= x {
367            row.push(' ');
368        }
369
370        // Replace the x-th character with the new element.
371        row.replace_range(x..=x, &element.to_string());
372
373        // Replace the y-th row with the new row, trimming any trailing whitespace.
374        rows[y] = row.trim_end();
375
376        // Replace the string with the new rows.
377        *self = rows.join("\n");
378    }
379}
380
381/// Provides [`DisplayGrid`] for a growable string of characters.
382///
383/// > ⓘ **NOTE**: This implementation is provided for consistency, but it's already a string, so...
384impl DisplayGrid for String {
385    fn to_string(&self) -> Result<String, FromUtf8Error> {
386        Ok(self.clone())
387    }
388
389    fn write_to(&self, stream: &mut impl std::io::Write) -> std::io::Result<()> {
390        write!(stream, "{}", self)
391    }
392}