cool_rust_input/
lib.rs

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