requestty_ui/
string_input.rs

1use std::{
2    io::{self, Write},
3    ops::Range,
4};
5
6use unicode_segmentation::UnicodeSegmentation;
7
8use crate::{
9    backend::Backend,
10    events::{KeyCode, KeyEvent, KeyModifiers, Movement},
11    layout::Layout,
12};
13
14/// A widget that inputs a string.
15///
16/// A `filter_map` function can optionally be provided to limit and change the characters allowed,
17/// similar to [`Iterator::filter_map`].
18///
19/// If only a single character is required, use [`CharInput`].
20///
21/// [`CharInput`]: crate::widgets::CharInput
22#[derive(Debug, Clone)]
23pub struct StringInput<F = super::widgets::FilterMapChar> {
24    value: String,
25    mask: Option<char>,
26    hide_output: bool,
27    /// The character length of the string
28    value_len: usize,
29    /// The position of the 'cursor' in characters
30    at: usize,
31    filter_map: F,
32}
33
34impl StringInput {
35    /// Creates a new [`StringInput`] which accepts all characters.
36    pub fn new() -> Self {
37        Self::with_filter_map(crate::widgets::no_filter)
38    }
39}
40
41impl<F> StringInput<F> {
42    /// Creates a new [`StringInput`] which only accepts characters as per the `filter_map` function.
43    pub fn with_filter_map(filter_map: F) -> Self {
44        Self {
45            value: String::new(),
46            value_len: 0,
47            at: 0,
48            filter_map,
49            mask: None,
50            hide_output: false,
51        }
52    }
53
54    /// A mask to render instead of the actual characters.
55    ///
56    /// This is useful for passwords.
57    pub fn mask(mut self, mask: char) -> Self {
58        self.mask = Some(mask);
59        self
60    }
61
62    /// Hide the value being entered, and render nothing.
63    ///
64    /// This is useful for passwords.
65    pub fn hide_output(mut self) -> Self {
66        self.hide_output = true;
67        self
68    }
69
70    /// A helper that sets mask if mask is some, otherwise hides the output
71    pub fn password(self, mask: Option<char>) -> Self {
72        match mask {
73            Some(mask) => self.mask(mask),
74            None => self.hide_output(),
75        }
76    }
77
78    /// Gets the location of the 'cursor' in characters.
79    pub fn get_at(&self) -> usize {
80        self.at
81    }
82
83    /// Sets the location of the 'cursor' in characters.
84    pub fn set_at(&mut self, at: usize) {
85        self.at = at.min(self.value_len);
86    }
87
88    /// The value of the `StringInput`
89    pub fn value(&self) -> &str {
90        &self.value
91    }
92
93    /// Sets the value
94    pub fn set_value(&mut self, value: String) {
95        self.value_len = value.chars().count();
96        self.value = value;
97        self.set_at(self.at);
98    }
99
100    /// Replaces the value with the result of the function
101    pub fn replace_with<W: FnOnce(String) -> String>(&mut self, with: W) {
102        self.value = with(std::mem::take(&mut self.value));
103        let old_len = self.value_len;
104        self.value_len = self.value.chars().count();
105        if self.at == old_len {
106            self.at = self.value_len;
107        } else {
108            self.set_at(self.at);
109        }
110    }
111
112    /// Returns the inputted string
113    pub fn finish(self) -> String {
114        self.value
115    }
116
117    /// Gets the byte index of a given char index
118    fn get_byte_i(&self, index: usize) -> usize {
119        self.value
120            .char_indices()
121            .nth(index)
122            .map(|(i, _)| i)
123            .unwrap_or_else(|| self.value.len())
124    }
125
126    /// Gets the char index of a given byte index
127    fn get_char_i(&self, byte_i: usize) -> usize {
128        self.value
129            .char_indices()
130            .position(|(i, _)| i == byte_i)
131            .unwrap_or_else(|| self.value.char_indices().count())
132    }
133
134    /// Get the word bound iterator for a given range
135    fn word_iter(&self, r: Range<usize>) -> impl DoubleEndedIterator<Item = (usize, &str)> {
136        self.value[r]
137            .split_word_bound_indices()
138            .filter(|(_, s)| !s.chars().next().map(char::is_whitespace).unwrap_or(true))
139    }
140
141    /// Returns the byte index of the start of the first word to the left (< byte_i)
142    fn find_word_left(&self, byte_i: usize) -> usize {
143        self.word_iter(0..byte_i)
144            .next_back()
145            .map(|(new_byte_i, _)| new_byte_i)
146            .unwrap_or(0)
147    }
148
149    /// Returns the byte index of the start of the first word to the right (> byte_i)
150    fn find_word_right(&self, byte_i: usize) -> usize {
151        self.word_iter(byte_i..self.value.len())
152            .nth(1)
153            .map(|(new_byte_i, _)| new_byte_i + byte_i)
154            .unwrap_or_else(|| self.value.len())
155    }
156
157    fn get_delete_movement(&self, key: KeyEvent) -> Option<Movement> {
158        let mov = match key.code {
159            KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => Movement::Home,
160            KeyCode::Backspace if key.modifiers.contains(KeyModifiers::ALT) => Movement::PrevWord,
161            KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::ALT) => Movement::PrevWord,
162            KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => Movement::Left,
163            KeyCode::Backspace => Movement::Left,
164
165            KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => Movement::End,
166
167            KeyCode::Delete if key.modifiers.contains(KeyModifiers::ALT) => Movement::NextWord,
168            KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::ALT) => Movement::NextWord,
169            KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => Movement::Right,
170            KeyCode::Delete => Movement::Right,
171
172            _ => return None,
173        };
174
175        match mov {
176            Movement::Home | Movement::PrevWord | Movement::Left if self.at != 0 => Some(mov),
177            Movement::End | Movement::NextWord | Movement::Right if self.at != self.value_len => {
178                Some(mov)
179            }
180            _ => None,
181        }
182    }
183}
184
185impl<F> super::Widget for StringInput<F>
186where
187    F: Fn(char) -> Option<char>,
188{
189    fn handle_key(&mut self, key: KeyEvent) -> bool {
190        if let Some(movement) = self.get_delete_movement(key) {
191            match movement {
192                Movement::Home => {
193                    let byte_i = self.get_byte_i(self.at);
194                    self.value_len -= self.at;
195                    self.at = 0;
196                    self.value.replace_range(..byte_i, "");
197                    return true;
198                }
199                Movement::PrevWord => {
200                    let was_at = self.at;
201                    let byte_i = self.get_byte_i(self.at);
202                    let prev_word = self.find_word_left(byte_i);
203                    self.at = self.get_char_i(prev_word);
204                    self.value_len -= was_at - self.at;
205                    self.value.replace_range(prev_word..byte_i, "");
206                    return true;
207                }
208                Movement::Left if self.at == self.value_len => {
209                    self.at -= 1;
210                    self.value_len -= 1;
211                    self.value.pop();
212                    return true;
213                }
214                Movement::Left => {
215                    self.at -= 1;
216                    let byte_i = self.get_byte_i(self.at);
217                    self.value_len -= 1;
218                    self.value.remove(byte_i);
219                    return true;
220                }
221
222                Movement::End => {
223                    let byte_i = self.get_byte_i(self.at);
224                    self.value_len = self.at;
225                    self.value.truncate(byte_i);
226                    return true;
227                }
228                Movement::NextWord => {
229                    let byte_i = self.get_byte_i(self.at);
230                    let next_word = self.find_word_right(byte_i);
231                    self.value_len -= self.get_char_i(next_word) - self.at;
232                    self.value.replace_range(byte_i..next_word, "");
233                    return true;
234                }
235                Movement::Right if self.at == self.value_len - 1 => {
236                    self.value_len -= 1;
237                    self.value.pop();
238                    return true;
239                }
240                Movement::Right => {
241                    let byte_i = self.get_byte_i(self.at);
242                    self.value_len -= 1;
243                    self.value.remove(byte_i);
244                    return true;
245                }
246
247                _ => {}
248            }
249        }
250
251        match key.code {
252            // FIXME: all chars with ctrl and alt are ignored, even though only some
253            // need to be ignored
254            KeyCode::Char(c)
255                if !key
256                    .modifiers
257                    .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
258            {
259                if let Some(c) = (self.filter_map)(c) {
260                    if self.at == self.value_len {
261                        self.value.push(c);
262                    } else {
263                        let byte_i = self.get_byte_i(self.at);
264                        self.value.insert(byte_i, c);
265                    };
266
267                    self.at += 1;
268                    self.value_len += 1;
269                    return true;
270                }
271            }
272
273            _ => {}
274        }
275
276        match Movement::try_from_key(key) {
277            Some(Movement::PrevWord) if self.at != 0 => {
278                self.at = self.get_char_i(self.find_word_left(self.get_byte_i(self.at)));
279            }
280            Some(Movement::Left) if self.at != 0 => {
281                self.at -= 1;
282            }
283
284            Some(Movement::NextWord) if self.at != self.value_len => {
285                self.at = self.get_char_i(self.find_word_right(self.get_byte_i(self.at)));
286            }
287            Some(Movement::Right) if self.at != self.value_len => {
288                self.at += 1;
289            }
290
291            Some(Movement::Home) if self.at != 0 => {
292                self.at = 0;
293            }
294            Some(Movement::End) if self.at != self.value_len => {
295                self.at = self.value_len;
296            }
297            _ => return false,
298        }
299
300        true
301    }
302
303    /// This widget ignores [`layout.offset_x`] and wraps around in the terminal.
304    ///
305    /// [`layout.offset_x`]: Layout.offset_x
306    fn render<B: Backend>(&mut self, layout: &mut Layout, backend: &mut B) -> io::Result<()> {
307        if self.hide_output {
308            return Ok(());
309        }
310
311        if let Some(mask) = self.mask {
312            print_mask(self.value_len, mask, backend)?;
313        } else {
314            // Terminal takes care of wrapping in case of large strings
315            backend.write_all(self.value.as_bytes())?;
316        }
317
318        // Adjust layout
319        self.height(layout);
320
321        Ok(())
322    }
323
324    fn height(&mut self, layout: &mut Layout) -> u16 {
325        if self.hide_output {
326            return 1;
327        }
328
329        let mut width = textwrap::core::display_width(&self.value) as u16;
330
331        if width > layout.line_width() {
332            width -= layout.line_width();
333
334            layout.line_offset = width % layout.width;
335            layout.offset_y += 1 + width / layout.width;
336
337            2 + width / layout.width
338        } else {
339            layout.line_offset += width;
340            1
341        }
342    }
343
344    fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
345        let display_at =
346            textwrap::core::display_width(&self.value[..self.get_byte_i(self.at)]) as u16;
347
348        let relative_pos = if self.hide_output {
349            // Nothing will be outputted so no need to move the cursor
350            (layout.line_offset, 0)
351        } else if layout.line_width() > display_at {
352            // It is in the same line as the prompt
353            (layout.line_offset + display_at, 0)
354        } else {
355            let at = display_at - layout.line_width();
356
357            (at % layout.width, 1 + at / layout.width)
358        };
359
360        layout.offset_cursor(relative_pos)
361    }
362}
363
364impl Default for StringInput {
365    fn default() -> Self {
366        Self::new()
367    }
368}
369
370fn print_mask<W: Write>(len: usize, mask: char, w: &mut W) -> io::Result<()> {
371    let mut buf = [0; 4];
372    let mask = mask.encode_utf8(&mut buf[..]);
373
374    for _ in 0..len {
375        w.write_all(mask.as_bytes())?;
376    }
377
378    Ok(())
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::{backend::TestBackend, events::KeyModifiers, test_consts::*, Widget};
385
386    #[test]
387    fn test_print_mask() {
388        fn test(mask: char) {
389            let mut buf = [0u8; 100];
390            let mask_len = mask.len_utf8();
391            print_mask(25, mask, &mut &mut buf[..]).unwrap();
392            assert!(std::str::from_utf8(&buf[0..(25 * mask_len)])
393                .unwrap()
394                .chars()
395                .all(|c| c == mask));
396            assert!(buf[(25 * mask_len)..].iter().all(|&b| b == 0));
397        }
398
399        test('*');
400        test('‣');
401    }
402
403    #[test]
404    fn test_delete_movement() {
405        let mut input = StringInput::default();
406
407        let backspace_movements = [
408            (
409                KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
410                Movement::Home,
411            ),
412            (
413                KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT),
414                Movement::PrevWord,
415            ),
416            (
417                KeyEvent::new(KeyCode::Char('w'), KeyModifiers::ALT),
418                Movement::PrevWord,
419            ),
420            (
421                KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL),
422                Movement::Left,
423            ),
424            (
425                KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()),
426                Movement::Left,
427            ),
428        ];
429
430        let delete_movements = [
431            (
432                KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL),
433                Movement::End,
434            ),
435            (
436                KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT),
437                Movement::NextWord,
438            ),
439            (
440                KeyEvent::new(KeyCode::Char('d'), KeyModifiers::ALT),
441                Movement::NextWord,
442            ),
443            (
444                KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
445                Movement::Right,
446            ),
447            (
448                KeyEvent::new(KeyCode::Delete, KeyModifiers::empty()),
449                Movement::Right,
450            ),
451        ];
452
453        assert!(backspace_movements
454            .iter()
455            .copied()
456            .all(|(key, _)| input.get_delete_movement(key).is_none()));
457
458        assert!(delete_movements
459            .iter()
460            .copied()
461            .all(|(key, _)| input.get_delete_movement(key).is_none()));
462
463        input.set_value("Hello world".into());
464
465        assert!(backspace_movements
466            .iter()
467            .copied()
468            .all(|(key, _)| input.get_delete_movement(key).is_none()));
469        assert!(delete_movements
470            .iter()
471            .copied()
472            .all(|(key, mov)| input.get_delete_movement(key).unwrap() == mov));
473
474        input.set_at(11);
475
476        assert!(backspace_movements
477            .iter()
478            .copied()
479            .all(|(key, mov)| input.get_delete_movement(key).unwrap() == mov));
480        assert!(delete_movements
481            .iter()
482            .copied()
483            .all(|(key, _)| input.get_delete_movement(key).is_none()));
484
485        input.set_at(5);
486
487        assert!(backspace_movements
488            .iter()
489            .copied()
490            .all(|(key, mov)| input.get_delete_movement(key).unwrap() == mov));
491        assert!(delete_movements
492            .iter()
493            .copied()
494            .all(|(key, mov)| input.get_delete_movement(key).unwrap() == mov));
495    }
496
497    #[test]
498    fn test_render() {
499        fn test(text: &str, line_offset: u16, offset_y: u16) {
500            let size = (100, 20).into();
501            let mut layout = Layout::new(0, size);
502
503            let mut backend = TestBackend::new(size);
504            let mut input = StringInput::default();
505            input.set_value(text.into());
506            input.render(&mut layout, &mut backend).unwrap();
507
508            crate::assert_backend_snapshot!(backend);
509
510            assert_eq!(
511                layout,
512                Layout::new(0, size)
513                    .with_line_offset(line_offset)
514                    .with_offset(0, offset_y)
515            );
516        }
517
518        test("Hello, World!", 13, 0);
519        test(LOREM, 70, 4);
520        test(UNICODE, 70, 4);
521    }
522
523    #[test]
524    fn test_handle_key() {
525        let mut input = StringInput::with_filter_map(|c| if c == 'i' { None } else { Some(c) });
526        input.set_value(LOREM.into());
527        input.set_at(40);
528
529        input.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty()));
530        assert_eq!(input.get_at(), 39);
531        input.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL));
532        assert_eq!(input.get_at(), 28);
533        input.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::empty()));
534        assert_eq!(input.get_at(), 29);
535        input.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL));
536        assert_eq!(input.get_at(), 41);
537        input.handle_key(KeyEvent::new(KeyCode::Home, KeyModifiers::empty()));
538        assert_eq!(input.get_at(), 0);
539        input.handle_key(KeyEvent::new(KeyCode::End, KeyModifiers::empty()));
540        assert_eq!(input.get_at(), 470);
541
542        input.set_at(40);
543        input.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()));
544        assert_eq!(input.get_at(), 39);
545        assert_eq!(input.value().chars().count(), 469);
546        input.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::empty()));
547        assert_eq!(input.get_at(), 40);
548        assert_eq!(input.value(), LOREM);
549
550        input.handle_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
551        assert_eq!(input.get_at(), 0);
552        assert_eq!(input.value().chars().count(), 430);
553        input.set_at(400);
554        input.handle_key(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL));
555        assert_eq!(input.get_at(), 400);
556        assert_eq!(input.value().chars().count(), 400);
557        input.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT));
558        assert_eq!(input.get_at(), 394);
559        assert_eq!(input.value().chars().count(), 394);
560
561        input.set_at(40);
562        input.handle_key(KeyEvent::new(KeyCode::Delete, KeyModifiers::empty()));
563        assert_eq!(input.get_at(), 40);
564        assert_eq!(input.value().chars().count(), 393);
565        input.handle_key(KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT));
566        assert_eq!(input.get_at(), 40);
567        assert_eq!(input.value().chars().count(), 388);
568
569        input.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::empty()));
570        assert_eq!(input.get_at(), 40);
571        assert_eq!(input.value().chars().count(), 388);
572        input.handle_key(KeyEvent::new(KeyCode::Char('I'), KeyModifiers::empty()));
573        assert_eq!(input.get_at(), 41);
574        assert_eq!(input.value().chars().count(), 389);
575
576        let mut input = StringInput::with_filter_map(|c| if c == 'ȼ' { None } else { Some(c) });
577        input.set_value(UNICODE.into());
578        input.set_at(40);
579
580        input.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty()));
581        assert_eq!(input.get_at(), 39);
582        input.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL));
583        assert_eq!(input.get_at(), 31);
584        input.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::empty()));
585        assert_eq!(input.get_at(), 32);
586        input.handle_key(KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL));
587        assert_eq!(input.get_at(), 39);
588        input.handle_key(KeyEvent::new(KeyCode::Home, KeyModifiers::empty()));
589        assert_eq!(input.get_at(), 0);
590        input.handle_key(KeyEvent::new(KeyCode::End, KeyModifiers::empty()));
591        assert_eq!(input.get_at(), 470);
592
593        input.set_at(41);
594        input.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()));
595        assert_eq!(input.get_at(), 40);
596        assert_eq!(input.value().chars().count(), 469);
597        input.handle_key(KeyEvent::new(KeyCode::Char('Æ'), KeyModifiers::empty()));
598        assert_eq!(input.get_at(), 41);
599        assert_eq!(input.value(), UNICODE);
600        input.set_at(40);
601
602        input.handle_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
603        assert_eq!(input.get_at(), 0);
604        assert_eq!(input.value().chars().count(), 430);
605        input.set_at(400);
606        input.handle_key(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL));
607        assert_eq!(input.get_at(), 400);
608        assert_eq!(input.value().chars().count(), 400);
609        input.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT));
610        assert_eq!(input.get_at(), 397);
611        assert_eq!(input.value().chars().count(), 397);
612
613        input.set_at(40);
614        input.handle_key(KeyEvent::new(KeyCode::Delete, KeyModifiers::empty()));
615        assert_eq!(input.get_at(), 40);
616        assert_eq!(input.value().chars().count(), 396);
617        input.handle_key(KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT));
618        assert_eq!(input.get_at(), 40);
619        assert_eq!(input.value().chars().count(), 386);
620
621        input.handle_key(KeyEvent::new(KeyCode::Char('ȼ'), KeyModifiers::empty()));
622        assert_eq!(input.get_at(), 40);
623        assert_eq!(input.value().chars().count(), 386);
624    }
625
626    #[test]
627    fn test_height() {
628        fn test(text: &str, indent: usize, max_width: usize, height: u16) {
629            let mut layout = Layout::new(indent as u16, (max_width as u16, 40).into());
630            let mut input = StringInput::default();
631            input.set_value(text.into());
632            assert_eq!(input.height(&mut layout), height);
633        }
634
635        test("Hello, World!", 0, 100, 1);
636        test("Hello, World!", 0, 7, 2);
637        test("Hello, World!", 2, 7, 3);
638
639        test(LOREM, 0, 100, 5);
640        test(LOREM, 40, 100, 6);
641    }
642
643    #[test]
644    fn test_cursor_pos() {
645        let mut layout = Layout::new(0, (100, 20).into());
646        let mut input = StringInput::default();
647        input.set_value("Hello, World!".into());
648        input.set_at(0);
649        assert_eq!(input.cursor_pos(layout), (0, 0));
650        input.set_at(4);
651        assert_eq!(input.cursor_pos(layout), (4, 0));
652
653        layout.line_offset = 5;
654        assert_eq!(input.cursor_pos(layout), (9, 0));
655        input.set_at(0);
656        assert_eq!(input.cursor_pos(layout), (5, 0));
657
658        layout.line_offset = 0;
659        input.set_value(LOREM.into());
660        assert_eq!(input.cursor_pos(layout), (0, 0));
661        input.set_at(4);
662        assert_eq!(input.cursor_pos(layout), (4, 0));
663
664        layout.line_offset = 5;
665        assert_eq!(input.cursor_pos(layout), (9, 0));
666        input.set_at(0);
667        assert_eq!(input.cursor_pos(layout), (5, 0));
668
669        input.set_at(130);
670        assert_eq!(input.cursor_pos(layout), (35, 1));
671
672        layout.line_offset = 0;
673        input.set_at(0);
674        input.set_value(UNICODE.into());
675        assert_eq!(input.cursor_pos(layout), (0, 0));
676        input.set_at(4);
677        assert_eq!(input.cursor_pos(layout), (4, 0));
678
679        layout.line_offset = 5;
680        assert_eq!(input.cursor_pos(layout), (9, 0));
681        input.set_at(0);
682        assert_eq!(input.cursor_pos(layout), (5, 0));
683
684        input.set_at(130);
685        assert_eq!(input.cursor_pos(layout), (35, 1));
686
687        layout.offset_y = 3;
688        assert_eq!(input.cursor_pos(layout), (35, 4));
689    }
690}