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}