fuzzy_select/
lib.rs

1//! ## A fuzzy select prompt for the terminal.
2//!
3//! This crate is a library for creating a fuzzy select prompt for the terminal.
4//! It uses [nucleo] as its fuzzy matching engine.
5//! The prompt is very simple and not very configurable.
6//!
7//! ## Usage
8//!
9//! Add the following to your `Cargo.toml`:
10//!
11//! ```toml
12//! [dependencies]
13//! fuzzy-select = "0.1"
14//! ```
15//!
16//! ## Example
17//! ```no_run
18//! # fn main() -> Result<(), fuzzy_select::Error> {
19//! use fuzzy_select::FuzzySelect;
20//!
21//! let options = vec!["foo", "bar", "baz"];
22//! let selected = FuzzySelect::new()
23//!     .with_prompt("Select something")
24//!     .with_options(options)
25//!     .select()?;
26//!
27//! println!("Selected: {:?}", selected);
28//! # Ok(())
29//! # }
30//! ```
31use std::{
32    borrow::Cow,
33    fmt,
34    io::{self, StderrLock, Write},
35    num::NonZeroU16,
36    rc::Rc,
37    sync::Arc,
38};
39
40use crossterm::{
41    cursor,
42    event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
43    style,
44    terminal::{self, ClearType},
45    QueueableCommand,
46};
47use nucleo::{
48    pattern::{CaseMatching, Normalization, Pattern},
49    Config, Matcher, Nucleo, Snapshot, Utf32Str,
50};
51
52pub use crossterm::style::{Attribute, Attributes, Color, ContentStyle, StyledContent, Stylize};
53use unicode_segmentation::UnicodeSegmentation;
54
55pub type Result<T, E = Error> = std::result::Result<T, E>;
56
57#[derive(Debug, onlyerror::Error)]
58pub enum Error {
59    #[error("No options to select from")]
60    NoOptions,
61
62    #[error("Selection cancelled")]
63    Cancelled,
64
65    #[error("Selected index is out of bounds")]
66    InvalidSelection,
67
68    #[error("Cannot run with the terminal in non-interactive mode (not a tty)")]
69    NonInteractive,
70
71    #[error("Failed to interacto with the terminal: {0}")]
72    IoError(#[from] io::Error),
73}
74
75/// An item that can be selected in a fuzzy select prompt.
76///
77/// This trait is implemented for a number of string like types, where
78/// the rendered and searched content is identical to the string.
79///
80/// For more complex items, trait implementation need to provide what
81/// content is being searched and how the item is rendered.
82///
83/// The search content will always be rendered and possibly highlighted
84/// with the matched parts of the search query. One can optionally
85/// render additional content before and after the search content.
86/// This will only be included in the rendering, not in the search.
87pub trait Select {
88    /// The content that is being used to match against the prompt input.
89    fn search_content(&self) -> &str;
90
91    /// Optionally render additional content before the search content when
92    /// showing this item.
93    fn render_before_content(&self) -> Option<impl fmt::Display + '_>;
94
95    /// Optionally render additional content after the search content when
96    /// showing this item.
97    fn render_after_content(&self) -> Option<impl fmt::Display + '_>;
98}
99
100#[allow(clippy::struct_excessive_bools)]
101/// A fuzzy select prompt. See the [module level documentation](crate) for more information.
102#[derive(Clone, Debug)]
103pub struct FuzzySelect<T> {
104    prompt: Option<String>,
105    options: Vec<T>,
106    initial_selection: u32,
107    query: Option<String>,
108    filter: bool,
109    highlight: bool,
110    page_size: Option<NonZeroU16>,
111    color: Option<bool>,
112    theme: Theme,
113    alternate_screen: bool,
114    select1: bool,
115}
116
117impl<T> Default for FuzzySelect<T> {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl<T> FuzzySelect<T> {
124    #[must_use]
125    pub fn new() -> Self {
126        Self {
127            prompt: None,
128            options: Vec::new(),
129            initial_selection: 0,
130            query: None,
131            filter: true,
132            highlight: true,
133            page_size: None,
134            color: None,
135            theme: Theme::default(),
136            alternate_screen: true,
137            select1: false,
138        }
139    }
140
141    #[must_use]
142    pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
143        self.prompt = Some(prompt.into());
144        self
145    }
146
147    #[must_use]
148    pub fn without_prompt(mut self) -> Self {
149        self.prompt = None;
150        self
151    }
152
153    #[must_use]
154    pub fn set_prompt<P: Into<String>>(mut self, prompt: impl Into<Option<P>>) -> Self {
155        self.prompt = prompt.into().map(Into::into);
156        self
157    }
158
159    #[must_use]
160    pub fn with_options(mut self, options: Vec<T>) -> Self {
161        self.options = options;
162        self
163    }
164
165    #[must_use]
166    pub fn with_options_from_iter(mut self, options: impl IntoIterator<Item = T>) -> Self {
167        self.options.extend(options);
168        self
169    }
170
171    #[must_use]
172    pub fn with_options_from_slice(mut self, options: &[T]) -> Self
173    where
174        T: Clone,
175    {
176        self.options.extend_from_slice(options);
177        self
178    }
179
180    #[must_use]
181    pub fn add_option(mut self, option: T) -> Self {
182        self.options.push(option);
183        self
184    }
185
186    #[must_use]
187    pub fn add_options(mut self, options: impl IntoIterator<Item = T>) -> Self {
188        self.options.extend(options);
189        self
190    }
191
192    #[must_use]
193    pub fn with_initial_selection(mut self, initial_selection: u32) -> Self {
194        self.initial_selection = initial_selection;
195        self
196    }
197
198    #[must_use]
199    pub fn with_query(mut self, query: impl Into<String>) -> Self {
200        self.query = Some(query.into());
201        self
202    }
203
204    #[must_use]
205    pub fn without_query(mut self) -> Self {
206        self.query = None;
207        self
208    }
209
210    #[must_use]
211    pub fn set_query<Q: Into<String>>(mut self, query: impl Into<Option<Q>>) -> Self {
212        self.query = query.into().map(Into::into);
213        self
214    }
215
216    #[must_use]
217    pub fn with_filter(mut self) -> Self {
218        self.filter = true;
219        self
220    }
221
222    #[must_use]
223    pub fn without_filter(mut self) -> Self {
224        self.filter = false;
225        self
226    }
227
228    #[must_use]
229    pub fn set_filter(mut self, filter: bool) -> Self {
230        self.filter = filter;
231        self
232    }
233
234    #[must_use]
235    pub fn with_select1(mut self) -> Self {
236        self.select1 = true;
237        self
238    }
239
240    #[must_use]
241    pub fn without_select1(mut self) -> Self {
242        self.select1 = false;
243        self
244    }
245
246    #[must_use]
247    pub fn set_select1(mut self, select1: bool) -> Self {
248        self.select1 = select1;
249        self
250    }
251
252    #[must_use]
253    pub fn without_highlight(mut self) -> Self {
254        self.highlight = false;
255        self
256    }
257
258    #[must_use]
259    pub fn set_highlight(mut self, highlight: bool) -> Self {
260        self.highlight = highlight;
261        self
262    }
263
264    #[must_use]
265    pub fn with_page_size(mut self, page_size: u16) -> Self {
266        self.page_size = NonZeroU16::new(page_size);
267        self
268    }
269
270    #[must_use]
271    pub fn with_default_page_size(mut self) -> Self {
272        self.page_size = None;
273        self
274    }
275
276    #[must_use]
277    pub fn with_color(mut self, color: bool) -> Self {
278        self.color = Some(color);
279        self
280    }
281
282    #[must_use]
283    pub fn with_default_color(mut self) -> Self {
284        self.color = None;
285        self
286    }
287
288    #[must_use]
289    pub fn set_color(mut self, color: impl Into<Option<bool>>) -> Self {
290        self.color = color.into();
291        self
292    }
293
294    #[must_use]
295    pub fn with_theme(mut self, theme: Theme) -> Self {
296        self.theme = theme;
297        self
298    }
299
300    #[must_use]
301    pub fn with_default_theme(mut self) -> Self {
302        self.theme = Theme::default();
303        self
304    }
305
306    #[must_use]
307    pub fn without_alternate_screen(mut self) -> Self {
308        self.alternate_screen = false;
309        self
310    }
311
312    #[must_use]
313    pub fn with_alternate_screen(mut self) -> Self {
314        self.alternate_screen = true;
315        self
316    }
317
318    #[must_use]
319    pub fn set_alternate_screen(mut self, alternate_screen: bool) -> Self {
320        self.alternate_screen = alternate_screen;
321        self
322    }
323
324    /// Runs the fuzzy select prompt and returns the selected item.
325    ///
326    /// # Errors
327    /// - [`Error::NoOptions`] if there are no options to select from.
328    ///     Need to call one of the `*option*` methods before calling this.
329    /// - [`Error::Cancelled`] if the user cancelled the selection
330    ///     (e.g. by hitting Ctrl-C or Esc).
331    /// - [`Error::InvalidSelection`] if the default selection index
332    ///     is out of bounds based off the provided options.
333    /// - [`Error::NonInteractive`] if the terminal is not in interactive mode.
334    ///    This is the case if the terminal is not a tty.
335    /// - [`Error::IoError`] if there was an error interacting with interacting
336    ///     with terminal in general.
337    pub fn select(mut self) -> Result<T>
338    where
339        T: Select,
340    {
341        if self.options.is_empty() {
342            return Err(Error::NoOptions);
343        }
344
345        let mut engine = self.init_engine();
346
347        if self.query.is_some() && self.select1 {
348            let _status = engine.tick(10);
349            let snap = engine.snapshot();
350            if snap.matched_item_count() == 1 {
351                if let Some(index) = snap.get_matched_item(0) {
352                    return Ok(self.options.swap_remove(*index.data));
353                }
354            }
355        }
356
357        let mut prompt = self.init_prompt()?;
358        prompt.initial_prompt(self.prompt.as_deref());
359
360        let mut redraw = Redraw::Selection;
361        let selected = loop {
362            let res = prompt.tick(&self.theme, &mut engine, &self.options, redraw)?;
363
364            match res {
365                Ok(r) => redraw = r,
366                Err(Stop::Quit) => return Err(Error::Cancelled),
367                Err(Stop::Selected(selected)) => break selected,
368            }
369        };
370        drop(prompt);
371
372        let item = *engine
373            .snapshot()
374            .get_matched_item(selected)
375            .ok_or(Error::InvalidSelection)?
376            .data;
377        let item = self.options.swap_remove(item);
378
379        Ok(item)
380    }
381
382    /// Runs the fuzzy select prompt and returns the selected item or None
383    /// if no selection was made.
384    ///
385    /// # Errors
386    /// - [`Error::NonInteractive`] if the terminal is not in interactive mode.
387    ///    This is the case if the terminal is not a tty.
388    /// - [`Error::IoError`] if there was an error interacting with interacting
389    ///     with terminal in general.
390    pub fn select_opt(self) -> Result<Option<T>>
391    where
392        T: Select,
393    {
394        match self.select() {
395            Ok(res) => Ok(Some(res)),
396            Err(Error::NoOptions | Error::Cancelled | Error::InvalidSelection) => Ok(None),
397            Err(e) => Err(e),
398        }
399    }
400
401    fn init_engine(&self) -> Nucleo<usize>
402    where
403        T: Select,
404    {
405        let mut engine = Nucleo::<usize>::new(Config::DEFAULT, Arc::new(|| {}), None, 1);
406
407        let injector = engine.injector();
408
409        for (idx, item) in self.options.iter().enumerate() {
410            let _ = injector.push(idx, move |_, cols| {
411                cols[0] = item.search_content().into();
412            });
413        }
414
415        if let Some(query) = self.query.as_deref() {
416            engine
417                .pattern
418                .reparse(0, query, CaseMatching::Smart, Normalization::Smart, true);
419        }
420
421        engine
422    }
423
424    fn init_prompt(&mut self) -> Result<Prompt> {
425        let term = Terminal::new(self.color, self.alternate_screen)?;
426        let page_size = self
427            .page_size
428            .map(NonZeroU16::get)
429            .or_else(|| terminal::size().ok().map(|(_, h)| h.saturating_sub(1)))
430            .unwrap_or(10);
431        let highlighter = Highlighter::new(self.highlight && self.color.unwrap_or(true));
432        let query = self.query.take().unwrap_or_default();
433        let input = Input::new(query);
434        let selected = self.initial_selection;
435        if selected as usize >= self.options.len() {
436            return Err(Error::InvalidSelection);
437        }
438
439        Ok(Prompt {
440            term,
441            scroll_offset: 0,
442            selected,
443            height: u32::from(page_size),
444            active: true,
445            filter: self.filter,
446            number_of_matches: u32::MAX,
447            highlighter,
448            input,
449        })
450    }
451}
452
453/// A theme describes how different parts of the prompt are rendered.
454#[derive(Copy, Clone, Debug)]
455pub struct Theme {
456    /// The inidicator to render for the row that is currently selected.
457    /// Defaults to `>` in red on a black background.
458    pub selected_indicator: StyledContent<char>,
459
460    /// The indicator to render for rows that are not selected.
461    /// Defaults to a space on a black background.
462    pub indicator: StyledContent<char>,
463
464    /// The style to render the text of the row that is currently selected.
465    /// Defaults to a black background.
466    pub selected_text: ContentStyle,
467
468    /// The style to render the text of rows that are not selected.
469    /// Defaults to non-styled text.
470    pub text: ContentStyle,
471
472    /// The style to render the highlighted parts of the text of the row
473    /// that is currently selected.
474    /// Defaults to dark cyan on black background.
475    pub selected_highlight: ContentStyle,
476
477    /// The style to render the highlighted parts of the text of rows
478    /// that are not selected.
479    /// Defaults to cyan text.
480    pub highlight: ContentStyle,
481}
482
483impl Default for Theme {
484    fn default() -> Self {
485        Self {
486            selected_indicator: ContentStyle::new().red().on_black().apply('>'),
487            indicator: ContentStyle::new().on_black().apply(' '),
488            selected_text: ContentStyle::new().white().on_black(),
489            text: ContentStyle::new(),
490            selected_highlight: ContentStyle::new().dark_cyan().on_black(),
491            highlight: ContentStyle::new().cyan(),
492        }
493    }
494}
495
496impl Select for String {
497    fn search_content(&self) -> &str {
498        self.as_str()
499    }
500
501    fn render_before_content(&self) -> Option<impl fmt::Display + '_> {
502        None::<Self>
503    }
504
505    fn render_after_content(&self) -> Option<impl fmt::Display + '_> {
506        None::<Self>
507    }
508}
509
510impl Select for &str {
511    fn search_content(&self) -> &str {
512        self
513    }
514
515    fn render_before_content(&self) -> Option<impl fmt::Display + '_> {
516        None::<Self>
517    }
518
519    fn render_after_content(&self) -> Option<impl fmt::Display + '_> {
520        None::<Self>
521    }
522}
523
524impl Select for Box<str> {
525    fn search_content(&self) -> &str {
526        self
527    }
528
529    fn render_before_content(&self) -> Option<impl fmt::Display + '_> {
530        None::<Self>
531    }
532
533    fn render_after_content(&self) -> Option<impl fmt::Display + '_> {
534        None::<Self>
535    }
536}
537
538impl Select for Rc<str> {
539    fn search_content(&self) -> &str {
540        self
541    }
542
543    fn render_before_content(&self) -> Option<impl fmt::Display + '_> {
544        None::<Self>
545    }
546
547    fn render_after_content(&self) -> Option<impl fmt::Display + '_> {
548        None::<Self>
549    }
550}
551
552impl Select for Arc<str> {
553    fn search_content(&self) -> &str {
554        self
555    }
556
557    fn render_before_content(&self) -> Option<impl fmt::Display + '_> {
558        None::<Self>
559    }
560
561    fn render_after_content(&self) -> Option<impl fmt::Display + '_> {
562        None::<Self>
563    }
564}
565
566impl Select for Cow<'_, str> {
567    fn search_content(&self) -> &str {
568        self
569    }
570
571    fn render_before_content(&self) -> Option<impl fmt::Display + '_> {
572        None::<Self>
573    }
574
575    fn render_after_content(&self) -> Option<impl fmt::Display + '_> {
576        None::<Self>
577    }
578}
579
580struct Prompt {
581    term: Terminal,
582    input: Input,
583    scroll_offset: u32,
584    selected: u32,
585    height: u32,
586    active: bool,
587    filter: bool,
588    number_of_matches: u32,
589    highlighter: Highlighter,
590}
591
592impl Prompt {
593    fn initial_prompt(&mut self, prompt: Option<&str>) {
594        self.term
595            .queue(cursor::MoveTo(0, 0))
596            .queue_(terminal::Clear(ClearType::All));
597
598        if let Some(prompt) = prompt {
599            self.term.queue_(style::Print(prompt));
600
601            if self.filter {
602                self.term.queue_(style::Print(" "));
603            } else {
604                self.term.queue_(cursor::Hide);
605            }
606        }
607
608        self.term.queue_(cursor::SavePosition);
609    }
610
611    fn tick<T: Select>(
612        &mut self,
613        theme: &Theme,
614        nucleo: &mut Nucleo<usize>,
615        items: &[T],
616        redraw: Redraw,
617    ) -> Result<Result<Redraw, Stop>> {
618        if self.active {
619            let status = nucleo.tick(10);
620            let snap = nucleo.snapshot();
621
622            if status.changed {
623                self.scroll_offset = 0;
624                self.selected = self
625                    .selected
626                    .min(snap.matched_item_count().saturating_sub(1));
627            }
628
629            self.render_items(theme, snap, items, redraw)?;
630        }
631
632        let key = crossterm::event::read()?;
633        let changed = self.handle_event(&key);
634
635        let redraw = match changed {
636            Changed::Nothing => Redraw::Nothing,
637            Changed::Cursor => Redraw::Cursor,
638            Changed::Selection => Redraw::Selection,
639            Changed::Content => {
640                nucleo.pattern.reparse(
641                    0,
642                    self.input.content(),
643                    CaseMatching::Smart,
644                    Normalization::Smart,
645                    self.input.appending(),
646                );
647
648                Redraw::Content
649            }
650            Changed::Stop(stop) => return Ok(Err(stop)),
651        };
652
653        Ok(Ok(redraw))
654    }
655
656    fn render_items<T: Select>(
657        &mut self,
658        theme: &Theme,
659        snapshot: &Snapshot<usize>,
660        items: &[T],
661        redraw: Redraw,
662    ) -> Result<()> {
663        self.number_of_matches = snapshot.matched_item_count();
664
665        if redraw >= Redraw::Content {
666            let end = self
667                .scroll_offset
668                .saturating_add(self.height)
669                .min(snapshot.matched_item_count());
670            let start = self.scroll_offset.min(end.saturating_sub(1));
671
672            let matched_items = snapshot.matched_items(start..end).enumerate();
673
674            for (idx, item) in matched_items {
675                #[allow(clippy::cast_possible_truncation)]
676                let idx = idx as u32 + start;
677                let entry = &items[*item.data];
678
679                let (indicator, text, hl) = if idx == self.selected {
680                    (
681                        theme.selected_indicator,
682                        &theme.selected_text,
683                        &theme.selected_highlight,
684                    )
685                } else {
686                    (theme.indicator, &theme.text, &theme.highlight)
687                };
688
689                self.term
690                    .queue(cursor::MoveToNextLine(1))
691                    .queue(terminal::Clear(ClearType::CurrentLine))
692                    .queue(style::PrintStyledContent(indicator))
693                    .queue_(style::PrintStyledContent(text.apply(" ")));
694
695                if let Some(content) = entry.render_before_content() {
696                    let _ = self
697                        .term
698                        .queue(style::PrintStyledContent(text.apply(content)));
699                }
700
701                self.highlighter.render(
702                    *text,
703                    *hl,
704                    &mut self.term,
705                    snapshot.pattern().column_pattern(0),
706                    item.matcher_columns[0].slice(..),
707                );
708
709                if let Some(content) = entry.render_after_content() {
710                    self.term
711                        .queue_(style::PrintStyledContent(text.apply(content)));
712                }
713            }
714        }
715
716        if redraw >= Redraw::Content {
717            self.term
718                .queue(terminal::Clear(ClearType::FromCursorDown))
719                .queue(cursor::RestorePosition)
720                .queue(terminal::Clear(ClearType::UntilNewLine))
721                .queue_(style::PrintStyledContent(self.input.content().bold()));
722
723            let offset = self.input.cursor_offset_from_end();
724            if offset > 0 {
725                self.term.queue_(cursor::MoveLeft(offset));
726            }
727        } else if redraw == Redraw::Cursor {
728            self.term.queue_(cursor::RestorePosition);
729
730            let offset = self.input.cursor_offset_from_start();
731            if offset > 0 {
732                self.term.queue_(cursor::MoveRight(offset));
733            }
734        }
735
736        if redraw > Redraw::Nothing {
737            self.term.flush()?;
738        }
739
740        Ok(())
741    }
742
743    fn handle_event(&mut self, event: &Event) -> Changed {
744        match event {
745            Event::Key(KeyEvent {
746                code,
747                modifiers,
748                kind: KeyEventKind::Press,
749                ..
750            }) => return self.handle_key_event(*code, *modifiers),
751            Event::FocusLost => self.active = false,
752            Event::FocusGained => self.active = true,
753            Event::Resize(_, h) => {
754                self.height = u32::from(h.saturating_sub(1));
755                return Changed::Selection;
756            }
757            Event::Mouse(_) | Event::Paste(_) | Event::Key(_) => {}
758        };
759
760        Changed::Nothing
761    }
762
763    fn handle_key_event(&mut self, code: KeyCode, modifiers: KeyModifiers) -> Changed {
764        match (code, modifiers) {
765            (KeyCode::Esc, _) => match self.input.clear() {
766                Changed::Nothing => Changed::Stop(Stop::Quit),
767                otherwise => otherwise,
768            },
769            (KeyCode::Char('c'), KeyModifiers::CONTROL) => Changed::Stop(Stop::Quit),
770            (KeyCode::Enter | KeyCode::Char('\n' | '\r'), _) => {
771                Changed::Stop(Stop::Selected(self.selected))
772            }
773            (KeyCode::Backspace, _) => self.input.delete_left(),
774            (KeyCode::Delete, _) => self.input.delete_right(),
775            (KeyCode::Home, _) => self.input.move_to_start(),
776            (KeyCode::End, _) => self.input.move_to_end(),
777            (KeyCode::Left, m) => self
778                .input
779                .move_by(isize::from(m.contains(KeyModifiers::SHIFT)) * -9 - 1),
780            (KeyCode::Right, m) => self
781                .input
782                .move_by(isize::from(m.contains(KeyModifiers::SHIFT)) * 9 + 1),
783            (KeyCode::Up, m) => {
784                self.move_selection(i32::from(m.contains(KeyModifiers::SHIFT)) * -9 - 1)
785            }
786            (KeyCode::Down, m) => {
787                self.move_selection(i32::from(m.contains(KeyModifiers::SHIFT)) * 9 + 1)
788            }
789            (KeyCode::PageUp, _) => {
790                self.scroll_offset = self.scroll_offset.saturating_sub(self.height);
791                Changed::Selection
792            }
793            (KeyCode::PageDown, _) => {
794                self.scroll_offset = self.scroll_offset.saturating_add(self.height);
795                Changed::Selection
796            }
797            (KeyCode::Char(c), _) if self.filter => self.input.insert(c),
798            _ => Changed::Nothing,
799        }
800    }
801
802    fn move_selection(&mut self, diff: i32) -> Changed {
803        self.selected = self
804            .selected
805            .saturating_add_signed(diff)
806            .min(self.number_of_matches.saturating_sub(1));
807
808        if self.selected < self.scroll_offset {
809            self.scroll_offset = self.selected;
810        }
811
812        if self.selected >= self.scroll_offset.saturating_add(self.height) {
813            self.scroll_offset = self.selected.saturating_sub(self.height).saturating_add(1);
814        }
815
816        Changed::Selection
817    }
818}
819
820#[derive(Clone, Debug)]
821struct Input {
822    content: String,
823    boundaries: Vec<usize>,
824    cursor: usize,
825    appending: bool,
826}
827
828impl Input {
829    fn new(content: impl Into<String>) -> Self {
830        let content = content.into();
831        let boundaries = content
832            .grapheme_indices(true)
833            .map(|(idx, _)| idx)
834            .collect::<Vec<_>>();
835        let cursor = boundaries.len();
836        Self {
837            content,
838            boundaries,
839            cursor,
840            appending: true,
841        }
842    }
843
844    fn content(&self) -> &str {
845        &self.content
846    }
847
848    fn appending(&self) -> bool {
849        self.appending
850    }
851
852    fn cursor_offset_from_start(&self) -> u16 {
853        #[allow(clippy::cast_possible_truncation)]
854        let cursor = self.cursor.min(usize::from(u16::MAX)) as u16;
855
856        cursor
857    }
858
859    fn cursor_offset_from_end(&self) -> u16 {
860        #[allow(clippy::cast_possible_truncation)]
861        let cursor_offset = self
862            .boundaries
863            .len()
864            .saturating_sub(self.cursor)
865            .min(usize::from(u16::MAX)) as u16;
866
867        cursor_offset
868    }
869
870    fn clear(&mut self) -> Changed {
871        if self.content.is_empty() {
872            self.appending = true;
873            Changed::Nothing
874        } else {
875            self.content.clear();
876            self.boundaries.clear();
877            self.cursor = 0;
878            self.appending = false;
879            Changed::Content
880        }
881    }
882
883    fn insert(&mut self, c: char) -> Changed {
884        if self.cursor >= self.boundaries.len() {
885            self.appending = true;
886            self.boundaries.push(self.content.len());
887            self.cursor = self.boundaries.len();
888            self.content.push(c);
889        } else {
890            self.appending = false;
891            self.content.insert(self.boundaries[self.cursor], c);
892            self.boundaries
893                .insert(self.cursor, self.boundaries[self.cursor]);
894            let len = c.len_utf8();
895            self.boundaries[self.cursor + 1..]
896                .iter_mut()
897                .for_each(|b| *b += len);
898            self.cursor += 1;
899        }
900        Changed::Content
901    }
902
903    fn delete_left(&mut self) -> Changed {
904        if let Some(pos) = self.cursor.checked_sub(1) {
905            let indexes = self.boundaries[pos]..self.index_at_cursor();
906            self.content.replace_range(indexes, "");
907            let _ = self.boundaries.remove(pos);
908            self.cursor = pos;
909            self.appending = false;
910            Changed::Content
911        } else {
912            Changed::Nothing
913        }
914    }
915
916    fn delete_right(&mut self) -> Changed {
917        if self.cursor < self.boundaries.len() {
918            let start = self.boundaries.remove(self.cursor);
919            let indexes = start..self.index_at_cursor();
920            self.content.replace_range(indexes, "");
921            self.appending = false;
922            Changed::Content
923        } else {
924            Changed::Nothing
925        }
926    }
927
928    fn move_by(&mut self, diff: isize) -> Changed {
929        self.move_to(self.cursor.saturating_add_signed(diff))
930    }
931
932    fn move_to(&mut self, cursor: usize) -> Changed {
933        let changed = cursor != self.cursor;
934        self.cursor = cursor.min(self.boundaries.len());
935        if changed {
936            Changed::Cursor
937        } else {
938            Changed::Nothing
939        }
940    }
941
942    fn move_to_start(&mut self) -> Changed {
943        self.move_to(0)
944    }
945
946    fn move_to_end(&mut self) -> Changed {
947        self.move_to(self.boundaries.len())
948    }
949
950    fn index_at_cursor(&self) -> usize {
951        self.boundaries
952            .get(self.cursor)
953            .map_or_else(|| self.content.len(), |&i| i)
954    }
955}
956
957#[derive(Copy, Clone, Debug, PartialEq, Eq)]
958enum Changed {
959    Nothing,
960    Cursor,
961    Content,
962    Selection,
963    Stop(Stop),
964}
965
966#[derive(Copy, Clone, Debug, PartialEq, Eq)]
967enum Stop {
968    Quit,
969    Selected(u32),
970}
971
972#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
973enum Redraw {
974    Nothing,
975    Cursor,
976    Content,
977    Selection,
978}
979
980struct Highlighter {
981    highlight: bool,
982    indices: Vec<u32>,
983    content: String,
984    matcher: Matcher,
985}
986
987impl Highlighter {
988    fn new(highlight: bool) -> Self {
989        Self {
990            highlight,
991            indices: Vec::new(),
992            content: String::new(),
993            matcher: Matcher::new(Config::DEFAULT),
994        }
995    }
996
997    fn render(
998        &mut self,
999        text: ContentStyle,
1000        hl: ContentStyle,
1001        term: &mut Terminal,
1002        pattern: &Pattern,
1003        matched_item: Utf32Str<'_>,
1004    ) {
1005        if self.highlight {
1006            let _score = pattern.indices(matched_item, &mut self.matcher, &mut self.indices);
1007            self.indices.sort_unstable();
1008            self.indices.dedup();
1009
1010            let mut indices = self.indices.drain(..).map(|i| i as usize);
1011            let mut match_idx = indices.next().unwrap_or(usize::MAX);
1012
1013            for (grapheme_idx, grapheme) in matched_item.chars().enumerate() {
1014                if grapheme_idx == match_idx {
1015                    if !self.content.is_empty() {
1016                        term.queue_(style::PrintStyledContent(text.apply(self.content.as_str())));
1017                        self.content.clear();
1018                    }
1019
1020                    term.queue_(style::PrintStyledContent(hl.apply(grapheme)));
1021                    match_idx = indices.next().unwrap_or(usize::MAX);
1022                } else {
1023                    self.content.push(grapheme);
1024                }
1025            }
1026
1027            if !self.content.is_empty() {
1028                term.queue_(style::PrintStyledContent(text.apply(self.content.as_str())));
1029                self.content.clear();
1030            }
1031        } else {
1032            term.queue_(style::PrintStyledContent(text.apply(matched_item)));
1033        }
1034    }
1035}
1036
1037struct Terminal {
1038    io: StderrLock<'static>,
1039    err: Option<Error>,
1040    alternate_screen: bool,
1041}
1042
1043impl Terminal {
1044    fn new(color: Option<bool>, alternate_screen: bool) -> Result<Self> {
1045        terminal::enable_raw_mode().map_err(|e| match e.raw_os_error() {
1046            Some(25 | 6) => Error::NonInteractive,
1047            _ => e.into(),
1048        })?;
1049
1050        if let Some(color) = color {
1051            style::force_color_output(color);
1052        }
1053
1054        let mut io = io::stderr().lock();
1055        if alternate_screen {
1056            let _ = io.queue(terminal::EnterAlternateScreen)?;
1057        }
1058
1059        Ok(Self {
1060            io,
1061            err: None,
1062            alternate_screen,
1063        })
1064    }
1065
1066    fn queue(&mut self, cmd: impl crossterm::Command) -> &mut Self {
1067        if let Err(e) = self.io.queue(cmd) {
1068            if self.err.is_none() {
1069                self.err = Some(e.into());
1070            }
1071        };
1072        self
1073    }
1074
1075    fn queue_(&mut self, cmd: impl crossterm::Command) {
1076        let _ = self.queue(cmd);
1077    }
1078
1079    fn flush(&mut self) -> Result<()> {
1080        if let Some(e) = self.err.take() {
1081            return Err(e);
1082        }
1083
1084        self.io.flush()?;
1085
1086        Ok(())
1087    }
1088}
1089
1090impl Drop for Terminal {
1091    fn drop(&mut self) {
1092        self.queue_(cursor::Show);
1093        if self.alternate_screen {
1094            let _ = self.queue(terminal::LeaveAlternateScreen).flush();
1095        }
1096        let _ = terminal::disable_raw_mode();
1097    }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102    use super::*;
1103
1104    #[test]
1105    fn input_push_at_end() {
1106        let mut input = Input::new("foo");
1107
1108        assert_eq!(input.content(), "foo");
1109        assert_eq!(input.cursor_offset_from_start(), 3);
1110        assert_eq!(input.cursor_offset_from_end(), 0);
1111        assert!(input.appending());
1112
1113        let changed = input.insert('b');
1114        assert_eq!(input.content(), "foob");
1115        assert_eq!(input.cursor_offset_from_start(), 4);
1116        assert_eq!(input.cursor_offset_from_end(), 0);
1117        assert!(input.appending());
1118
1119        assert_eq!(changed, Changed::Content);
1120    }
1121
1122    #[test]
1123    fn input_push_at_middle() {
1124        let mut input = Input::new("foo");
1125        let changed = input.move_by(-1);
1126        assert_eq!(changed, Changed::Cursor);
1127
1128        assert_eq!(input.content(), "foo");
1129        assert_eq!(input.cursor_offset_from_start(), 2);
1130        assert_eq!(input.cursor_offset_from_end(), 1);
1131        assert!(input.appending());
1132
1133        let changed = input.insert('b');
1134        assert_eq!(input.content(), "fobo");
1135        assert_eq!(input.cursor_offset_from_start(), 3);
1136        assert_eq!(input.cursor_offset_from_end(), 1);
1137        assert!(!input.appending());
1138
1139        assert_eq!(changed, Changed::Content);
1140    }
1141
1142    #[test]
1143    fn input_delete_left() {
1144        let mut input = Input::new("foo");
1145
1146        assert_eq!(input.content(), "foo");
1147        assert_eq!(input.cursor_offset_from_start(), 3);
1148        assert_eq!(input.cursor_offset_from_end(), 0);
1149        assert!(input.appending());
1150
1151        let changed = input.delete_left();
1152        assert_eq!(input.content(), "fo");
1153        assert_eq!(input.cursor_offset_from_start(), 2);
1154        assert_eq!(input.cursor_offset_from_end(), 0);
1155        assert!(!input.appending());
1156
1157        assert_eq!(changed, Changed::Content);
1158    }
1159
1160    #[test]
1161    fn input_delete_left_from_middle() {
1162        let mut input = Input::new("fob");
1163        let changed = input.move_by(-1);
1164        assert_eq!(changed, Changed::Cursor);
1165
1166        assert_eq!(input.content(), "fob");
1167        assert_eq!(input.cursor_offset_from_start(), 2);
1168        assert_eq!(input.cursor_offset_from_end(), 1);
1169        assert!(input.appending());
1170
1171        let changed = input.delete_left();
1172        assert_eq!(input.content(), "fb");
1173        assert_eq!(input.cursor_offset_from_start(), 1);
1174        assert_eq!(input.cursor_offset_from_end(), 1);
1175        assert!(!input.appending());
1176
1177        assert_eq!(changed, Changed::Content);
1178    }
1179
1180    #[test]
1181    fn input_delete_left_from_start() {
1182        let mut input = Input::new("fob");
1183        let changed = input.move_to(0);
1184        assert_eq!(changed, Changed::Cursor);
1185
1186        assert_eq!(input.content(), "fob");
1187        assert_eq!(input.cursor_offset_from_start(), 0);
1188        assert_eq!(input.cursor_offset_from_end(), 3);
1189        assert!(input.appending());
1190
1191        let changed = input.delete_left();
1192        assert_eq!(input.content(), "fob");
1193        assert_eq!(input.cursor_offset_from_start(), 0);
1194        assert_eq!(input.cursor_offset_from_end(), 3);
1195        assert!(input.appending());
1196
1197        assert_eq!(changed, Changed::Nothing);
1198    }
1199
1200    #[test]
1201    fn input_delete_right() {
1202        let mut input = Input::new("foo");
1203
1204        assert_eq!(input.content(), "foo");
1205        assert_eq!(input.cursor_offset_from_start(), 3);
1206        assert_eq!(input.cursor_offset_from_end(), 0);
1207        assert!(input.appending());
1208
1209        let changed = input.delete_right();
1210        assert_eq!(input.content(), "foo");
1211        assert_eq!(input.cursor_offset_from_start(), 3);
1212        assert_eq!(input.cursor_offset_from_end(), 0);
1213        assert!(input.appending());
1214
1215        assert_eq!(changed, Changed::Nothing);
1216    }
1217
1218    #[test]
1219    fn input_delete_right_from_middle() {
1220        let mut input = Input::new("fob");
1221        let changed = input.move_by(-1);
1222        assert_eq!(changed, Changed::Cursor);
1223
1224        assert_eq!(input.content(), "fob");
1225        assert_eq!(input.cursor_offset_from_start(), 2);
1226        assert_eq!(input.cursor_offset_from_end(), 1);
1227        assert!(input.appending());
1228
1229        let changed = input.delete_right();
1230        assert_eq!(input.content(), "fo");
1231        assert_eq!(input.cursor_offset_from_start(), 2);
1232        assert_eq!(input.cursor_offset_from_end(), 0);
1233        assert!(!input.appending());
1234
1235        assert_eq!(changed, Changed::Content);
1236    }
1237
1238    #[test]
1239    fn input_delete_right_from_start() {
1240        let mut input = Input::new("fob");
1241        let changed = input.move_to(0);
1242        assert_eq!(changed, Changed::Cursor);
1243
1244        assert_eq!(input.content(), "fob");
1245        assert_eq!(input.cursor_offset_from_start(), 0);
1246        assert_eq!(input.cursor_offset_from_end(), 3);
1247        assert!(input.appending());
1248
1249        let changed = input.delete_right();
1250        assert_eq!(input.content(), "ob");
1251        assert_eq!(input.cursor_offset_from_start(), 0);
1252        assert_eq!(input.cursor_offset_from_end(), 2);
1253        assert!(!input.appending());
1254
1255        assert_eq!(changed, Changed::Content);
1256    }
1257
1258    #[test]
1259    fn input_move_by() {
1260        let mut input = Input::new("foo");
1261
1262        assert_eq!(input.content(), "foo");
1263        assert_eq!(input.cursor_offset_from_start(), 3);
1264        assert_eq!(input.cursor_offset_from_end(), 0);
1265        assert!(input.appending());
1266
1267        let changed = input.move_by(-1);
1268        assert_eq!(input.content(), "foo");
1269        assert_eq!(input.cursor_offset_from_start(), 2);
1270        assert_eq!(input.cursor_offset_from_end(), 1);
1271        assert!(input.appending());
1272
1273        assert_eq!(changed, Changed::Cursor);
1274
1275        let changed = input.move_by(1);
1276        assert_eq!(input.content(), "foo");
1277        assert_eq!(input.cursor_offset_from_start(), 3);
1278        assert_eq!(input.cursor_offset_from_end(), 0);
1279        assert!(input.appending());
1280
1281        assert_eq!(changed, Changed::Cursor);
1282    }
1283
1284    #[test]
1285    fn input_move_to() {
1286        let mut input = Input::new("foo");
1287
1288        assert_eq!(input.content(), "foo");
1289        assert_eq!(input.cursor_offset_from_start(), 3);
1290        assert_eq!(input.cursor_offset_from_end(), 0);
1291        assert!(input.appending());
1292
1293        let changed = input.move_to(1);
1294        assert_eq!(input.content(), "foo");
1295        assert_eq!(input.cursor_offset_from_start(), 1);
1296        assert_eq!(input.cursor_offset_from_end(), 2);
1297        assert!(input.appending());
1298
1299        assert_eq!(changed, Changed::Cursor);
1300
1301        let changed = input.move_to(3);
1302        assert_eq!(input.content(), "foo");
1303        assert_eq!(input.cursor_offset_from_start(), 3);
1304        assert_eq!(input.cursor_offset_from_end(), 0);
1305        assert!(input.appending());
1306
1307        assert_eq!(changed, Changed::Cursor);
1308    }
1309
1310    #[test]
1311    fn input_move_to_start_and_end() {
1312        let mut input = Input::new("foo");
1313
1314        assert_eq!(input.content(), "foo");
1315        assert_eq!(input.cursor_offset_from_start(), 3);
1316        assert_eq!(input.cursor_offset_from_end(), 0);
1317        assert!(input.appending());
1318
1319        let changed = input.move_to_start();
1320        assert_eq!(input.content(), "foo");
1321        assert_eq!(input.cursor_offset_from_start(), 0);
1322        assert_eq!(input.cursor_offset_from_end(), 3);
1323        assert!(input.appending());
1324
1325        assert_eq!(changed, Changed::Cursor);
1326
1327        let changed = input.move_to_end();
1328        assert_eq!(input.content(), "foo");
1329        assert_eq!(input.cursor_offset_from_start(), 3);
1330        assert_eq!(input.cursor_offset_from_end(), 0);
1331        assert!(input.appending());
1332
1333        assert_eq!(changed, Changed::Cursor);
1334    }
1335
1336    #[test]
1337    fn input_clear() {
1338        let mut input = Input::new("foo");
1339
1340        assert_eq!(input.content(), "foo");
1341        assert_eq!(input.cursor_offset_from_start(), 3);
1342        assert_eq!(input.cursor_offset_from_end(), 0);
1343        assert!(input.appending());
1344
1345        let changed = input.clear();
1346        assert_eq!(input.content(), "");
1347        assert_eq!(input.cursor_offset_from_start(), 0);
1348        assert_eq!(input.cursor_offset_from_end(), 0);
1349        assert!(!input.appending());
1350
1351        assert_eq!(changed, Changed::Content);
1352
1353        let changed = input.clear();
1354        assert_eq!(input.content(), "");
1355        assert_eq!(input.cursor_offset_from_start(), 0);
1356        assert_eq!(input.cursor_offset_from_end(), 0);
1357        assert!(input.appending());
1358
1359        assert_eq!(changed, Changed::Nothing);
1360    }
1361}