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