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}