chaos_engine/
chaos.rs

1use crate::{Page, types::Vector2};
2use crossterm::{
3    ExecutableCommand, cursor,
4    event::{self, Event, KeyCode, KeyEvent},
5    terminal::{self, enable_raw_mode},
6};
7use std::io::{self, Write};
8
9/// The primary struct of chaos-engine.
10///
11/// This struct must be instantiated once to start using chaos-engine and its features.
12///
13/// # Examples
14///
15/// ```no_run
16/// use chaos_engine::{Chaos, ChaosOptions};
17///
18/// let stdout = std::io::stdout();
19/// let options = ChaosOptions::default();
20///
21/// let mut chaos = Chaos::new(stdout, options);
22/// ```
23pub struct Chaos<'a> {
24    paddings: ChaosPaddings,
25    stdout: io::Stdout,
26    input_label: &'a str,
27    dimensions: Vector2<u16>,
28    position: Vector2<u16>,
29}
30
31impl<'a> Chaos<'a> {
32    /// Instantiate the chaos engine with specified options.
33    ///
34    /// It enables raw mode where input must be handled manually.
35    pub fn new(stdout: io::Stdout, options: ChaosOptions<'a>) -> Self {
36        enable_raw_mode().unwrap();
37
38        Self {
39            stdout,
40            input_label: options.input_label,
41            dimensions: Self::get_dimensions(),
42            position: Self::get_position(),
43            paddings: ChaosPaddings {
44                input: options.input_padding,
45                buffer: options.buffer_padding,
46            },
47        }
48    }
49
50    /// Completely clears the terminal screen of any visible text.
51    ///
52    /// # Panics
53    ///
54    /// Panics in the case of a terminal error.
55    pub fn clear_terminal(&mut self) {
56        self.stdout
57            .execute(terminal::Clear(terminal::ClearType::All))
58            .unwrap();
59    }
60
61    /// Returns the X position of the last character in the input.
62    fn last_character_pos(&self, input_len: usize) -> u16 {
63        // left padding + input label + input length
64        self.paddings.input.x + self.input_label.len() as u16 + 1 + input_len as u16
65    }
66
67    /// Gets input from the user.
68    ///
69    /// It takes a page to resize in the event of a terminal resize, then writes the input
70    /// prompt on the last line of the terminal, overwriting anything on that line.
71    ///
72    /// # Examples
73    ///
74    /// ```no_run
75    /// use chaos_engine::{Chaos, ChaosOptions, Page};
76    ///
77    /// let mut page = Page::new();
78    /// let mut chaos = Chaos::new(std::io::stdout(), ChaosOptions::default());
79    ///
80    /// loop {
81    ///     chaos.clear_terminal();
82    ///     chaos.print(&mut page);
83    ///
84    ///     let input = chaos.get_input(&mut page).unwrap();
85    ///     if input == "exit" {
86    ///         chaos.alternate_screen(false);
87    ///         break;
88    ///     }
89    ///
90    ///     // do stuff here
91    /// }
92    /// ```
93    ///
94    /// # Panics
95    ///
96    /// This can panic when it fails to read the terminal events.
97    pub fn get_input(&mut self, page: &mut Page) -> Result<String, io::Error> {
98        let mut input = String::new();
99        self.prepare_input();
100
101        loop {
102            match event::read()? {
103                Event::Resize(_, _) => {
104                    self.update_dimensions();
105                    page.align(&self);
106                    self.clear_terminal();
107                    self.print(page);
108                    self.prepare_input();
109
110                    let last_character_pos = self.last_character_pos(input.len());
111
112                    if last_character_pos < self.dimensions.x {
113                        print!("{input}");
114                        self.move_cursor(last_character_pos, self.dimensions.y - 1);
115                        self.update_position();
116                    } else {
117                        input = String::new();
118                    }
119                }
120                Event::Key(KeyEvent {
121                    code: KeyCode::Backspace,
122                    ..
123                }) if !input.is_empty() => {
124                    self.move_cursor(self.position.x - 1, self.position.y);
125                    print!(" ");
126                    self.move_cursor(self.position.x - 1, self.position.y);
127                    self.update_position();
128                    input.pop();
129                }
130                Event::Key(KeyEvent {
131                    code: KeyCode::Char(c),
132                    ..
133                }) if c.is_ascii()
134                    && self.dimensions.x - 1 > self.last_character_pos(input.len()) =>
135                {
136                    print!("{c}");
137                    self.move_cursor(self.position.x + 1, self.position.y);
138                    self.update_position();
139                    input.push(c);
140                }
141                Event::Key(KeyEvent {
142                    code: KeyCode::Enter,
143                    ..
144                }) => break,
145                _ => (),
146            }
147        }
148
149        Ok(input)
150    }
151
152    /// Prints the input prompt on the last line, and moves the cursor to the right position.
153    fn prepare_input(&mut self) {
154        self.move_cursor(self.paddings.input.x, self.dimensions.y - 1);
155        print!("{}", self.input_label);
156        self.move_cursor(
157            self.paddings.input.x + self.input_label.len() as u16 + 1,
158            self.dimensions.y - 1,
159        );
160        self.update_position();
161    }
162
163    /// Moves the cursor to the specified X and Y positions.
164    ///
165    /// # Panics
166    ///
167    /// Panics in the case of a terminal error.
168    pub fn move_cursor(&mut self, x: u16, y: u16) {
169        self.stdout.execute(cursor::MoveTo(x, y)).unwrap();
170        self.stdout.flush().unwrap();
171    }
172
173    /// Enables and disables the terminal's alternate screen.
174    ///
175    /// An alternate screen is a separate buffer. On entering an alternate screen,
176    /// the terminal gets completely cleared to allow for program output, and once
177    /// the screen is disabled, the original buffer is restored.
178    ///
179    /// # Panics
180    ///
181    /// Panics in the case of a terminal error.
182    pub fn alternate_screen(&mut self, on: bool) {
183        if on {
184            self.stdout.execute(terminal::EnterAlternateScreen).unwrap();
185        } else {
186            self.stdout.execute(terminal::LeaveAlternateScreen).unwrap();
187        }
188    }
189
190    /// Prints the given `Page` onto the screen, respecting the paddings and word wrapping.
191    ///
192    /// Calls `Page::align()` on the given `Page` to apply the word wrapping before
193    /// printing it to the output.
194    pub fn print(&mut self, page: &mut Page) {
195        let mut starting_line = self.paddings.buffer.y - 1;
196        self.move_cursor(starting_line, 0);
197        page.align(&self);
198
199        for index in 0..page.text().len() {
200            let string = &page.text()[index];
201            if index >= self.dimensions.y as usize - 1 {
202                continue;
203            }
204            starting_line += 1;
205            self.move_cursor(self.paddings.buffer.x / 2, starting_line);
206            print!("{string}");
207        }
208    }
209
210    /// Returns the last stored position of the cursor.
211    pub fn position(&self) -> &Vector2<u16> {
212        &self.position
213    }
214
215    /// Returns the current cursor position.
216    fn get_position() -> Vector2<u16> {
217        let (pos_x, pos_y) = cursor::position().unwrap();
218        Vector2::new(pos_x, pos_y)
219    }
220
221    /// Updates the stored cursor position to the current one.
222    fn update_position(&mut self) {
223        self.position = Self::get_position();
224    }
225
226    /// Returns the last stored dimensions of the terminal.
227    pub fn dimensions(&self) -> &Vector2<u16> {
228        &self.dimensions
229    }
230
231    /// Returns the current terminal dimensions.
232    ///
233    /// # Panics
234    ///
235    /// Panics in the case of a terminal error.
236    fn get_dimensions() -> Vector2<u16> {
237        let (dim_x, dim_y) = terminal::size().unwrap();
238        Vector2::new(dim_x, dim_y)
239    }
240
241    /// Updates the stored dimensions of the terminal.
242    fn update_dimensions(&mut self) {
243        self.dimensions = Self::get_dimensions();
244    }
245
246    /// Returns the current paddings.
247    pub fn paddings(&self) -> &ChaosPaddings {
248        &self.paddings
249    }
250
251    /// Updates the paddings to new values. Any active page must be printed again to take effect.
252    pub fn update_paddings(&mut self, padding: PaddingType, new_padding: Vector2<u16>) {
253        match padding {
254            PaddingType::Input => self.paddings.input = new_padding,
255            PaddingType::Buffer => self.paddings.buffer = new_padding,
256        }
257    }
258
259    #[cfg(feature = "test")]
260    pub fn test_setup(options: ChaosTestOptions<'a>) -> Self {
261        Self {
262            stdout: options.stdout,
263            input_label: options.input_label,
264            dimensions: options.dimensions,
265            position: options.position,
266            paddings: ChaosPaddings {
267                input: options.input_padding,
268                buffer: options.buffer_padding,
269            },
270        }
271    }
272}
273
274/// A helper struct to set some options for a [`Chaos`] instance.
275///
276/// # Examples
277///
278/// ```
279/// use chaos_engine::{ChaosOptions, types::Vector2};
280///
281/// let options = ChaosOptions {
282///     input_label: "Input:", // The input label
283///     input_padding: Vector2::new(1, 1), // Input paddings (bottom line where input is written)
284///     buffer_padding: Vector2::new(4, 2), // Buffer paddings (main text output area)
285/// };
286/// ```
287pub struct ChaosOptions<'a> {
288    pub input_padding: Vector2<u16>,
289    pub buffer_padding: Vector2<u16>,
290    pub input_label: &'a str,
291}
292
293impl<'a> Default for ChaosOptions<'a> {
294    fn default() -> Self {
295        ChaosOptions {
296            input_label: "Input:",
297            input_padding: Vector2::new(1, 0),
298            buffer_padding: Vector2::new(8, 2),
299        }
300    }
301}
302
303#[cfg(feature = "test")]
304pub struct ChaosTestOptions<'a> {
305    pub stdout: std::io::Stdout,
306    pub input_label: &'a str,
307    pub dimensions: Vector2<u16>,
308    pub position: Vector2<u16>,
309    pub input_padding: Vector2<u16>,
310    pub buffer_padding: Vector2<u16>,
311}
312
313/// A struct that stores paddings for the input and buffer sections of the terminal.
314pub struct ChaosPaddings {
315    pub input: Vector2<u16>,
316    pub buffer: Vector2<u16>,
317}
318
319/// An enum containing all possible types of paddings.
320pub enum PaddingType {
321    Input,
322    Buffer,
323}