Skip to main content

rich_interact/components/
input.rs

1//! Text input with validation, history and suggestions (#457, #289).
2//!
3//! The line edits like a shell's: the arrows, Home/End, Ctrl+A/E, Ctrl+U,
4//! Ctrl+W, Backspace and Delete. A validator's message shows under the line
5//! and Enter waits until it passes. Up and Down walk the history, or the
6//! suggestions when some are showing; Tab accepts one. Suggestions come from
7//! a fixed list, filtered as you type, or from a provider called on a
8//! background thread, so a slow lookup never stalls typing. The input has
9//! one such thread; it runs one lookup at a time and, when it is free,
10//! takes only the latest text, so typing never piles up lookups.
11//!
12//! The line is a [`TextBuffer`] from the [kit](crate::kit), and the keys are
13//! a [`Keymap`] (context `input`) that can be rebound and listed.
14
15use std::sync::Arc;
16use std::sync::mpsc::{Receiver, Sender, channel};
17use std::time::Duration;
18
19use rich::Segment;
20
21use crate::component::{Component, Context, Flow, View};
22use crate::components::{Theme, fit, highlight, plain, question, text};
23use crate::event::{Event, Key, KeyCode, Modifiers};
24use crate::fuzzy::rank;
25use crate::keymap::{Keymap, keys};
26use crate::kit::TextBuffer;
27use crate::policy::{LineIo, NotInteractive};
28
29/// A completion: the text it inserts, and an optional note beside it.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct Suggestion {
32    pub value: String,
33    pub description: Option<String>,
34}
35
36impl Suggestion {
37    pub fn new(value: impl Into<String>) -> Suggestion {
38        Suggestion {
39            value: value.into(),
40            description: None,
41        }
42    }
43
44    #[must_use]
45    pub fn description(mut self, description: impl Into<String>) -> Suggestion {
46        self.description = Some(description.into());
47        self
48    }
49}
50
51impl From<&str> for Suggestion {
52    fn from(value: &str) -> Suggestion {
53        Suggestion::new(value)
54    }
55}
56
57impl From<String> for Suggestion {
58    fn from(value: String) -> Suggestion {
59        Suggestion::new(value)
60    }
61}
62
63/// Suggestions for what is typed so far, computed off the event thread.
64pub type Provider = Arc<dyn Fn(&str) -> Vec<Suggestion> + Send + Sync>;
65type Validator = Box<dyn Fn(&str) -> Result<(), String>>;
66
67/// The keys of an [`Input`], in context `input`.
68pub fn input_keymap() -> Keymap {
69    Keymap::new("input")
70        .bind("submit", keys("enter"), "submit")
71        .bind("cancel", keys("escape"), "cancel")
72        .bind("complete", keys("tab"), "accept a suggestion")
73        .bind("up", keys("up"), "previous suggestion or answer")
74        .bind("down", keys("down"), "next suggestion or answer")
75        .bind("left", keys("left"), "move left")
76        .bind("right", keys("right"), "move right")
77        .bind("home", keys("home ctrl+a"), "go to the start")
78        .bind("end", keys("end ctrl+e"), "go to the end")
79        .bind("delete-to-start", keys("ctrl+u"), "delete to the start")
80        .bind("delete-word", keys("ctrl+w"), "delete a word")
81        .bind("backspace", keys("backspace"), "delete back")
82        .bind("delete", keys("delete"), "delete forward")
83}
84
85/// One line of text.
86#[must_use = "a component does nothing until it is run or mounted"]
87pub struct Input {
88    prompt: String,
89    /// The text and the caret, which moves by grapheme cluster.
90    buffer: TextBuffer,
91    keymap: Keymap,
92    placeholder: Option<String>,
93    default: Option<String>,
94    help: Option<String>,
95    mask: Option<char>,
96    validator: Option<Validator>,
97    error: Option<String>,
98    history: Vec<String>,
99    /// Where Up has got to in the history, and what was typed before.
100    recall: Option<(usize, String)>,
101    suggestions: Vec<Suggestion>,
102    provider: Option<Provider>,
103    /// (generation, text) to the provider's thread, started on first use.
104    requests: Option<Sender<(u64, String)>>,
105    /// (generation, results) back from it.
106    sender: Sender<(u64, Vec<Suggestion>)>,
107    receiver: Receiver<(u64, Vec<Suggestion>)>,
108    generation: u64,
109    pending: bool,
110    /// Suggestions showing, with the characters to highlight.
111    shown: Vec<(Suggestion, Vec<usize>)>,
112    selected: Option<usize>,
113    limit: usize,
114    theme: Theme,
115    answer: Option<Option<String>>,
116}
117
118impl Input {
119    pub fn new(prompt: impl Into<String>) -> Input {
120        let (sender, receiver) = channel();
121        Input {
122            prompt: prompt.into(),
123            buffer: TextBuffer::new(),
124            keymap: input_keymap(),
125            placeholder: None,
126            default: None,
127            help: None,
128            mask: None,
129            validator: None,
130            error: None,
131            history: Vec::new(),
132            recall: None,
133            suggestions: Vec::new(),
134            provider: None,
135            requests: None,
136            sender,
137            receiver,
138            generation: 0,
139            pending: false,
140            shown: Vec::new(),
141            selected: None,
142            limit: 5,
143            theme: Theme::default(),
144            answer: None,
145        }
146    }
147
148    /// A masked input, for passwords and tokens: what is typed shows as
149    /// `•`, and a default only as `(default set)`.
150    pub fn masked(prompt: impl Into<String>) -> Input {
151        Input::new(prompt).mask('•')
152    }
153
154    pub fn mask(mut self, mask: char) -> Self {
155        self.mask = Some(mask);
156        self
157    }
158
159    /// Start with this text.
160    pub fn value(mut self, value: impl Into<String>) -> Self {
161        self.buffer.set_text(value);
162        self.refresh();
163        self
164    }
165
166    /// Make `keys` do `action` (see [`input_keymap`]) on this input only;
167    /// no keys unbinds it.
168    pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
169        self.keymap.rebind(action, keys);
170        self
171    }
172
173    /// Dim text shown while the line is empty.
174    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
175        self.placeholder = Some(placeholder.into());
176        self
177    }
178
179    /// The answer when Enter is pressed on an empty line, and without a
180    /// terminal when the policy asks for defaults.
181    pub fn default(mut self, default: impl Into<String>) -> Self {
182        self.default = Some(default.into());
183        self
184    }
185
186    /// A line of help under the input.
187    pub fn help(mut self, help: impl Into<String>) -> Self {
188        self.help = Some(help.into());
189        self
190    }
191
192    /// Check the answer on Enter; an `Err` message shows under the line.
193    pub fn validate(mut self, validator: impl Fn(&str) -> Result<(), String> + 'static) -> Self {
194        self.validator = Some(Box::new(validator));
195        self
196    }
197
198    /// Earlier answers, oldest first, for Up and Down.
199    pub fn history<I: IntoIterator<Item = S>, S: Into<String>>(mut self, history: I) -> Self {
200        self.history = history.into_iter().map(Into::into).collect();
201        self
202    }
203
204    /// Fixed suggestions, filtered by what is typed.
205    pub fn suggestions<I: IntoIterator<Item = S>, S: Into<Suggestion>>(
206        mut self,
207        suggestions: I,
208    ) -> Self {
209        self.suggestions = suggestions.into_iter().map(Into::into).collect();
210        self.refresh();
211        self
212    }
213
214    /// Suggestions from `provider`, called on a background thread after
215    /// each change; results for text since changed are dropped.
216    pub fn provider(
217        mut self,
218        provider: impl Fn(&str) -> Vec<Suggestion> + Send + Sync + 'static,
219    ) -> Self {
220        self.provider = Some(Arc::new(provider));
221        self.refresh();
222        self
223    }
224
225    /// Show at most `rows` suggestions (default 5).
226    pub fn limit(mut self, rows: usize) -> Self {
227        self.limit = rows.max(1);
228        self
229    }
230
231    pub fn theme(mut self, theme: Theme) -> Self {
232        self.theme = theme;
233        self
234    }
235
236    /// The text typed so far.
237    pub fn text(&self) -> &str {
238        self.buffer.text()
239    }
240
241    /// The line being edited.
242    pub fn buffer(&self) -> &TextBuffer {
243        &self.buffer
244    }
245
246    fn insert(&mut self, text: &str) {
247        self.buffer.insert(text);
248        self.changed();
249    }
250
251    fn changed(&mut self) {
252        self.error = None;
253        self.recall = None;
254        self.refresh();
255    }
256
257    /// Recompute the suggestions for the current text.
258    fn refresh(&mut self) {
259        self.selected = None;
260        if let Some(provider) = &self.provider {
261            self.generation += 1;
262            self.pending = true;
263            let requests = self.requests.get_or_insert_with(|| {
264                let (requests, queue) = channel::<(u64, String)>();
265                let (provider, sender) = (Arc::clone(provider), self.sender.clone());
266                // Ends when the input, and with it `requests`, is dropped.
267                std::thread::spawn(move || {
268                    while let Ok(mut request) = queue.recv() {
269                        // Skip to the latest text: older lookups are stale.
270                        while let Ok(newer) = queue.try_recv() {
271                            request = newer;
272                        }
273                        let (generation, query) = request;
274                        if sender.send((generation, provider(&query))).is_err() {
275                            return;
276                        }
277                    }
278                });
279                requests
280            });
281            let _ = requests.send((self.generation, self.buffer.text().to_string()));
282            // Until the new results come, keep only what still fits.
283            let current: Vec<Suggestion> = self.shown.iter().map(|(s, _)| s.clone()).collect();
284            self.show(current);
285            return;
286        }
287        self.show(self.suggestions.clone());
288    }
289
290    fn show(&mut self, suggestions: Vec<Suggestion>) {
291        let texts: Vec<&str> = suggestions.iter().map(|s| s.value.as_str()).collect();
292        let value = self.buffer.text();
293        self.shown = if value.is_empty() {
294            Vec::new()
295        } else {
296            rank(value, texts)
297                .into_iter()
298                .map(|(index, found)| (suggestions[index].clone(), found.positions))
299                .filter(|(suggestion, _)| suggestion.value != value)
300                .take(self.limit)
301                .collect()
302        };
303    }
304
305    /// Take results a provider thread has sent, keeping only the latest.
306    fn receive(&mut self) {
307        let mut latest = None;
308        // A moment's wait for a pending lookup, so a fast provider's
309        // results show on the next tick rather than the one after.
310        if self.pending
311            && let Ok((generation, results)) = self.receiver.recv_timeout(Duration::from_millis(5))
312            && generation == self.generation
313        {
314            latest = Some(results);
315        }
316        while let Ok((generation, results)) = self.receiver.try_recv() {
317            if generation == self.generation {
318                latest = Some(results);
319            }
320        }
321        if let Some(results) = latest {
322            self.pending = false;
323            self.show(results);
324        }
325    }
326
327    fn accept(&mut self, index: usize) {
328        if let Some((suggestion, _)) = self.shown.get(index) {
329            self.buffer.set_text(suggestion.value.clone());
330            self.changed();
331        }
332    }
333
334    fn recall(&mut self, older: bool) {
335        if self.history.is_empty() {
336            return;
337        }
338        let next = match (&self.recall, older) {
339            (None, true) => Some(self.history.len() - 1),
340            (None, false) => return,
341            (Some((0, _)), true) => Some(0),
342            (Some((index, _)), true) => Some(index - 1),
343            (Some((index, _)), false) if index + 1 < self.history.len() => Some(index + 1),
344            (Some(_), false) => None,
345        };
346        let typed = match self.recall.take() {
347            Some((_, typed)) => typed,
348            None => self.buffer.text().to_string(),
349        };
350        self.buffer.set_text(match next {
351            Some(index) => self.history[index].clone(),
352            None => typed.clone(),
353        });
354        self.error = None;
355        self.recall = next.map(|index| (index, typed));
356    }
357
358    fn submit(&mut self) -> Flow<String> {
359        if let Some(index) = self.selected {
360            self.accept(index);
361        }
362        let mut answer = self.buffer.text().to_string();
363        if answer.is_empty()
364            && let Some(default) = &self.default
365        {
366            answer = default.clone();
367        }
368        if let Some(validator) = &self.validator
369            && let Err(message) = validator(&answer)
370        {
371            self.error = Some(message);
372            return Flow::Continue;
373        }
374        self.answer = Some(Some(answer.clone()));
375        Flow::Done(answer)
376    }
377
378    /// The answer Enter would give: the text, or the default when empty,
379    /// checked by the validator.
380    pub fn resolve(&self) -> Result<String, String> {
381        let mut answer = self.buffer.text().to_string();
382        if answer.is_empty()
383            && let Some(default) = &self.default
384        {
385            answer = default.clone();
386        }
387        if let Some(validator) = &self.validator {
388            validator(&answer)?;
389        }
390        Ok(answer)
391    }
392
393    /// The line's text as shown (masked, or the placeholder) in
394    /// `available` cells, scrolled to keep the caret in view, and the
395    /// caret's column in it.
396    pub fn field(&self, available: usize) -> (Vec<Segment>, usize) {
397        if self.buffer.is_empty() {
398            let hint = self.placeholder.clone().or_else(|| self.default_hint());
399            return (
400                hint.map(|hint| vec![text(hint, &self.theme.hint)])
401                    .unwrap_or_default(),
402                0,
403            );
404        }
405        let (shown, column) = self.buffer.window(available, self.mask);
406        (vec![plain(shown)], column)
407    }
408
409    /// How the default is shown when nothing is typed: `(value)`, or for
410    /// a masked input only that there is one, so a secret default is never
411    /// shown or written in clear.
412    fn default_hint(&self) -> Option<String> {
413        let default = self.default.as_ref()?;
414        Some(match self.mask {
415            Some(_) => "(default set)".to_string(),
416            None => format!("({default})"),
417        })
418    }
419
420    /// The prompt.
421    pub fn label(&self) -> &str {
422        &self.prompt
423    }
424
425    /// Whether suggestions are showing: Tab, Up, Down and Enter (with one
426    /// selected) are theirs.
427    pub fn suggesting(&self) -> bool {
428        !self.shown.is_empty()
429    }
430
431    /// Whether Enter would accept a selected suggestion rather than submit.
432    pub fn has_selection(&self) -> bool {
433        self.selected.is_some()
434    }
435
436    /// Accept the selected suggestion, as Enter does before submitting.
437    pub fn accept_selected(&mut self) {
438        if let Some(index) = self.selected {
439            self.accept(index);
440        }
441    }
442
443    /// The suggestion rows (or a pending marker), each after `indent`.
444    pub fn suggestion_rows(&self, width: usize, indent: &str) -> Vec<Vec<Segment>> {
445        let theme = &self.theme;
446        let mut lines = Vec::new();
447        for (index, (suggestion, positions)) in self.shown.iter().enumerate() {
448            let selected = self.selected == Some(index);
449            let mut row: Vec<Segment> = vec![plain(indent.to_string())];
450            row.push(if selected {
451                text(format!("  {} ", theme.pointer), &theme.pointer_style)
452            } else {
453                plain("    ")
454            });
455            let base = selected.then_some(&theme.focused);
456            row.extend(highlight(
457                &suggestion.value,
458                positions,
459                base,
460                &theme.matched,
461            ));
462            if let Some(description) = &suggestion.description {
463                row.push(text(format!("  {description}"), &theme.hint));
464            }
465            lines.push(fit(row, width));
466        }
467        if self.pending && self.shown.is_empty() && !self.buffer.is_empty() {
468            lines.push(vec![text(format!("{indent}    …"), &theme.hint)]);
469        }
470        lines
471    }
472
473    /// `value` as this input shows it: masked, for a masked input.
474    pub fn display_value(&self, value: &str) -> String {
475        self.shown_value(value)
476    }
477
478    fn shown_value(&self, value: &str) -> String {
479        TextBuffer::masked(value, self.mask)
480    }
481
482    /// The action `key` triggers here. A key with modifiers that is not
483    /// bound as it is does what it does without them, unless it types.
484    fn key_action(&self, key: Key) -> Option<&str> {
485        self.keymap.action(key).or_else(|| {
486            let typing = matches!(key.code, KeyCode::Char(_));
487            (!typing && key.modifiers != Modifiers::NONE)
488                .then(|| self.keymap.action(Key::new(key.code)))
489                .flatten()
490        })
491    }
492}
493
494impl Component for Input {
495    type Output = String;
496
497    fn handle(&mut self, event: &Event, _: &Context<'_>) -> Flow<String> {
498        if let Event::Tick = event {
499            self.receive();
500            return Flow::Continue;
501        }
502        if let Event::Paste(text) = event {
503            self.insert(&crate::components::pasted(text, " "));
504            return Flow::Continue;
505        }
506        let Some(key) = event.key() else {
507            return Flow::Ignored;
508        };
509        match self.key_action(key) {
510            Some("submit") => return self.submit(),
511            Some("cancel") => {
512                self.answer = Some(None);
513                return Flow::Cancel;
514            }
515            // With nothing to complete, Tab is left to the container: the
516            // next field.
517            Some("complete") if self.shown.is_empty() => return Flow::Ignored,
518            Some("complete") => {
519                let index = self.selected.unwrap_or(0);
520                self.accept(index);
521            }
522            Some(direction @ ("up" | "down")) if !self.shown.is_empty() => {
523                let last = self.shown.len() - 1;
524                self.selected = Some(match (self.selected, direction == "up") {
525                    (None, true) => last,
526                    (None, false) => 0,
527                    (Some(0), true) => last,
528                    (Some(index), true) => index - 1,
529                    (Some(index), false) => (index + 1) % (last + 1),
530                });
531            }
532            Some("up") => self.recall(true),
533            Some("down") => self.recall(false),
534            Some("left") => self.buffer.left(),
535            Some("right") => self.buffer.right(),
536            Some("home") => self.buffer.home(),
537            Some("end") => self.buffer.end(),
538            Some("delete-to-start") => {
539                self.buffer.delete_to_start();
540                self.changed();
541            }
542            Some("delete-word") => {
543                self.buffer.delete_word();
544                self.changed();
545            }
546            Some("backspace") => {
547                if self.buffer.backspace() {
548                    self.changed();
549                }
550            }
551            Some("delete") => {
552                if self.buffer.delete() {
553                    self.changed();
554                }
555            }
556            _ => match key.code {
557                KeyCode::Char(c) if !key.modifiers.ctrl && !key.modifiers.alt => {
558                    let mut buffer = [0; 4];
559                    self.insert(c.encode_utf8(&mut buffer));
560                }
561                _ => return Flow::Ignored,
562            },
563        }
564        Flow::Continue
565    }
566
567    fn render(&self, context: &Context<'_>) -> View {
568        let width = context.width;
569        let theme = &self.theme;
570        let mut line = question(theme, &self.prompt);
571        if let Some(answer) = &self.answer {
572            line.push(match answer {
573                Some(answer) => text(self.shown_value(answer), &theme.answer),
574                None => text("cancelled", &theme.hint),
575            });
576            return View::new(vec![fit(line, width)]);
577        }
578        let start = crate::components::width(&line);
579        let (field, caret) = self.field(width.saturating_sub(start));
580        line.extend(field);
581        let column = start + caret;
582        let mut lines = vec![fit(line, width)];
583        if let Some(error) = &self.error {
584            lines.push(fit(vec![text(format!("  ✗ {error}"), &theme.error)], width));
585        } else if let Some(help) = &self.help {
586            lines.push(fit(vec![text(format!("  {help}"), &theme.hint)], width));
587        }
588        lines.extend(self.suggestion_rows(width, ""));
589        View::new(lines).with_cursor(0, column.min(width.saturating_sub(1)))
590    }
591
592    fn tick(&self) -> Option<Duration> {
593        self.provider.as_ref().map(|_| Duration::from_millis(30))
594    }
595
596    fn keymap(&self) -> Keymap {
597        self.keymap.clone()
598    }
599
600    fn default_value(&self) -> Option<String> {
601        self.default.clone()
602    }
603
604    fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<String>, NotInteractive> {
605        let mut prompt = self.prompt.clone();
606        if let Some(hint) = self.default_hint() {
607            prompt.push_str(&format!(" [{}]", &hint[1..hint.len() - 1]));
608        }
609        let prompt = format!("{prompt}: ");
610        // A masked answer is read without echo where the terminal has it,
611        // turned off before the prompt shows.
612        let read = if self.mask.is_some() {
613            io.prompt_secret(&prompt)
614        } else {
615            io.write(&prompt);
616            io.read_line().map(Some).ok_or(NotInteractive::Ended)
617        };
618        let mut line = match read {
619            Ok(Some(line)) => line,
620            Ok(None) => return Ok(None),
621            // Input ended: the default is the answer, as for an empty line.
622            Err(NotInteractive::Ended) => match &self.default {
623                Some(default) => default.clone(),
624                None => return Err(NotInteractive::Ended),
625            },
626            Err(error) => return Err(error),
627        };
628        if line.is_empty()
629            && let Some(default) = &self.default
630        {
631            line = default.clone();
632        }
633        if let Some(validator) = &self.validator {
634            validator(&line).map_err(NotInteractive::Invalid)?;
635        }
636        Ok(Some(line))
637    }
638}