cool_rust_input/
lib.rs

1use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
2use crossterm::{
3    cursor, execute, queue,
4    style::ResetColor,
5    terminal::{self, disable_raw_mode, enable_raw_mode},
6};
7use std::cmp;
8use std::io::{self, stdout, Write};
9
10// Get slice of string by starting character index and end character index
11fn get_slice_of_string(text: &str, start: usize, end: usize) -> String {
12    let mut new_text = String::new();
13    let length = text.chars().count();
14
15    for i in start..end {
16        if i >= length {
17            break;
18        }
19        new_text.insert(
20            new_text.len(),
21            text.chars().nth(i).expect("Char at pos should exist"),
22        );
23    }
24    new_text
25}
26
27/// Helper function to draw text to the screen by a coordinate
28pub fn set_terminal_line(
29    text: &str,
30    x: usize,
31    y: usize,
32    overwrite: bool,
33) -> Result<(), std::io::Error> {
34    if overwrite {
35        queue!(
36            stdout(),
37            cursor::MoveTo(x as u16, y as u16),
38            terminal::Clear(terminal::ClearType::CurrentLine)
39        )?;
40        print!("{text}");
41    } else {
42        queue!(stdout(), cursor::MoveTo(x as u16, y as u16))?;
43        print!("{text}");
44    }
45    Ok(())
46}
47
48/// A basic default input handler that implements all default functions of the [CustomInputHandler] trait.
49pub struct DefaultInputHandler;
50impl CustomInputHandler for DefaultInputHandler {}
51
52/// Returned by [CustomInputHandler's handle_key_press](CustomInputHandler::handle_key_press) to signal how the key event should be handled.
53pub enum KeyPressResult {
54    /// Tells the input that this event has been handled, and shouldn't be further processed.
55    Handled,
56    /// Tells the input to stop, like it is finished / submitted.
57    Stop,
58    /// Continue handling event as normal.
59    Continue,
60}
61
62/// Context given to [CustomInputHandler]
63pub struct HandlerContext<'a> {
64    pub text_data: &'a mut TextInputData,
65    pub terminal_size: &'a (u16, u16),
66}
67
68/// Struct for size and offset of an [input](CoolInput)
69pub struct InputTransform {
70    pub size: (u16, u16),
71    pub offset: (u16, u16),
72}
73
74/// Trait that allows custom implementations / behaviour of an [input](CoolInput)
75#[allow(unused_variables)]
76pub trait CustomInputHandler {
77    /// Called before handling of every key press.
78    fn handle_key_press(&mut self, key: &Event, ctx: HandlerContext) -> KeyPressResult {
79        if let Event::Key(key_event) = key {
80            if key_event.kind == KeyEventKind::Press {
81                // Make pressing Escape stop the input
82                if let KeyCode::Esc = key_event.code {
83                    return KeyPressResult::Stop;
84                }
85
86                // Make CTRL + C also stop
87                if let KeyCode::Char(c) = key_event.code {
88                    if c == 'c' && key_event.modifiers.contains(KeyModifiers::CONTROL) {
89                        return KeyPressResult::Stop;
90                    }
91                }
92            }
93        }
94        KeyPressResult::Continue
95    }
96    /// Called before the user's text input is drawn. Here you can ex. change color of the inputted text
97    fn before_draw_text(&mut self, ctx: HandlerContext) {
98        let _ = queue!(stdout(), ResetColor);
99    }
100    /// Called after the user's text is drawn. Here you can ex. draw other text like information or a title of the document.
101    fn after_draw_text(&mut self, ctx: HandlerContext) {}
102    /// Called after the cursor is updated/drawn. Here you can ex. disable cursor blinking or hide it all together
103    fn after_update_cursor(&mut self, ctx: HandlerContext) {}
104    /// Called by the parent [input](CoolInput) to get the input area's size and offset (in a [InputTransform]).
105    fn get_input_transform(&mut self, ctx: HandlerContext) -> InputTransform {
106        let size = *ctx.terminal_size;
107        let offset = (0, 0);
108        InputTransform { size, offset }
109    }
110}
111
112/// Handles key presses, writing text, and moving the cursor
113pub struct TextInputData {
114    pub text: String,
115    pub cursor_x: usize,
116    pub cursor_y: usize,
117    pub tab_width: usize,
118}
119
120/// The main input type. Uses a custom input handler (a struct which implements [CustomInputHandler])
121pub struct CoolInput<H: CustomInputHandler> {
122    pub text_data: TextInputData,
123    pub scroll_x: usize,
124    pub scroll_y: usize,
125    pub listening: bool,
126    pub custom_input: H,
127}
128
129impl TextInputData {
130    pub fn write_char(&mut self, c: char) -> Result<(), std::io::Error> {
131        self.insert_char(c, self.cursor_x, self.cursor_y);
132        self.move_cursor_right()?;
133        Ok(())
134    }
135    pub fn insert_char(&mut self, c: char, x: usize, y: usize) {
136        let mut new = String::new();
137        let mut cur_x = 0;
138        let mut cur_y = 0;
139
140        if x == 0 && y == 0 {
141            self.text.insert(0, c);
142        } else {
143            for char in self.text.chars() {
144                cur_x += 1;
145                if char == '\n' {
146                    cur_y += 1;
147                    cur_x = 0;
148                }
149                new.insert(new.len(), char);
150                if cur_x == x && cur_y == y {
151                    new.insert(new.len(), c);
152                }
153            }
154            self.text = new;
155        }
156    }
157    pub fn remove_character(&mut self, x: usize, y: usize) -> Result<(), std::io::Error> {
158        let mut new = String::new();
159        let mut cur_x = 0;
160        let mut cur_y = 0;
161
162        if x == 0 {
163            self.move_cursor_up()?;
164            self.cursor_x = self.get_current_line_length()?;
165        } else {
166            self.move_cursor_left()?;
167        }
168
169        if !self.text.is_empty() {
170            for char in self.text.chars() {
171                cur_x += 1;
172                if char == '\n' {
173                    cur_y += 1;
174                    cur_x = 0;
175                }
176                if cur_x != x || cur_y != y {
177                    new.insert(new.len(), char);
178                }
179            }
180        }
181        self.text = new;
182        Ok(())
183    }
184    fn move_cursor_end(&mut self) -> Result<(), std::io::Error> {
185        if self.get_amt_lines() > 0 {
186            self.cursor_x = self.get_current_line_length()?;
187        }
188        Ok(())
189    }
190    fn move_cursor_up(&mut self) -> Result<(), std::io::Error> {
191        if self.cursor_y > 0 {
192            self.cursor_y -= 1;
193            self.cursor_x = cmp::min(self.get_current_line_length()?, self.cursor_x);
194        } else {
195            self.cursor_x = 0;
196        }
197        Ok(())
198    }
199    fn move_cursor_down(&mut self) -> Result<(), std::io::Error> {
200        if self.cursor_y < self.get_amt_lines() - 1 {
201            self.cursor_y += 1;
202            self.cursor_x = cmp::min(self.get_current_line_length()?, self.cursor_x);
203        } else {
204            self.move_cursor_end()?;
205        }
206        Ok(())
207    }
208    fn move_cursor_left(&mut self) -> Result<(), std::io::Error> {
209        if self.cursor_x > 0 || self.cursor_y != 0 {
210            if self.cursor_x > 0 {
211                self.cursor_x -= 1;
212            } else {
213                self.cursor_y -= 1;
214                self.cursor_x = self.get_current_line_length()?;
215            }
216        }
217        Ok(())
218    }
219    fn move_cursor_right(&mut self) -> Result<(), std::io::Error> {
220        if self.cursor_y != self.get_amt_lines() - 1
221            || self.cursor_x < self.get_current_line_length()?
222        {
223            if self.cursor_x != self.get_current_line_length()? {
224                self.cursor_x += 1;
225            } else {
226                self.cursor_y += 1;
227                self.cursor_x = 0;
228            }
229        }
230
231        Ok(())
232    }
233    pub fn get_amt_lines(&mut self) -> usize {
234        let mut amt = self.text.lines().count();
235        if self.text.ends_with("\n") {
236            amt += 1;
237        }
238        amt
239    }
240    pub fn get_line_at(&mut self, y: usize) -> Option<&str> {
241        if self.text.ends_with("\n") && y == self.text.lines().count() {
242            return Some("");
243        }
244        self.text.lines().nth(y)
245    }
246    pub fn get_current_line_length(&mut self) -> Result<usize, std::io::Error> {
247        let line = self.get_line_at(self.cursor_y);
248        match line {
249            Some(text) => Ok(text.chars().count()),
250            None => Err(std::io::Error::new(
251                io::ErrorKind::Other,
252                "Couldn't get length of current line because it doesn't exist.",
253            )),
254        }
255    }
256    fn handle_key_press(&mut self, key_event: KeyEvent) -> Result<(), std::io::Error> {
257        match key_event.code {
258            KeyCode::Char(c) => {
259                self.insert_char(c, self.cursor_x, self.cursor_y);
260                self.move_cursor_right()?;
261            }
262            KeyCode::Enter => {
263                self.insert_char('\n', self.cursor_x, self.cursor_y);
264                self.cursor_y += 1;
265                self.cursor_x = 0;
266            }
267            KeyCode::Backspace => {
268                if self.cursor_x > 0 || self.cursor_y != 0 {
269                    self.remove_character(self.cursor_x, self.cursor_y)?;
270                }
271            }
272            KeyCode::Tab => {
273                for _ in 0..self.tab_width {
274                    self.insert_char(' ', self.cursor_x, self.cursor_y);
275                }
276                self.cursor_x += self.tab_width;
277            }
278            KeyCode::Delete => {
279                if self.get_amt_lines() > 0 {
280                    let line_length = self.get_current_line_length()?;
281                    if self.cursor_x < line_length || self.cursor_y != self.get_amt_lines() - 1 {
282                        if self.cursor_x == line_length {
283                            self.cursor_x = 0;
284                            self.cursor_y += 1;
285                        } else {
286                            self.cursor_x += 1;
287                        }
288                        self.remove_character(self.cursor_x, self.cursor_y)?;
289                    }
290                }
291            }
292            KeyCode::Up => {
293                self.move_cursor_up()?;
294            }
295            KeyCode::Down => {
296                if self.get_amt_lines() > 0 {
297                    self.move_cursor_down()?;
298                }
299            }
300            KeyCode::Left => {
301                self.move_cursor_left()?;
302            }
303            KeyCode::Right if self.get_amt_lines() > 0 => {
304                self.move_cursor_right()?;
305            }
306            KeyCode::Home => {
307                self.cursor_x = 0;
308            }
309            KeyCode::End => {
310                self.move_cursor_end()?;
311            }
312            _ => {}
313        }
314        Ok(())
315    }
316}
317
318impl<H: CustomInputHandler> CoolInput<H> {
319    pub fn new(handler: H, tab_width: usize) -> Self {
320        CoolInput {
321            text_data: TextInputData {
322                text: String::new(),
323                cursor_x: 0,
324                cursor_y: 0,
325                tab_width,
326            },
327            listening: false,
328            scroll_x: 0,
329            scroll_y: 0,
330            custom_input: handler,
331        }
332    }
333    /// Get the size of the terminal running the program
334    pub fn get_terminal_size(&mut self) -> Result<(u16, u16), std::io::Error> {
335        let mut terminal_size = terminal::size()?;
336        terminal_size.1 -= 1;
337        Ok(terminal_size)
338    }
339    pub fn get_input_transform(&mut self) -> Result<InputTransform, std::io::Error> {
340        let terminal_size = self.get_terminal_size()?;
341        let input_transform = self.custom_input.get_input_transform(HandlerContext {
342            text_data: &mut self.text_data,
343            terminal_size: &terminal_size,
344        });
345        let mut size = input_transform.size;
346        let offset = input_transform.offset;
347        if size.0 + offset.0 > terminal_size.0 {
348            size.0 = terminal_size.0.saturating_sub(offset.0);
349        }
350        if size.1 + offset.1 > terminal_size.1 {
351            size.1 = terminal_size.1.saturating_sub(offset.1);
352        }
353        Ok(InputTransform { size, offset })
354    }
355    /// Render all text and update cursor
356    pub fn render(&mut self) -> Result<(), std::io::Error> {
357        self.update_text()?;
358        self.update_cursor()?;
359        io::stdout().flush()?;
360        Ok(())
361    }
362    fn update_cursor(&mut self) -> Result<(), std::io::Error> {
363        let terminal_size = self.get_terminal_size()?;
364        let input_transform = self.get_input_transform()?;
365
366        let x =
367            self.text_data.cursor_x as i16 + input_transform.offset.0 as i16 - self.scroll_x as i16;
368        let x: u16 = cmp::max(x, 0_i16) as u16;
369        let x = cmp::min(x, input_transform.offset.0 + input_transform.size.0);
370        let target_y = (self.text_data.cursor_y as u16) + input_transform.offset.1;
371        let target_y = target_y.saturating_sub(self.scroll_y as u16);
372        let y = cmp::min(
373            cmp::min(
374                target_y,
375                input_transform.offset.1 + input_transform.size.1 - 1,
376            ),
377            terminal_size.1 - 1,
378        );
379        queue!(stdout(), cursor::Show)?;
380        queue!(stdout(), cursor::MoveTo(x, y))?;
381
382        self.custom_input.after_update_cursor(HandlerContext {
383            text_data: &mut self.text_data,
384            terminal_size: &terminal_size,
385        });
386        Ok(())
387    }
388    fn update_text(&mut self) -> Result<(), std::io::Error> {
389        let terminal_size = self.get_terminal_size()?;
390        let input_transform = self.get_input_transform()?;
391
392        self.custom_input.before_draw_text(HandlerContext {
393            text_data: &mut self.text_data,
394            terminal_size: &terminal_size,
395        });
396
397        let offset_y = input_transform.offset.1 as i16;
398        for y in offset_y..offset_y + (input_transform.size.1 as i16) {
399            let y_line_index = y - offset_y + (self.scroll_y as i16);
400            if y_line_index >= 0 && y_line_index < (self.text_data.text.lines().count() as i16) {
401                if let Some(line) = self.text_data.get_line_at(y_line_index as usize) {
402                    let text = get_slice_of_string(
403                        line,
404                        self.scroll_x,
405                        self.scroll_x + input_transform.size.0 as usize,
406                    );
407                    set_terminal_line(&text, input_transform.offset.0 as usize, y as usize, true)?;
408                }
409            } else {
410                set_terminal_line("", input_transform.offset.0 as usize, y as usize, true)?;
411            }
412        }
413
414        self.custom_input.after_draw_text(HandlerContext {
415            text_data: &mut self.text_data,
416            terminal_size: &terminal_size,
417        });
418
419        Ok(())
420    }
421    fn scroll_in_view(
422        &mut self,
423        moving_right: bool,
424        moving_down: bool,
425    ) -> Result<(), std::io::Error> {
426        let input_transform = self.get_input_transform()?;
427        self.scroll_x = self.keep_scroll_axis_in_view(
428            self.scroll_x,
429            self.text_data.cursor_x,
430            input_transform.size.0 as usize,
431            moving_right,
432        )?;
433        self.scroll_y = self.keep_scroll_axis_in_view(
434            self.scroll_y,
435            self.text_data.cursor_y,
436            input_transform.size.1 as usize,
437            moving_down,
438        )?;
439        Ok(())
440    }
441    fn keep_scroll_axis_in_view(
442        &mut self,
443        scroll_amt: usize,
444        cursor_pos: usize,
445        bounds: usize,
446        moving_direction: bool,
447    ) -> Result<usize, std::io::Error> {
448        let mut scroll_amt = scroll_amt;
449        if moving_direction {
450            if cursor_pos > bounds - 1 {
451                scroll_amt = cmp::max(scroll_amt, cursor_pos - bounds + 1);
452            }
453        } else if cursor_pos < scroll_amt {
454            scroll_amt = cursor_pos;
455        }
456        Ok(scroll_amt)
457    }
458    /// Handle a key event
459    pub fn handle_key_press(&mut self, key: Event) -> Result<(), std::io::Error> {
460        let terminal_size = self.get_terminal_size()?;
461        let old_cursor_x = self.text_data.cursor_x;
462        let old_cursor_y = self.text_data.cursor_y;
463        match self.custom_input.handle_key_press(
464            &key,
465            HandlerContext {
466                text_data: &mut self.text_data,
467                terminal_size: &terminal_size,
468            },
469        ) {
470            KeyPressResult::Handled => {
471                self.scroll_in_view(
472                    self.text_data.cursor_x > old_cursor_x,
473                    self.text_data.cursor_y > old_cursor_y,
474                )?;
475                self.render()?;
476                return Ok(());
477            }
478            KeyPressResult::Stop => {
479                self.listening = false;
480                return Ok(());
481            }
482            KeyPressResult::Continue => {
483                if let Event::Key(key_event) = key {
484                    if key_event.kind == KeyEventKind::Press {
485                        self.text_data.handle_key_press(key_event)?;
486                        self.scroll_in_view(
487                            self.text_data.cursor_x > old_cursor_x,
488                            self.text_data.cursor_y > old_cursor_y,
489                        )?;
490                        self.render()?;
491                    }
492                }
493            }
494        }
495        Ok(())
496    }
497    /// Start listening for key presses without preparing the terminal
498    pub fn listen_quiet(&mut self) -> Result<(), std::io::Error> {
499        self.listening = true;
500        while self.listening {
501            self.handle_key_press(event::read()?)?;
502        }
503        Ok(())
504    }
505    /// Prepare the terminal for input
506    pub fn pre_listen(&mut self) -> Result<(), std::io::Error> {
507        let input_transform = self.get_input_transform()?;
508        enable_raw_mode()?;
509        execute!(
510            stdout(),
511            terminal::Clear(terminal::ClearType::All),
512            cursor::MoveTo(
513                (self.text_data.cursor_x as u16) + input_transform.offset.0,
514                (self.text_data.cursor_y as u16) + input_transform.offset.1
515            )
516        )?;
517        Ok(())
518    }
519    /// Restore the terminal after input is finished.
520    pub fn post_listen(&mut self) -> Result<(), std::io::Error> {
521        execute!(
522            stdout(),
523            ResetColor,
524            terminal::Clear(terminal::ClearType::All),
525            cursor::MoveTo(0, 0),
526            cursor::Show,
527        )?;
528        disable_raw_mode()?;
529        Ok(())
530    }
531    /// Prepare terminal and start to listen for key presses until finished.
532    pub fn listen(&mut self) -> Result<(), std::io::Error> {
533        self.pre_listen()?;
534        self.render()?;
535        self.listen_quiet()?;
536        self.post_listen()?;
537        Ok(())
538    }
539}