tui_input/
input.rs

1//! Core logic for handling input.
2//!
3//! # Example: Without any backend
4//!
5//! ```
6//! use tui_input::{Input, InputRequest, StateChanged};
7//!
8//! let mut input: Input = "Hello Worl".into();
9//!
10//! let req = InputRequest::InsertChar('d');
11//! let resp = input.handle(req);
12//!
13//! assert_eq!(resp, Some(StateChanged { value: true, cursor: true }));
14//! assert_eq!(input.cursor(), 11);
15//! assert_eq!(input.to_string(), "Hello World");
16//! ```
17
18/// Input requests are used to change the input state.
19///
20/// Different backends can be used to convert events into requests.
21#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum InputRequest {
24    SetCursor(usize),
25    InsertChar(char),
26    GoToPrevChar,
27    GoToNextChar,
28    GoToPrevWord,
29    GoToNextWord,
30    GoToStart,
31    GoToEnd,
32    DeletePrevChar,
33    DeleteNextChar,
34    DeletePrevWord,
35    DeleteNextWord,
36    DeleteLine,
37    DeleteTillEnd,
38}
39
40#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct StateChanged {
43    pub value: bool,
44    pub cursor: bool,
45}
46
47pub type InputResponse = Option<StateChanged>;
48
49/// The input buffer with cursor support.
50///
51/// Example:
52///
53/// ```
54/// use tui_input::Input;
55///
56/// let input: Input = "Hello World".into();
57///
58/// assert_eq!(input.cursor(), 11);
59/// assert_eq!(input.to_string(), "Hello World");
60/// ```
61#[derive(Default, Debug, Clone)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct Input {
64    value: String,
65    cursor: usize,
66}
67
68impl Input {
69    /// Initialize a new instance with a given value
70    /// Cursor will be set to the given value's length.
71    pub fn new(value: String) -> Self {
72        let len = value.chars().count();
73        Self { value, cursor: len }
74    }
75
76    /// Set the value manually.
77    /// Cursor will be set to the given value's length.
78    pub fn with_value(mut self, value: String) -> Self {
79        self.cursor = value.chars().count();
80        self.value = value;
81        self
82    }
83
84    /// Set the cursor manually.
85    /// If the input is larger than the value length, it'll be auto adjusted.
86    pub fn with_cursor(mut self, cursor: usize) -> Self {
87        self.cursor = cursor.min(self.value.chars().count());
88        self
89    }
90
91    // Reset the cursor and value to default
92    pub fn reset(&mut self) {
93        self.cursor = Default::default();
94        self.value = Default::default();
95    }
96
97    // Reset the cursor and value to default, returning the previous value
98    pub fn value_and_reset(&mut self) -> String {
99        let val = self.value.clone();
100        self.reset();
101        val
102    }
103
104    /// Handle request and emit response.
105    pub fn handle(&mut self, req: InputRequest) -> InputResponse {
106        use InputRequest::*;
107        match req {
108            SetCursor(pos) => {
109                let pos = pos.min(self.value.chars().count());
110                if self.cursor == pos {
111                    None
112                } else {
113                    self.cursor = pos;
114                    Some(StateChanged {
115                        value: false,
116                        cursor: true,
117                    })
118                }
119            }
120            InsertChar(c) => {
121                if self.cursor == self.value.chars().count() {
122                    self.value.push(c);
123                } else {
124                    self.value = self
125                        .value
126                        .chars()
127                        .take(self.cursor)
128                        .chain(
129                            std::iter::once(c)
130                                .chain(self.value.chars().skip(self.cursor)),
131                        )
132                        .collect();
133                }
134                self.cursor += 1;
135                Some(StateChanged {
136                    value: true,
137                    cursor: true,
138                })
139            }
140
141            DeletePrevChar => {
142                if self.cursor == 0 {
143                    None
144                } else {
145                    self.cursor -= 1;
146                    self.value = self
147                        .value
148                        .chars()
149                        .enumerate()
150                        .filter(|(i, _)| i != &self.cursor)
151                        .map(|(_, c)| c)
152                        .collect();
153
154                    Some(StateChanged {
155                        value: true,
156                        cursor: true,
157                    })
158                }
159            }
160
161            DeleteNextChar => {
162                if self.cursor == self.value.chars().count() {
163                    None
164                } else {
165                    self.value = self
166                        .value
167                        .chars()
168                        .enumerate()
169                        .filter(|(i, _)| i != &self.cursor)
170                        .map(|(_, c)| c)
171                        .collect();
172                    Some(StateChanged {
173                        value: true,
174                        cursor: false,
175                    })
176                }
177            }
178
179            GoToPrevChar => {
180                if self.cursor == 0 {
181                    None
182                } else {
183                    self.cursor -= 1;
184                    Some(StateChanged {
185                        value: false,
186                        cursor: true,
187                    })
188                }
189            }
190
191            GoToPrevWord => {
192                if self.cursor == 0 {
193                    None
194                } else {
195                    self.cursor = self
196                        .value
197                        .chars()
198                        .rev()
199                        .skip(self.value.chars().count().max(self.cursor) - self.cursor)
200                        .skip_while(|c| !c.is_alphanumeric())
201                        .skip_while(|c| c.is_alphanumeric())
202                        .count();
203                    Some(StateChanged {
204                        value: false,
205                        cursor: true,
206                    })
207                }
208            }
209
210            GoToNextChar => {
211                if self.cursor == self.value.chars().count() {
212                    None
213                } else {
214                    self.cursor += 1;
215                    Some(StateChanged {
216                        value: false,
217                        cursor: true,
218                    })
219                }
220            }
221
222            GoToNextWord => {
223                if self.cursor == self.value.chars().count() {
224                    None
225                } else {
226                    self.cursor = self
227                        .value
228                        .chars()
229                        .enumerate()
230                        .skip(self.cursor)
231                        .skip_while(|(_, c)| c.is_alphanumeric())
232                        .find(|(_, c)| c.is_alphanumeric())
233                        .map(|(i, _)| i)
234                        .unwrap_or_else(|| self.value.chars().count());
235
236                    Some(StateChanged {
237                        value: false,
238                        cursor: true,
239                    })
240                }
241            }
242
243            DeleteLine => {
244                if self.value.is_empty() {
245                    None
246                } else {
247                    let cursor = self.cursor;
248                    self.value = "".into();
249                    self.cursor = 0;
250                    Some(StateChanged {
251                        value: true,
252                        cursor: self.cursor == cursor,
253                    })
254                }
255            }
256
257            DeletePrevWord => {
258                if self.cursor == 0 {
259                    None
260                } else {
261                    let remaining = self.value.chars().skip(self.cursor);
262                    let rev = self
263                        .value
264                        .chars()
265                        .rev()
266                        .skip(self.value.chars().count().max(self.cursor) - self.cursor)
267                        .skip_while(|c| !c.is_alphanumeric())
268                        .skip_while(|c| c.is_alphanumeric())
269                        .collect::<Vec<char>>();
270                    let rev_len = rev.len();
271                    self.value = rev.into_iter().rev().chain(remaining).collect();
272                    self.cursor = rev_len;
273                    Some(StateChanged {
274                        value: true,
275                        cursor: true,
276                    })
277                }
278            }
279
280            DeleteNextWord => {
281                if self.cursor == self.value.chars().count() {
282                    None
283                } else {
284                    self.value = self
285                        .value
286                        .chars()
287                        .take(self.cursor)
288                        .chain(
289                            self.value
290                                .chars()
291                                .skip(self.cursor)
292                                .skip_while(|c| c.is_alphanumeric())
293                                .skip_while(|c| !c.is_alphanumeric()),
294                        )
295                        .collect();
296
297                    Some(StateChanged {
298                        value: true,
299                        cursor: false,
300                    })
301                }
302            }
303
304            GoToStart => {
305                if self.cursor == 0 {
306                    None
307                } else {
308                    self.cursor = 0;
309                    Some(StateChanged {
310                        value: false,
311                        cursor: true,
312                    })
313                }
314            }
315
316            GoToEnd => {
317                let count = self.value.chars().count();
318                if self.cursor == count {
319                    None
320                } else {
321                    self.cursor = count;
322                    Some(StateChanged {
323                        value: false,
324                        cursor: true,
325                    })
326                }
327            }
328
329            DeleteTillEnd => {
330                self.value = self.value.chars().take(self.cursor).collect();
331                Some(StateChanged {
332                    value: true,
333                    cursor: false,
334                })
335            }
336        }
337    }
338
339    /// Get a reference to the current value.
340    pub fn value(&self) -> &str {
341        self.value.as_str()
342    }
343
344    /// Get the currect cursor placement.
345    pub fn cursor(&self) -> usize {
346        self.cursor
347    }
348
349    /// Get the current cursor position with account for multispace characters.
350    pub fn visual_cursor(&self) -> usize {
351        if self.cursor == 0 {
352            return 0;
353        }
354
355        // Safe, because the end index will always be within bounds
356        unicode_width::UnicodeWidthStr::width(unsafe {
357            self.value.get_unchecked(
358                0..self
359                    .value
360                    .char_indices()
361                    .nth(self.cursor)
362                    .map_or_else(|| self.value.len(), |(index, _)| index),
363            )
364        })
365    }
366
367    /// Get the scroll position with account for multispace characters.
368    pub fn visual_scroll(&self, width: usize) -> usize {
369        let scroll = (self.visual_cursor()).max(width) - width;
370        let mut uscroll = 0;
371        let mut chars = self.value().chars();
372
373        while uscroll < scroll {
374            match chars.next() {
375                Some(c) => {
376                    uscroll += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
377                }
378                None => break,
379            }
380        }
381        uscroll
382    }
383}
384
385impl From<Input> for String {
386    fn from(input: Input) -> Self {
387        input.value
388    }
389}
390
391impl From<String> for Input {
392    fn from(value: String) -> Self {
393        Self::new(value)
394    }
395}
396
397impl From<&str> for Input {
398    fn from(value: &str) -> Self {
399        Self::new(value.into())
400    }
401}
402
403impl std::fmt::Display for Input {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        self.value.fmt(f)
406    }
407}
408
409#[cfg(test)]
410mod tests {
411
412    const TEXT: &str = "first second, third.";
413
414    use super::*;
415
416    #[test]
417    fn format() {
418        let input: Input = TEXT.into();
419        println!("{}", input);
420        println!("{}", input);
421    }
422
423    #[test]
424    fn set_cursor() {
425        let mut input: Input = TEXT.into();
426
427        let req = InputRequest::SetCursor(3);
428        let resp = input.handle(req);
429
430        assert_eq!(
431            resp,
432            Some(StateChanged {
433                value: false,
434                cursor: true,
435            })
436        );
437
438        assert_eq!(input.value(), "first second, third.");
439        assert_eq!(input.cursor(), 3);
440
441        let req = InputRequest::SetCursor(30);
442        let resp = input.handle(req);
443
444        assert_eq!(input.cursor(), TEXT.chars().count());
445        assert_eq!(
446            resp,
447            Some(StateChanged {
448                value: false,
449                cursor: true,
450            })
451        );
452
453        let req = InputRequest::SetCursor(TEXT.chars().count());
454        let resp = input.handle(req);
455
456        assert_eq!(input.cursor(), TEXT.chars().count());
457        assert_eq!(resp, None);
458    }
459
460    #[test]
461    fn insert_char() {
462        let mut input: Input = TEXT.into();
463
464        let req = InputRequest::InsertChar('x');
465        let resp = input.handle(req);
466
467        assert_eq!(
468            resp,
469            Some(StateChanged {
470                value: true,
471                cursor: true,
472            })
473        );
474
475        assert_eq!(input.value(), "first second, third.x");
476        assert_eq!(input.cursor(), TEXT.chars().count() + 1);
477        input.handle(req);
478        assert_eq!(input.value(), "first second, third.xx");
479        assert_eq!(input.cursor(), TEXT.chars().count() + 2);
480
481        let mut input = input.with_cursor(3);
482        input.handle(req);
483        assert_eq!(input.value(), "firxst second, third.xx");
484        assert_eq!(input.cursor(), 4);
485
486        input.handle(req);
487        assert_eq!(input.value(), "firxxst second, third.xx");
488        assert_eq!(input.cursor(), 5);
489    }
490
491    #[test]
492    fn go_to_prev_char() {
493        let mut input: Input = TEXT.into();
494
495        let req = InputRequest::GoToPrevChar;
496        let resp = input.handle(req);
497
498        assert_eq!(
499            resp,
500            Some(StateChanged {
501                value: false,
502                cursor: true,
503            })
504        );
505
506        assert_eq!(input.value(), "first second, third.");
507        assert_eq!(input.cursor(), TEXT.chars().count() - 1);
508
509        let mut input = input.with_cursor(3);
510        input.handle(req);
511        assert_eq!(input.value(), "first second, third.");
512        assert_eq!(input.cursor(), 2);
513
514        input.handle(req);
515        assert_eq!(input.value(), "first second, third.");
516        assert_eq!(input.cursor(), 1);
517    }
518
519    #[test]
520    fn remove_unicode_chars() {
521        let mut input: Input = "¡test¡".into();
522
523        let req = InputRequest::DeletePrevChar;
524        let resp = input.handle(req);
525
526        assert_eq!(
527            resp,
528            Some(StateChanged {
529                value: true,
530                cursor: true,
531            })
532        );
533
534        assert_eq!(input.value(), "¡test");
535        assert_eq!(input.cursor(), 5);
536
537        input.handle(InputRequest::GoToStart);
538
539        let req = InputRequest::DeleteNextChar;
540        let resp = input.handle(req);
541
542        assert_eq!(
543            resp,
544            Some(StateChanged {
545                value: true,
546                cursor: false,
547            })
548        );
549
550        assert_eq!(input.value(), "test");
551        assert_eq!(input.cursor(), 0);
552    }
553
554    #[test]
555    fn insert_unicode_chars() {
556        let mut input = Input::from("¡test¡").with_cursor(5);
557
558        let req = InputRequest::InsertChar('☆');
559        let resp = input.handle(req);
560
561        assert_eq!(
562            resp,
563            Some(StateChanged {
564                value: true,
565                cursor: true,
566            })
567        );
568
569        assert_eq!(input.value(), "¡test☆¡");
570        assert_eq!(input.cursor(), 6);
571
572        input.handle(InputRequest::GoToStart);
573        input.handle(InputRequest::GoToNextChar);
574
575        let req = InputRequest::InsertChar('☆');
576        let resp = input.handle(req);
577
578        assert_eq!(
579            resp,
580            Some(StateChanged {
581                value: true,
582                cursor: true,
583            })
584        );
585
586        assert_eq!(input.value(), "¡☆test☆¡");
587        assert_eq!(input.cursor(), 2);
588    }
589
590    #[test]
591    fn multispace_characters() {
592        let input: Input = "Hello, world!".into();
593        assert_eq!(input.cursor(), 13);
594        assert_eq!(input.visual_cursor(), 23);
595        assert_eq!(input.visual_scroll(6), 18);
596    }
597}