Skip to main content

tui_input/
input.rs

1//! Core logic for handling input.
2//!
3//! # Units
4//!
5//! A string has four different possible notions of length or position:
6//!
7//! - **bytes**:  indices into the UTF-8 encoding, used only internally.
8//! - **codepoints**:  Unicode scalar values (what [`str::chars`] yields).
9//!   This is what [`Input::cursor`] returns and what
10//!   [`InputRequest::SetCursor`] accepts.
11//! - **graphemes**:  user-perceived characters (per `unicode-segmentation`).
12//!   Movement and deletion ([`InputRequest::GoToPrevChar`],
13//!   [`InputRequest::GoToNextChar`], [`InputRequest::DeletePrevChar`],
14//!   [`InputRequest::DeleteNextChar`], [`InputRequest::GoToPrevWord`],
15//!   [`InputRequest::GoToNextWord`], [`InputRequest::DeletePrevWord`],
16//!   [`InputRequest::DeleteNextWord`]) step one *grapheme* or *word*
17//!   at a time, which may span multiple codepoints.
18//! - **display columns**:  terminal cell width (per `unicode-width`).
19//!   Returned by [`Input::visual_cursor`] and [`Input::visual_scroll`].
20//!
21//! All four can differ for one string.  For example, `🤦🏼‍♂️` is
22//! actually `"🤦🏼\u{200D}♂\u{FE0F}"`, which is 17 bytes, 5 codepoints,
23//! 1 grapheme, 2 display columns.
24//!
25//! # Example: Without any backend
26//!
27//! ```
28//! use tui_input::{Input, InputRequest, StateChanged};
29//!
30//! let mut input: Input = "Hello Worl".into();
31//!
32//! let req = InputRequest::InsertChar('d');
33//! let resp = input.handle(req);
34//!
35//! assert_eq!(resp, Some(StateChanged { value: true, cursor: true }));
36//! assert_eq!(input.cursor(), 11);
37//! assert_eq!(input.to_string(), "Hello World");
38//! ```
39
40use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
41
42fn prev_grapheme(s: &str, byte: usize) -> Option<usize> {
43    GraphemeCursor::new(byte, s.len(), true)
44        .prev_boundary(s, 0)
45        .ok()
46        .flatten()
47}
48
49fn next_grapheme(s: &str, byte: usize) -> Option<usize> {
50    GraphemeCursor::new(byte, s.len(), true)
51        .next_boundary(s, 0)
52        .ok()
53        .flatten()
54}
55
56fn is_word(s: &str) -> bool {
57    s.chars()
58        .any(|c| !c.is_whitespace() && !c.is_ascii_punctuation())
59}
60
61fn prev_word_byte(s: &str, byte: usize) -> usize {
62    let mut words = s
63        .split_word_bound_indices()
64        .filter(|(i, _)| *i < byte)
65        .rev();
66    while let Some((i, word)) = words.next() {
67        if is_word(word) {
68            return i;
69        }
70    }
71    0
72}
73
74fn next_word_byte(s: &str, byte: usize) -> usize {
75    let mut words = s.split_word_bound_indices().filter(|(i, _)| *i > byte);
76    while let Some((i, word)) = words.next() {
77        if is_word(word) {
78            return i;
79        }
80    }
81    s.len()
82}
83
84fn codepoint_to_byte(s: &str, n: usize) -> usize {
85    s.char_indices().nth(n).map_or(s.len(), |(i, _)| i)
86}
87
88fn byte_to_codepoint(s: &str, byte: usize) -> usize {
89    s[..byte].chars().count()
90}
91
92enum Side {
93    Left,
94    Right,
95}
96
97/// Input requests are used to change the input state.
98///
99/// Different backends can be used to convert events into requests.
100#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102pub enum InputRequest {
103    SetCursor(usize),
104    InsertChar(char),
105    GoToPrevChar,
106    GoToNextChar,
107    GoToPrevWord,
108    GoToNextWord,
109    GoToStart,
110    GoToEnd,
111    DeletePrevChar,
112    DeleteNextChar,
113    DeletePrevWord,
114    DeleteNextWord,
115    DeleteLine,
116    DeleteTillEnd,
117    Yank,
118}
119
120#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122pub struct StateChanged {
123    pub value: bool,
124    pub cursor: bool,
125}
126
127pub type InputResponse = Option<StateChanged>;
128
129/// The input buffer with cursor support.
130///
131/// Example:
132///
133/// ```
134/// use tui_input::Input;
135///
136/// let input: Input = "Hello World".into();
137///
138/// assert_eq!(input.cursor(), 11);
139/// assert_eq!(input.to_string(), "Hello World");
140/// ```
141#[derive(Default, Debug, Clone)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143pub struct Input {
144    value: String,
145    /// Codepoints preceding the cursor.  See the module-level `Units` section.
146    cursor: usize,
147    yank: String,
148    last_was_cut: bool,
149}
150
151impl Input {
152    /// Initialize a new instance with a given value
153    /// Cursor will be set to the given value's length.
154    pub fn new(value: String) -> Self {
155        let len = value.chars().count();
156        Self {
157            value,
158            cursor: len,
159            yank: String::new(),
160            last_was_cut: false,
161        }
162    }
163
164    /// Set the value manually.
165    /// Cursor will be set to the given value's length.
166    pub fn with_value(mut self, value: String) -> Self {
167        self.cursor = value.chars().count();
168        self.value = value;
169        self
170    }
171
172    /// Set the cursor manually.
173    /// If the input is larger than the value length, it'll be auto adjusted.
174    pub fn with_cursor(mut self, cursor: usize) -> Self {
175        self.cursor = cursor.min(self.value.chars().count());
176        self
177    }
178
179    // Reset the cursor and value to default
180    pub fn reset(&mut self) {
181        self.cursor = Default::default();
182        self.value = Default::default();
183    }
184
185    // Reset the cursor and value to default, returning the previous value
186    pub fn value_and_reset(&mut self) -> String {
187        let val = self.value.clone();
188        self.reset();
189        val
190    }
191
192    fn add_to_yank(&mut self, deleted: String, side: Side) {
193        if self.last_was_cut {
194            match side {
195                Side::Left => self.yank.insert_str(0, &deleted),
196                Side::Right => self.yank.push_str(&deleted),
197            }
198        } else {
199            self.yank = deleted;
200        }
201    }
202
203    fn set_last_was_cut(&mut self, req: InputRequest) {
204        use InputRequest::*;
205        self.last_was_cut = matches!(
206            req,
207            DeleteLine | DeletePrevWord | DeleteNextWord | DeleteTillEnd
208        );
209    }
210
211    /// Handle request and emit response.
212    pub fn handle(&mut self, req: InputRequest) -> InputResponse {
213        use InputRequest::*;
214        let result = match req {
215            SetCursor(pos) => {
216                let pos = pos.min(self.value.chars().count());
217                if self.cursor == pos {
218                    None
219                } else {
220                    self.cursor = pos;
221                    Some(StateChanged {
222                        value: false,
223                        cursor: true,
224                    })
225                }
226            }
227            InsertChar(c) => {
228                if self.cursor == self.value.chars().count() {
229                    self.value.push(c);
230                } else {
231                    self.value = self
232                        .value
233                        .chars()
234                        .take(self.cursor)
235                        .chain(
236                            std::iter::once(c)
237                                .chain(self.value.chars().skip(self.cursor)),
238                        )
239                        .collect();
240                }
241                self.cursor += 1;
242                Some(StateChanged {
243                    value: true,
244                    cursor: true,
245                })
246            }
247
248            DeletePrevChar => {
249                let byte = codepoint_to_byte(&self.value, self.cursor);
250                let prev = prev_grapheme(&self.value, byte)?;
251                let removed = self.value[prev..byte].chars().count();
252                self.value.replace_range(prev..byte, "");
253                self.cursor -= removed;
254                Some(StateChanged {
255                    value: true,
256                    cursor: true,
257                })
258            }
259
260            DeleteNextChar => {
261                let byte = codepoint_to_byte(&self.value, self.cursor);
262                let next = next_grapheme(&self.value, byte)?;
263                self.value.replace_range(byte..next, "");
264                Some(StateChanged {
265                    value: true,
266                    cursor: false,
267                })
268            }
269
270            GoToPrevChar => {
271                let byte = codepoint_to_byte(&self.value, self.cursor);
272                let prev = prev_grapheme(&self.value, byte)?;
273                self.cursor -= self.value[prev..byte].chars().count();
274                Some(StateChanged {
275                    value: false,
276                    cursor: true,
277                })
278            }
279
280            GoToPrevWord => {
281                let byte = codepoint_to_byte(&self.value, self.cursor);
282                let prev = prev_word_byte(&self.value, byte);
283                if self.cursor == 0 {
284                    None
285                } else {
286                    self.cursor = byte_to_codepoint(&self.value, prev);
287                    Some(StateChanged {
288                        value: false,
289                        cursor: true,
290                    })
291                }
292            }
293
294            GoToNextChar => {
295                let byte = codepoint_to_byte(&self.value, self.cursor);
296                let next = next_grapheme(&self.value, byte)?;
297                self.cursor += self.value[byte..next].chars().count();
298                Some(StateChanged {
299                    value: false,
300                    cursor: true,
301                })
302            }
303
304            GoToNextWord => {
305                let byte = codepoint_to_byte(&self.value, self.cursor);
306                let next = next_word_byte(&self.value, byte);
307                if self.cursor == self.value.chars().count() {
308                    None
309                } else {
310                    self.cursor = byte_to_codepoint(&self.value, next);
311                    Some(StateChanged {
312                        value: false,
313                        cursor: true,
314                    })
315                }
316            }
317
318            DeleteLine => {
319                if self.value.is_empty() {
320                    None
321                } else {
322                    let side = if self.cursor == self.value.chars().count() {
323                        Side::Left
324                    } else {
325                        Side::Right
326                    };
327                    self.add_to_yank(self.value.clone(), side);
328                    self.value = "".into();
329                    self.cursor = 0;
330                    Some(StateChanged {
331                        value: true,
332                        cursor: true,
333                    })
334                }
335            }
336
337            DeletePrevWord => {
338                if self.cursor == 0 {
339                    None
340                } else {
341                    let byte = codepoint_to_byte(&self.value, self.cursor);
342                    let prev = prev_word_byte(&self.value, byte);
343                    let deleted = self.value[prev..byte].to_string();
344                    self.add_to_yank(deleted, Side::Left);
345                    self.value.replace_range(prev..byte, "");
346                    self.cursor = byte_to_codepoint(&self.value, prev);
347                    Some(StateChanged {
348                        value: true,
349                        cursor: true,
350                    })
351                }
352            }
353
354            DeleteNextWord => {
355                let byte = codepoint_to_byte(&self.value, self.cursor);
356                let next = next_word_byte(&self.value, byte);
357                if self.cursor == self.value.chars().count() {
358                    None
359                } else {
360                    let deleted = self.value[byte..next].to_string();
361                    self.add_to_yank(deleted, Side::Right);
362                    self.value.replace_range(byte..next, "");
363                    Some(StateChanged {
364                        value: true,
365                        cursor: false,
366                    })
367                }
368            }
369
370            GoToStart => {
371                if self.cursor == 0 {
372                    None
373                } else {
374                    self.cursor = 0;
375                    Some(StateChanged {
376                        value: false,
377                        cursor: true,
378                    })
379                }
380            }
381
382            GoToEnd => {
383                let count = self.value.chars().count();
384                if self.cursor == count {
385                    None
386                } else {
387                    self.cursor = count;
388                    Some(StateChanged {
389                        value: false,
390                        cursor: true,
391                    })
392                }
393            }
394
395            DeleteTillEnd => {
396                let deleted: String = self.value.chars().skip(self.cursor).collect();
397                self.add_to_yank(deleted, Side::Right);
398                self.value = self.value.chars().take(self.cursor).collect();
399                Some(StateChanged {
400                    value: true,
401                    cursor: false,
402                })
403            }
404
405            Yank => {
406                if self.yank.is_empty() {
407                    None
408                } else if self.cursor == self.value.chars().count() {
409                    self.value.push_str(&self.yank);
410                    self.cursor += self.yank.chars().count();
411                    Some(StateChanged {
412                        value: true,
413                        cursor: true,
414                    })
415                } else {
416                    self.value = self
417                        .value
418                        .chars()
419                        .take(self.cursor)
420                        .chain(self.yank.chars())
421                        .chain(self.value.chars().skip(self.cursor))
422                        .collect();
423                    self.cursor += self.yank.chars().count();
424                    Some(StateChanged {
425                        value: true,
426                        cursor: true,
427                    })
428                }
429            }
430        };
431        self.set_last_was_cut(req);
432        result
433    }
434
435    /// Get a reference to the current value.
436    pub fn value(&self) -> &str {
437        self.value.as_str()
438    }
439
440    /// Returns the number of **codepoints** preceding the cursor.  Movement
441    /// and deletion operations step one *grapheme* at a time, so a single
442    /// [`InputRequest::GoToNextChar`] or [`InputRequest::DeletePrevChar`]
443    /// may change this count by more than one.
444    pub fn cursor(&self) -> usize {
445        self.cursor
446    }
447
448    /// Returns the cursor's position in **display columns** (per
449    /// `unicode-width`).
450    pub fn visual_cursor(&self) -> usize {
451        if self.cursor == 0 {
452            return 0;
453        }
454
455        // Safe, because the end index will always be within bounds
456        unicode_width::UnicodeWidthStr::width(unsafe {
457            self.value.get_unchecked(
458                0..self
459                    .value
460                    .char_indices()
461                    .nth(self.cursor)
462                    .map_or_else(|| self.value.len(), |(index, _)| index),
463            )
464        })
465    }
466
467    /// Get the scroll position with account for multispace characters.
468    pub fn visual_scroll(&self, width: usize) -> usize {
469        let scroll = (self.visual_cursor()).max(width) - width;
470        let mut uscroll = 0;
471        let mut chars = self.value().chars();
472
473        while uscroll < scroll {
474            match chars.next() {
475                Some(c) => {
476                    uscroll += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
477                }
478                None => break,
479            }
480        }
481        uscroll
482    }
483}
484
485impl From<Input> for String {
486    fn from(input: Input) -> Self {
487        input.value
488    }
489}
490
491impl From<String> for Input {
492    fn from(value: String) -> Self {
493        Self::new(value)
494    }
495}
496
497impl From<&str> for Input {
498    fn from(value: &str) -> Self {
499        Self::new(value.into())
500    }
501}
502
503impl std::fmt::Display for Input {
504    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505        self.value.fmt(f)
506    }
507}
508
509#[cfg(test)]
510mod tests;