Skip to main content

rich_interact/components/
select.rs

1//! Fuzzy selection (#287), single and multiple, with a preview pane
2//! (#454).
3//!
4//! Typing filters the items with the [fuzzy](crate::fuzzy) matcher and
5//! highlights what matched; the arrows move; Enter picks. A multi-select
6//! marks items with Tab. When the focused item has a
7//! [`Preview`](crate::Preview), a pane shows it beside the list on a wide
8//! terminal and below it on a narrow one. An item's
9//! [`Action`](crate::Action) keys pick it and record which action was asked
10//! for; Ctrl+K opens a menu of every action for the focused item, its own
11//! and the view's ([`Actions`], #491).
12//!
13//! With the mouse on (#476), a click focuses a row and a second click on it
14//! picks it, the wheel moves, and the border between the list and a preview
15//! beside it can be dragged to resize the panes.
16//!
17//! Since 0.0.14 a select is made of the public [kit](crate::kit): a
18//! [`FilterState`] ranks the items against the query, a [`ListState`] holds
19//! the cursor, the scroll and the marks, a [`Divider`] is the preview's
20//! border and an [`ActionMenu`] is the Ctrl+K menu. Its keys are declared in
21//! a [`Keymap`] (context `select`), so they can be rebound and listed.
22
23use std::cell::Cell;
24use std::sync::mpsc::{Receiver, TryRecvError};
25use std::time::Duration;
26
27use rich::Segment;
28
29use crate::component::{Component, Context, Flow, View};
30use crate::components::{Theme, fit, highlight, pad, plain, question, text};
31use crate::event::{Button, Event, Key, KeyCode, Modifiers, Mouse, MouseKind};
32use crate::fuzzy::rank;
33use crate::item::{Action, ActionTarget, Actions, Item, TargetKind};
34use crate::keymap::{Keymap, keys};
35use crate::kit::{ActionMenu, Divider, FilterState, ListState, MenuReply};
36use crate::policy::{LineIo, NotInteractive};
37
38/// Where the preview pane goes.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub enum PreviewLayout {
41    /// Beside the list from 72 columns, below it when narrower.
42    #[default]
43    Auto,
44    Right,
45    Below,
46    Hidden,
47}
48
49/// Works out an item's [`ActionTarget::value`] from its index.
50pub(crate) type ValueOf = std::sync::Arc<dyn Fn(usize) -> String + Send + Sync>;
51
52/// The narrowest a pane gets when the border between them is dragged.
53const MIN_PANE: usize = 12;
54
55/// The keys of a [`Select`] (and of the views built on one), in context
56/// `select`. The `mark` actions apply to a [`MultiSelect`].
57pub fn select_keymap() -> Keymap {
58    Keymap::new("select")
59        .bind("pick", keys("enter"), "pick")
60        .bind("cancel", keys("escape"), "cancel")
61        .bind("up", keys("up ctrl+p"), "move up")
62        .bind("down", keys("down ctrl+n"), "move down")
63        .bind("page-up", keys("pageup"), "page up")
64        .bind("page-down", keys("pagedown"), "page down")
65        .bind("first", keys("home"), "first")
66        .bind("last", keys("end"), "last")
67        .bind("mark", keys("tab"), "mark and move down")
68        .bind("mark-up", keys("shift+tab"), "mark and move up")
69        .bind("mark-all", keys("ctrl+a"), "mark every match")
70        .bind("clear", keys("ctrl+u"), "clear the filter")
71        .bind("delete", keys("backspace"), "delete a character")
72        .bind("actions", keys("ctrl+k"), "actions")
73        .bind("reload", Vec::new(), "reload the items")
74}
75
76/// How often a select fed from a channel looks for new items.
77const FEED_POLL: Duration = Duration::from_millis(100);
78
79/// Where reloaded items come from (#485).
80type Source<T> = Box<dyn FnMut() -> Vec<Item<T>> + Send>;
81
82/// Actions only a multi-select has.
83const MARKING: [&str; 3] = ["mark", "mark-up", "mark-all"];
84
85/// A fuzzy single choice among items. Enter returns the focused item's
86/// value; Escape cancels.
87#[must_use = "a component does nothing until it is run or mounted"]
88pub struct Select<T> {
89    prompt: String,
90    items: Vec<Item<T>>,
91    /// The query, and the item indices that match it, best first, with the
92    /// label characters to highlight.
93    filter: FilterState,
94    /// The cursor over the matches, the first shown, and the marked items
95    /// (by item index).
96    list: ListState,
97    height: usize,
98    multi: bool,
99    preview: PreviewLayout,
100    preview_height: usize,
101    theme: Theme,
102    action: Option<String>,
103    default: Option<usize>,
104    /// Repaint at least this often, for previews that change by themselves.
105    repaint: Option<Duration>,
106    /// Once finished: the answer shown in place of the list (`None` inside
107    /// means cancelled).
108    answer: Option<Option<String>>,
109    /// What an action is done to here, and the actions offered on every
110    /// item (#491).
111    kind: TargetKind,
112    actions: Actions,
113    menu_key: Key,
114    menu: Option<ActionMenu>,
115    keymap: Keymap,
116    /// Each item's identity for an action ([`ActionTarget::value`]); the
117    /// label when empty.
118    values: Vec<String>,
119    /// Each item's identity where `values` has none, worked out when asked
120    /// (a tree node's path), so a large list keeps no string per item.
121    pub(crate) value_of: Option<ValueOf>,
122    /// A tree's guides, drawn for the rows on screen only (in place of
123    /// `prefixes`).
124    pub(crate) guides: Option<crate::components::views::Guides>,
125    /// A line above the list, under the question: a table's headings.
126    pub(crate) heading: Option<Vec<Segment>>,
127    /// Text before each label, not searched: a tree's guides, an emoji.
128    pub(crate) prefixes: Vec<String>,
129    /// Prefixes drawn as they are rather than dimmed.
130    pub(crate) prefix_plain: bool,
131    /// An icon after each prefix, by index: cells drawn as they are, style
132    /// metadata kept (a micro asset's placeholder, which the painter's
133    /// graphics draw over).
134    pub(crate) icons: Vec<Option<Vec<Segment>>>,
135    /// Items left out while nothing is typed: a collapsed node's children.
136    pub(crate) hidden: Vec<bool>,
137    /// A line of extra key hints for the footer.
138    pub(crate) hints: Option<String>,
139    /// Always take `height` rows, however few items there are: for lists
140    /// that change under the user, such as a directory's entries.
141    pub(crate) steady: bool,
142    mouse: bool,
143    /// The list position the last click focused, until a key: a click on
144    /// it again picks it.
145    clicked: Option<usize>,
146    /// Rows on screen, from the last context: the list is cut to fit.
147    space: Cell<usize>,
148    /// The border between the list and a preview beside it.
149    divider: Divider,
150    /// A line shown in place of the footer until the next key: what an
151    /// action just did ("copied path").
152    status: Option<Vec<Segment>>,
153    /// What the `reload` key reloads the items from (#485).
154    source: Option<Source<T>>,
155    /// New items sent from elsewhere, taken on every event and tick.
156    feed: Option<Receiver<Vec<Item<T>>>>,
157    /// A wide table's horizontal scroll (#717), set by the table before
158    /// each render: which cells of each label and of the heading show.
159    pub(crate) pan: Cell<Option<crate::components::views::Pan>>,
160}
161
162impl<T> Select<T> {
163    pub fn new<I, V>(prompt: impl Into<String>, items: I) -> Select<T>
164    where
165        I: IntoIterator<Item = V>,
166        V: Into<Item<T>>,
167    {
168        let items: Vec<Item<T>> = items.into_iter().map(Into::into).collect();
169        let mut list = ListState::new();
170        list.clear_selection(items.len());
171        let mut select = Select {
172            prompt: prompt.into(),
173            items,
174            filter: FilterState::default(),
175            list,
176            height: 10,
177            multi: false,
178            preview: PreviewLayout::Auto,
179            preview_height: 10,
180            theme: Theme::default(),
181            action: None,
182            default: None,
183            repaint: None,
184            answer: None,
185            kind: TargetKind::Item,
186            actions: Actions::new(),
187            menu_key: Key::ctrl('k'),
188            menu: None,
189            keymap: select_keymap(),
190            values: Vec::new(),
191            value_of: None,
192            guides: None,
193            heading: None,
194            prefixes: Vec::new(),
195            prefix_plain: false,
196            icons: Vec::new(),
197            hidden: Vec::new(),
198            hints: None,
199            steady: false,
200            mouse: false,
201            clicked: None,
202            space: Cell::new(usize::MAX),
203            divider: Divider::new(3).min(MIN_PANE),
204            status: None,
205            source: None,
206            feed: None,
207            pan: Cell::new(None),
208        };
209        select.refilter();
210        select
211    }
212
213    /// Show at most `rows` items at once (default 10).
214    pub fn height(mut self, rows: usize) -> Self {
215        self.height = rows.max(1);
216        self
217    }
218
219    pub fn preview(mut self, layout: PreviewLayout) -> Self {
220        self.preview = layout;
221        self
222    }
223
224    /// Rows the preview takes when it is below the list (default 10).
225    pub fn preview_height(mut self, rows: usize) -> Self {
226        self.preview_height = rows.max(1);
227        self
228    }
229
230    pub fn theme(mut self, theme: Theme) -> Self {
231        self.theme = theme;
232        self
233    }
234
235    /// Start with this filter text.
236    pub fn query(mut self, query: impl Into<String>) -> Self {
237        *self.filter.query_mut() = query.into();
238        self.refilter();
239        self
240    }
241
242    /// Render again at least every `interval`, even with no key pressed:
243    /// for previews that fill in by themselves, such as a command's output
244    /// computed on another thread. An unchanged view still writes nothing.
245    pub fn repaint_every(mut self, interval: Duration) -> Self {
246        self.repaint = Some(interval);
247        self
248    }
249
250    /// The item returned without a terminal when the policy asks for
251    /// defaults, and focused first.
252    pub fn default(mut self, index: usize) -> Self {
253        if index < self.items.len() {
254            self.default = Some(index);
255            if let Some(position) = self.filter.position_of(index) {
256                self.list.set_cursor(position);
257            }
258        }
259        self
260    }
261
262    /// Actions offered on every item, beside each item's own (#491): in
263    /// the action menu, and on their keys.
264    pub fn actions(mut self, actions: Actions) -> Self {
265        self.actions = actions;
266        self
267    }
268
269    /// The key that opens the action menu (default Ctrl+K).
270    pub fn menu_key(mut self, key: Key) -> Self {
271        self.menu_key = key;
272        self.keymap.rebind("actions", [key]);
273        self
274    }
275
276    /// Make `keys` do `action` (see [`select_keymap`]) on this select only;
277    /// no keys unbinds it.
278    pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
279        self.keymap.rebind(action, keys);
280        self
281    }
282
283    /// Report the mouse (#476): a click focuses a row, a second click picks
284    /// it, and the border beside a preview drags.
285    pub fn with_mouse(mut self, on: bool) -> Self {
286        self.mouse = on;
287        self
288    }
289
290    /// Reload the items from `source` when `key` is pressed (#485), as
291    /// [`reload`](Self::reload) does: the query, the focused item and the
292    /// marks stay.
293    pub fn reload_on(
294        mut self,
295        key: Key,
296        source: impl FnMut() -> Vec<Item<T>> + Send + 'static,
297    ) -> Self {
298        self.keymap.rebind("reload", [key]);
299        self.source = Some(Box::new(source));
300        self
301    }
302
303    /// Take new items from `feed` whenever they arrive (#485): a watcher or
304    /// a command on another thread sends the whole list again, and the
305    /// select [reloads](Self::reload) it, keeping the query, the focused
306    /// item and the marks. The select looks every 100 ms.
307    pub fn reload_from(mut self, feed: Receiver<Vec<Item<T>>>) -> Self {
308        self.feed = Some(feed);
309        self
310    }
311
312    /// The list's width beside a preview, in columns: where the border is
313    /// until it is dragged.
314    pub fn split(mut self, columns: usize) -> Self {
315        self.divider.set_position(Some(columns));
316        self
317    }
318
319    /// The id of the [`Action`] that picked the item, from its key or the
320    /// action menu, if one did.
321    pub fn action(&self) -> Option<&str> {
322        self.action.as_deref()
323    }
324
325    pub fn items(&self) -> &[Item<T>] {
326        &self.items
327    }
328
329    /// The focused item's index, if any item matches.
330    pub fn focused(&self) -> Option<usize> {
331        self.filter.index(self.list.cursor())
332    }
333
334    /// The list's width beside a preview, once dragged.
335    pub fn split_width(&self) -> Option<usize> {
336        self.divider.position()
337    }
338
339    /// The query and what matches it.
340    pub fn filter(&self) -> &FilterState {
341        &self.filter
342    }
343
344    /// The cursor, the scroll and the marks.
345    pub fn list(&self) -> &ListState {
346        &self.list
347    }
348
349    /// The border beside the preview.
350    pub fn divider(&self) -> &Divider {
351        &self.divider
352    }
353
354    /// The action menu, while it is open.
355    pub fn menu(&self) -> Option<&ActionMenu> {
356        self.menu.as_ref()
357    }
358
359    /// The keys, with any rebinding.
360    pub fn select_keymap(&self) -> &Keymap {
361        &self.keymap
362    }
363
364    /// Whether this is the list of a [`MultiSelect`].
365    pub fn is_multi(&self) -> bool {
366        self.multi
367    }
368
369    /// What an action is done to: an item, a row, a node, a file.
370    pub fn set_kind(&mut self, kind: TargetKind) {
371        self.kind = kind;
372    }
373
374    /// Each item's identity for an action ([`ActionTarget::value`]), by
375    /// index; the label where missing.
376    pub fn set_values(&mut self, values: Vec<String>) {
377        self.values = values;
378    }
379
380    /// A line above the list, under the question: a table's headings.
381    pub fn set_heading(&mut self, heading: Option<Vec<Segment>>) {
382        self.heading = heading;
383    }
384
385    /// Text before each label, by index, not searched: a tree's guides, an
386    /// emoji. Dimmed, unless `plain`. Call [`refilter`](Self::refilter)
387    /// after.
388    pub fn set_prefixes(&mut self, prefixes: Vec<String>, plain: bool) {
389        self.prefixes = prefixes;
390        self.prefix_plain = plain;
391    }
392
393    /// An icon before each label, by index, after its prefix: one line of
394    /// text (see [`kit::icon`](crate::kit::icon)), its styles and metadata
395    /// kept, so a micro asset's placeholder cells stay tagged for the
396    /// painter's graphics. `None` leaves an item without one.
397    pub fn set_icons(&mut self, icons: Vec<Option<rich::Text>>) {
398        self.icons = icons
399            .iter()
400            .map(|icon| icon.as_ref().map(crate::kit::icon))
401            .collect();
402    }
403
404    /// Items left out while nothing is typed, by index: a collapsed node's
405    /// children. Call [`refilter`](Self::refilter) after.
406    pub fn set_hidden(&mut self, hidden: Vec<bool>) {
407        self.hidden = hidden;
408    }
409
410    /// A line of extra key hints for the footer.
411    pub fn set_hints(&mut self, hints: Option<String>) {
412        self.hints = hints;
413    }
414
415    /// Always take the full height, however few items there are: for lists
416    /// that change under the user, such as a directory's entries.
417    pub fn set_steady(&mut self, steady: bool) {
418        self.steady = steady;
419    }
420
421    /// Replace the items and keep the user's place (#485): the query stays
422    /// and is matched against the new items, the focused item stays
423    /// focused if it is still there (matched by label), and so do the
424    /// marks. Per-item settings (values, prefixes, what is hidden) are
425    /// cleared, as with [`replace_items`](Self::replace_items); a view that
426    /// sets them sets them again.
427    pub fn reload(&mut self, items: Vec<Item<T>>) {
428        let focused = self.focused().map(|index| self.items[index].label.clone());
429        let marked: std::collections::HashSet<String> = self
430            .list
431            .selected()
432            .into_iter()
433            .map(|index| self.items[index].label.clone())
434            .collect();
435        let offset = self.list.offset();
436        self.list.clear_selection(items.len());
437        for (index, item) in items.iter().enumerate() {
438            if marked.contains(&item.label) {
439                self.list.set_selected(index, true);
440            }
441        }
442        self.items = items;
443        self.values.clear();
444        self.value_of = None;
445        self.guides = None;
446        self.prefixes.clear();
447        self.icons.clear();
448        self.hidden.clear();
449        if self.default.is_some_and(|index| index >= self.items.len()) {
450            self.default = None;
451        }
452        self.menu = None;
453        self.clicked = None;
454        self.refilter();
455        let target = focused
456            .and_then(|label| self.items.iter().position(|item| item.label == label))
457            .and_then(|index| self.filter.position_of(index));
458        if let Some(position) = target {
459            // Where it was on screen, if that still works.
460            self.list.set_offset(offset.min(position));
461            self.list.set_cursor(position);
462            self.scroll();
463        }
464    }
465
466    /// Take the newest list sent through [`reload_from`](Self::reload_from).
467    fn poll_feed(&mut self) {
468        let Some(feed) = &self.feed else { return };
469        let mut latest = None;
470        loop {
471            match feed.try_recv() {
472                Ok(items) => latest = Some(items),
473                Err(TryRecvError::Empty) => break,
474                Err(TryRecvError::Disconnected) => {
475                    self.feed = None;
476                    break;
477                }
478            }
479        }
480        if let Some(items) = latest {
481            self.reload(items);
482        }
483    }
484
485    /// Show `status` in place of the footer until the next key: what an
486    /// action just did.
487    pub fn set_status(&mut self, status: Option<String>) {
488        self.status = status.map(|status| vec![text(format!("  {status}"), &self.theme.hint)]);
489    }
490
491    /// The status line, if one is showing.
492    pub fn status(&self) -> Option<&[Segment]> {
493        self.status.as_deref()
494    }
495
496    /// Filter as a tree (#428; see [`FilterState::set_tree`]): matches keep
497    /// their ancestors, dimmed, and the cursor goes to the best match.
498    pub fn set_tree(&mut self, parents: Option<Vec<Option<usize>>>) {
499        self.filter.set_tree(parents);
500        self.refilter();
501    }
502
503    /// How often to tick: the repaint interval, and often enough to take
504    /// fed items.
505    pub fn tick_interval(&self) -> Option<Duration> {
506        match (self.repaint, self.feed.is_some()) {
507            (Some(repaint), true) => Some(repaint.min(FEED_POLL)),
508            (None, true) => Some(FEED_POLL),
509            (repaint, false) => repaint,
510        }
511    }
512
513    /// Replace the items, keeping the settings; the query is cleared.
514    pub fn replace_items(&mut self, items: Vec<Item<T>>) {
515        self.list.clear_selection(items.len());
516        self.items = items;
517        self.values.clear();
518        self.value_of = None;
519        self.guides = None;
520        self.prefixes.clear();
521        self.icons.clear();
522        self.hidden.clear();
523        self.filter.query_mut().clear();
524        self.default = None;
525        self.menu = None;
526        self.list.set_cursor(0);
527        // A click on the old items is not the first of a pair on the new
528        // ones, which may have another item at the same position.
529        self.clicked = None;
530        self.refilter();
531    }
532
533    /// Whether the action menu is open.
534    pub fn menu_open(&self) -> bool {
535        self.menu.is_some()
536    }
537
538    /// What is typed.
539    pub fn query_text(&self) -> &str {
540        self.filter.query()
541    }
542
543    /// Show `answer` in place of the list (`None`: cancelled), as a
544    /// finished select does.
545    pub fn set_answer(&mut self, answer: Option<String>) {
546        self.answer = Some(answer);
547    }
548
549    /// Carry on after an Enter that did not finish (a directory opened).
550    pub fn reopen(&mut self) {
551        self.answer = None;
552        self.action = None;
553    }
554
555    /// Focus item `index` if it is listed.
556    pub fn focus_item(&mut self, index: usize) {
557        if let Some(position) = self.filter.position_of(index) {
558            self.list.set_cursor(position);
559            self.scroll();
560        }
561    }
562
563    /// The target an action on item `index` is done to.
564    pub fn target(&self, index: usize) -> ActionTarget {
565        let label = self.items[index].label.clone();
566        ActionTarget {
567            kind: self.kind,
568            index,
569            value: self
570                .values
571                .get(index)
572                .cloned()
573                .or_else(|| self.value_of.as_ref().map(|value| value(index)))
574                .or_else(|| {
575                    let guides = self.guides.as_ref()?;
576                    Some(guides.path(index, |node| self.items[node].label.as_str()))
577                })
578                .unwrap_or_else(|| label.clone()),
579            label,
580        }
581    }
582
583    /// Every action for item `index`: its own, then the view's that apply.
584    pub fn actions_for(&self, index: usize) -> Vec<Action> {
585        self.actions
586            .for_target(&self.target(index), &self.items[index].actions)
587    }
588
589    /// Match the items against the query again, after the items, their
590    /// prefixes or what is hidden changed.
591    pub fn refilter(&mut self) {
592        let focused = self.focused();
593        self.filter.set_hidden(self.hidden.clone());
594        self.filter.set_highlight_limits(
595            self.items
596                .iter()
597                .map(|item| item.label.chars().count())
598                .collect(),
599        );
600        self.filter
601            .set_candidates(self.items.iter().map(Item::search_text));
602        // Keep the focused item when it still matches, else the best match.
603        let cursor = focused
604            .and_then(|index| self.filter.position_of(index))
605            .filter(|_| !self.filter.is_filtering())
606            .or_else(|| self.filter.best())
607            .unwrap_or(0);
608        self.list.set_len(self.filter.len());
609        self.list.reset(cursor, self.shown());
610    }
611
612    fn scroll(&mut self) {
613        self.list.follow(self.shown());
614    }
615
616    fn step(&mut self, delta: isize) {
617        self.list.step(delta, self.shown());
618    }
619
620    /// Handle an event as the select does, for a component built round
621    /// one: `Some(Flow::Done)` finishes with item indices (the view then
622    /// collapses to the answer), `Some(Flow::Cancel)` cancels,
623    /// `Some(Flow::Ignored)` means the event was not the select's, and
624    /// `None` carries on.
625    pub fn event(&mut self, event: &Event, context: &Context<'_>) -> Option<Flow<Vec<usize>>> {
626        self.space.set(context.height);
627        self.poll_feed();
628        if matches!(event, Event::Key(_) | Event::Paste(_) | Event::Mouse(_)) {
629            self.status = None;
630        }
631        let flow = self.event_inner(event, context.width);
632        match &flow {
633            Some(Flow::Done(indices)) => {
634                let labels: Vec<&str> = indices
635                    .iter()
636                    .map(|&index| self.items[index].label.as_str())
637                    .collect();
638                self.answer = Some(Some(labels.join(", ")));
639            }
640            Some(Flow::Cancel) => self.answer = Some(None),
641            _ => {}
642        }
643        flow
644    }
645
646    /// Pick `index` with `action`.
647    fn run_action(&mut self, index: usize, action: &Action) -> Option<Flow<Vec<usize>>> {
648        self.menu = None;
649        self.action = Some(action.id.clone());
650        Some(Flow::Done(vec![index]))
651    }
652
653    fn pick_focused(&mut self) -> Option<Flow<Vec<usize>>> {
654        let index = self.focused()?;
655        let marked = self.list.selected();
656        Some(Flow::Done(if self.multi && !marked.is_empty() {
657            marked
658        } else {
659            vec![index]
660        }))
661    }
662
663    fn menu_event(&mut self, event: &Event, width: usize) -> Option<Flow<Vec<usize>>> {
664        let index = self.focused()?;
665        // The menu is a modal over the list: see `menu_box`.
666        let (x, y, outer_width, outer_height) = self.menu_box(width)?;
667        if let Some(mouse) = event.mouse() {
668            let (column, row) = (mouse.column as usize, mouse.row as usize);
669            let inside = (x..x + outer_width).contains(&column)
670                && (y + 1..y + outer_height - 1).contains(&row);
671            if mouse.is_click() && !inside {
672                self.menu = None;
673                return None;
674            }
675        }
676        match self.menu.as_mut()?.handle(event, y + 1) {
677            MenuReply::Stay => None,
678            MenuReply::Close => {
679                self.menu = None;
680                None
681            }
682            MenuReply::Run(action) => self.run_action(index, &action),
683        }
684    }
685
686    /// The action `key` triggers here. A key with modifiers that is not
687    /// bound as it is does what it does without them, unless it types.
688    fn key_action(&self, key: Key) -> Option<&str> {
689        let action = self.keymap.action(key).or_else(|| {
690            let typing = matches!(key.code, KeyCode::Char(_));
691            (!typing && key.modifiers != Modifiers::NONE)
692                .then(|| self.keymap.action(Key::new(key.code)))
693                .flatten()
694        })?;
695        (self.multi || !MARKING.contains(&action)).then_some(action)
696    }
697
698    fn event_inner(&mut self, event: &Event, width: usize) -> Option<Flow<Vec<usize>>> {
699        if self.menu.is_some() {
700            return self.menu_event(event, width);
701        }
702        if let Event::Mouse(mouse) = event {
703            return self.mouse_event(*mouse, width);
704        }
705        if let Event::Paste(text) = event {
706            self.clicked = None;
707            self.filter
708                .query_mut()
709                .push_str(&crate::components::pasted(text, " "));
710            self.refilter();
711            return None;
712        }
713        let key = event.key()?;
714        // A click after a key is a first click again.
715        self.clicked = None;
716        if let Some(index) = self.focused() {
717            let actions = self.actions_for(index);
718            if self.keymap.is(key, "actions") && !actions.is_empty() {
719                self.menu = Some(ActionMenu::new(actions, key));
720                return None;
721            }
722            if let Some(at) = key.pick(actions.iter().map(|action| action.key.as_slice())) {
723                let action = actions[at].clone();
724                return self.run_action(index, &action);
725            }
726        }
727        match self.key_action(key) {
728            Some("reload") if self.source.is_some() => {
729                if let Some(source) = self.source.as_mut() {
730                    let items = source();
731                    self.reload(items);
732                }
733            }
734            Some("pick") => return self.pick_focused(),
735            Some("cancel") => return Some(Flow::Cancel),
736            Some("up") => self.step(-1),
737            Some("down") => self.step(1),
738            Some("page-up") => self.step(-(self.shown() as isize)),
739            Some("page-down") => self.step(self.shown() as isize),
740            Some("first") => self.step(isize::MIN / 2),
741            Some("last") => self.step(isize::MAX / 2),
742            Some(mark @ ("mark" | "mark-up")) => {
743                let down = mark == "mark";
744                if let Some(index) = self.focused() {
745                    self.list.toggle(index);
746                    self.step(if down { 1 } else { -1 });
747                }
748            }
749            Some("mark-all") => {
750                let matches: Vec<usize> = self.filter.matches().iter().map(|(i, _)| *i).collect();
751                let all = matches.iter().all(|&index| self.list.is_selected(index));
752                for index in matches {
753                    self.list.set_selected(index, !all);
754                }
755            }
756            Some("clear") => {
757                self.filter.query_mut().clear();
758                self.refilter();
759            }
760            Some("delete") => {
761                if self.filter.query_mut().pop().is_some() {
762                    self.refilter();
763                }
764            }
765            _ => match key.code {
766                KeyCode::Char(c) if !key.modifiers.ctrl && !key.modifiers.alt => {
767                    self.filter.query_mut().push(c);
768                    self.refilter();
769                }
770                _ => return Some(Flow::Ignored),
771            },
772        }
773        None
774    }
775
776    /// Rows above the list: the question, and the heading if any.
777    fn top(&self) -> usize {
778        1 + usize::from(self.heading.is_some())
779    }
780
781    /// The most rows the list takes: the height asked for, cut to what
782    /// fits on screen beside the question, the heading and the footer.
783    fn shown(&self) -> usize {
784        self.height
785            .min(self.space.get().saturating_sub(self.top() + 1))
786            .max(1)
787    }
788
789    /// Rows the list takes.
790    fn rows(&self) -> usize {
791        if self.steady {
792            return self.shown();
793        }
794        self.shown().min(self.items.len()).max(1)
795    }
796
797    /// Where the action menu's modal goes at `width`, border included:
798    /// `(x, y, width, height)`, centred over the list, from its first row.
799    fn menu_box(&self, width: usize) -> Option<(usize, usize, usize, usize)> {
800        let menu = self.menu.as_ref()?;
801        let (inner, rows) = menu.size();
802        let outer = (inner + 2).min(width.max(2));
803        Some(((width - outer) / 2, self.top(), outer, rows + 2))
804    }
805
806    /// The list's width in a view `width` wide: less a preview beside it.
807    pub(crate) fn list_width(&self, width: usize) -> usize {
808        match self.layout(width) {
809            (PreviewLayout::Right, left) => left,
810            _ => width,
811        }
812    }
813
814    /// The preview's layout at `width` for the focused item, and the
815    /// list's width when the preview is beside it.
816    fn layout(&self, width: usize) -> (PreviewLayout, usize) {
817        let preview = self
818            .focused()
819            .is_some_and(|index| self.items[index].preview.is_some())
820            && self.preview != PreviewLayout::Hidden;
821        if !preview {
822            return (PreviewLayout::Hidden, width);
823        }
824        let layout = match self.preview {
825            PreviewLayout::Auto if width >= 72 => PreviewLayout::Right,
826            PreviewLayout::Auto => PreviewLayout::Below,
827            layout => layout,
828        };
829        let left = self
830            .divider
831            .resolve(width, (width * 45 / 100).max(20).min(width));
832        (layout, left)
833    }
834
835    fn mouse_event(&mut self, mouse: Mouse, width: usize) -> Option<Flow<Vec<usize>>> {
836        let (layout, left) = self.layout(width);
837        let (row, column) = (mouse.row as usize, mouse.column as usize);
838        let top = self.top();
839        let in_list = row >= top && row < top + self.rows();
840        match mouse.kind {
841            MouseKind::ScrollUp => self.step(-1),
842            MouseKind::ScrollDown => self.step(1),
843            MouseKind::Down(Button::Left) => {
844                let border = layout == PreviewLayout::Right && self.divider.hit(column, left);
845                if border && in_list {
846                    self.divider.press(column, left);
847                } else if in_list && (layout != PreviewLayout::Right || column < left) {
848                    let position = self.list.offset() + row - top;
849                    if position < self.filter.len() {
850                        if self.clicked == Some(position) && position == self.list.cursor() {
851                            return self.pick_focused();
852                        }
853                        self.list.set_cursor(position);
854                        self.clicked = Some(position);
855                    }
856                }
857            }
858            MouseKind::Drag(Button::Left) if self.divider.dragging() => {
859                self.divider.drag(column, width);
860            }
861            MouseKind::Up(_) => self.divider.release(),
862            _ => {}
863        }
864        None
865    }
866
867    fn row(&self, position: usize, width: usize) -> Vec<Segment> {
868        let (index, positions) = &self.filter.matches()[position];
869        let item = &self.items[*index];
870        let theme = &self.theme;
871        let focused = position == self.list.cursor();
872        let mut line = if focused {
873            vec![text(format!("{} ", theme.pointer), &theme.pointer_style)]
874        } else {
875            vec![plain("  ")]
876        };
877        if self.multi {
878            line.push(if self.list.is_selected(*index) {
879                text(format!("{} ", theme.checked), &theme.checked_style)
880            } else {
881                text(format!("{} ", theme.unchecked), &theme.hint)
882            });
883        }
884        let prefix = match &self.guides {
885            Some(guides) => Some(guides.prefix(*index)),
886            None => self.prefixes.get(*index).cloned(),
887        };
888        if let Some(prefix) = prefix.filter(|p| !p.is_empty()) {
889            line.push(if self.prefix_plain {
890                plain(prefix)
891            } else {
892                text(prefix, &theme.hint)
893            });
894        }
895        if let Some(Some(icon)) = self.icons.get(*index) {
896            line.extend(icon.iter().cloned());
897            line.push(plain(" "));
898        }
899        // An ancestor kept only to place a match (a tree's filter) is dim.
900        let base = if self.filter.is_context(position) {
901            Some(&theme.hint)
902        } else {
903            focused.then_some(&theme.focused)
904        };
905        let label = highlight(&item.label, positions, base, &theme.matched);
906        match self.pan.get() {
907            Some(pan) => line.extend(pan.window(&label)),
908            None => line.extend(label),
909        }
910        if let Some(description) = &item.description {
911            line.push(text(format!("  {description}"), &theme.hint));
912        }
913        fit(line, width)
914    }
915
916    /// The list's rows at `width`: the matches shown, or "no matches", kept
917    /// at a steady height.
918    pub fn list_lines(&self, width: usize) -> Vec<Vec<Segment>> {
919        let rows = self.rows();
920        let mut lines: Vec<Vec<Segment>> = (self.list.offset()..self.filter.len())
921            .take(rows)
922            .map(|position| self.row(position, width))
923            .collect();
924        if self.filter.is_empty() {
925            lines.push(vec![text("  no matches", &self.theme.hint)]);
926        }
927        // A steady height, so the footer does not jump as the list filters.
928        lines.resize_with(rows, Vec::new);
929        lines
930    }
931
932    /// The footer: counts and key hints.
933    pub fn footer(&self, width: usize) -> Vec<Segment> {
934        let mut hint = format!("{}/{}", self.filter.len(), self.items.len());
935        if self.multi {
936            let marked = self.list.selected_count();
937            hint.push_str(&format!(" · {marked} marked · tab mark"));
938        }
939        hint.push_str(" · ↑↓ move · enter pick");
940        if let Some(extra) = &self.hints {
941            hint.push_str(&format!(" · {extra}"));
942        }
943        if self
944            .focused()
945            .is_some_and(|index| !self.actions_for(index).is_empty())
946        {
947            hint.push_str(&format!(" · {} actions", self.menu_key));
948        }
949        hint.push_str(" · esc cancel");
950        fit(vec![text(format!("  {hint}"), &self.theme.hint)], width)
951    }
952
953    fn render_view(&self, context: &Context<'_>) -> View {
954        self.space.set(context.height);
955        let width = context.width;
956        let theme = &self.theme;
957        let mut header = question(theme, &self.prompt);
958        if let Some(answer) = &self.answer {
959            header.push(match answer {
960                Some(answer) => text(answer.clone(), &theme.answer),
961                None => text("cancelled", &theme.hint),
962            });
963            return View::new(vec![fit(header, width)]);
964        }
965        let query = self.filter.query();
966        let column = crate::components::width(&header) + rich::cells::cell_len(query);
967        header.push(plain(query.to_string()));
968        let mut lines = vec![fit(header, width)];
969        if let Some(heading) = &self.heading {
970            let indent = if self.multi { "    " } else { "  " };
971            match self.pan.get() {
972                Some(pan) => lines.push(pan.heading(indent, heading, self.list_width(width))),
973                None => {
974                    let mut line = vec![plain(indent)];
975                    line.extend(heading.iter().cloned());
976                    lines.push(fit(line, width));
977                }
978            }
979        }
980        let footer = |this: &Self| match &this.status {
981            Some(status) => vec![fit(status.clone(), width)],
982            None => vec![this.footer(width)],
983        };
984        let preview = self
985            .focused()
986            .and_then(|index| self.items[index].preview.as_ref());
987        match (preview, self.layout(width)) {
988            (Some(preview), (PreviewLayout::Right, left)) => {
989                let right = width.saturating_sub(left + 3).max(1);
990                let list = self.list_lines(left);
991                let shown = context.lines_at(&*preview.renderable(), right);
992                for (row, line) in list.into_iter().enumerate() {
993                    let mut line = pad(line, left);
994                    line.push(text(" │ ", &theme.border));
995                    if let Some(preview) = shown.get(row) {
996                        line.extend(fit(preview.clone(), right));
997                    }
998                    lines.push(line);
999                }
1000                lines.extend(footer(self));
1001            }
1002            (Some(preview), (PreviewLayout::Below, _)) => {
1003                lines.extend(self.list_lines(width));
1004                lines.extend(footer(self));
1005                lines.push(vec![text("─".repeat(width), &theme.border)]);
1006                let shown = context.lines_at(&*preview.renderable(), width);
1007                lines.extend(
1008                    shown
1009                        .into_iter()
1010                        .take(self.preview_height)
1011                        .map(|line| fit(line, width)),
1012                );
1013            }
1014            _ => {
1015                lines.extend(self.list_lines(width));
1016                lines.extend(footer(self));
1017            }
1018        }
1019        // The action menu (#474): a modal over the view, titled with its
1020        // target, dimming what is under it.
1021        if let (Some(menu), Some((x, y, outer, _))) = (&self.menu, self.menu_box(width)) {
1022            let inner = outer.saturating_sub(2);
1023            let title = self
1024                .focused()
1025                .map(|index| format!("Actions · {}", self.items[index].label));
1026            let boxed = crate::kit::frame(
1027                menu.render(theme, inner),
1028                inner,
1029                title.as_deref(),
1030                &theme.border,
1031            );
1032            let dim = rich::Style::parse("dim").expect("a built-in style");
1033            crate::kit::place(&mut lines, x, y, &boxed, Some(&dim));
1034            return View::new(lines);
1035        }
1036        View::new(lines).with_cursor(0, column.min(width.saturating_sub(1)))
1037    }
1038
1039    /// The keys that apply now: no marking for a single choice, and the
1040    /// action menu's while it is open.
1041    pub fn visible_keymap(&self) -> Keymap {
1042        if self.menu.is_some() {
1043            return Keymap::new("menu")
1044                .bind("up", keys("up"), "move up")
1045                .bind("down", keys("down tab"), "move down")
1046                .bind("run", keys("enter"), "run")
1047                .bind("close", keys("escape"), "close");
1048        }
1049        let mut keymap = Keymap::new("select");
1050        for binding in self.keymap.bindings() {
1051            let action = binding.action.as_str();
1052            let shown = (self.multi || !MARKING.contains(&action))
1053                && (action != "reload" || self.source.is_some());
1054            if shown {
1055                keymap.add(binding);
1056            }
1057        }
1058        keymap
1059    }
1060
1061    /// Ask without a terminal: list the items, read a number or text.
1062    fn ask(&mut self, io: &mut dyn LineIo) -> Result<Option<Vec<usize>>, NotInteractive> {
1063        io.write(&format!("{}\n", self.prompt));
1064        for (index, item) in self.items.iter().enumerate() {
1065            let mut row = format!("  {}) {}", index + 1, item.label);
1066            if let Some(description) = &item.description {
1067                row.push_str(&format!(" — {description}"));
1068            }
1069            io.write(&format!("{row}\n"));
1070        }
1071        io.write(if self.multi {
1072            "Numbers or names, separated by commas: "
1073        } else {
1074            "Number or name: "
1075        });
1076        // At the end of input, or on an empty line, the default answers:
1077        // the default item, or with `multi` the marked ones.
1078        let default: Vec<usize> = if self.multi {
1079            self.list.selected()
1080        } else {
1081            self.default.into_iter().collect()
1082        };
1083        let Some(line) = io.read_line() else {
1084            if default.is_empty() {
1085                return Err(NotInteractive::Ended);
1086            }
1087            return Ok(Some(default));
1088        };
1089        if line.trim().is_empty() && !default.is_empty() {
1090            return Ok(Some(default));
1091        }
1092        let answers: Vec<&str> = if self.multi {
1093            line.split(',')
1094                .map(str::trim)
1095                .filter(|a| !a.is_empty())
1096                .collect()
1097        } else {
1098            vec![line.trim()]
1099        };
1100        let texts: Vec<String> = self.items.iter().map(Item::search_text).collect();
1101        let mut chosen = Vec::new();
1102        for answer in answers {
1103            let index = match answer.parse::<usize>() {
1104                Ok(number) if (1..=self.items.len()).contains(&number) => number - 1,
1105                _ => rank(answer, texts.iter().map(String::as_str))
1106                    .first()
1107                    .map(|(index, _)| *index)
1108                    .filter(|_| !answer.is_empty())
1109                    .ok_or_else(|| {
1110                        NotInteractive::Invalid(format!("no item matches {answer:?}"))
1111                    })?,
1112            };
1113            if !chosen.contains(&index) {
1114                chosen.push(index);
1115            }
1116        }
1117        if chosen.is_empty() {
1118            return Err(NotInteractive::Invalid("nothing chosen".into()));
1119        }
1120        Ok(Some(chosen))
1121    }
1122}
1123
1124/// The list's width beside a preview when the border is at `column`: both
1125/// panes at least [`MIN_PANE`] wide when there is room for that.
1126#[cfg(test)]
1127fn clamp_split(column: usize, width: usize) -> usize {
1128    Divider::new(3).min(MIN_PANE).clamp(column, width)
1129}
1130
1131impl<T: Clone> Component for Select<T> {
1132    type Output = T;
1133
1134    fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<T> {
1135        match self.event(event, context) {
1136            Some(Flow::Done(indices)) => Flow::Done(self.items[indices[0]].value.clone()),
1137            Some(Flow::Cancel) => Flow::Cancel,
1138            Some(Flow::Ignored) => Flow::Ignored,
1139            _ => Flow::Continue,
1140        }
1141    }
1142
1143    fn render(&self, context: &Context<'_>) -> View {
1144        self.render_view(context)
1145    }
1146
1147    fn tick(&self) -> Option<Duration> {
1148        self.tick_interval()
1149    }
1150
1151    fn mouse(&self) -> bool {
1152        self.mouse
1153    }
1154
1155    fn keymap(&self) -> Keymap {
1156        self.visible_keymap()
1157    }
1158
1159    fn default_value(&self) -> Option<T> {
1160        self.default.map(|index| self.items[index].value.clone())
1161    }
1162
1163    fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<T>, NotInteractive> {
1164        Ok(self
1165            .ask(io)?
1166            .map(|indices| self.items[indices[0]].value.clone()))
1167    }
1168}
1169
1170/// A fuzzy multiple choice: Tab marks items (Shift+Tab marks and moves up,
1171/// Ctrl+A marks every match), Enter returns the marked items in list
1172/// order, or the focused one when none is marked.
1173#[must_use = "a component does nothing until it is run or mounted"]
1174pub struct MultiSelect<T>(Select<T>);
1175
1176impl<T> MultiSelect<T> {
1177    pub fn new<I, V>(prompt: impl Into<String>, items: I) -> MultiSelect<T>
1178    where
1179        I: IntoIterator<Item = V>,
1180        V: Into<Item<T>>,
1181    {
1182        let mut select = Select::new(prompt, items);
1183        select.multi = true;
1184        MultiSelect(select)
1185    }
1186
1187    pub fn height(self, rows: usize) -> Self {
1188        MultiSelect(self.0.height(rows))
1189    }
1190
1191    pub fn preview(self, layout: PreviewLayout) -> Self {
1192        MultiSelect(self.0.preview(layout))
1193    }
1194
1195    pub fn theme(self, theme: Theme) -> Self {
1196        MultiSelect(self.0.theme(theme))
1197    }
1198
1199    pub fn query(self, query: impl Into<String>) -> Self {
1200        MultiSelect(self.0.query(query))
1201    }
1202
1203    /// See [`Select::repaint_every`].
1204    pub fn repaint_every(self, interval: Duration) -> Self {
1205        MultiSelect(self.0.repaint_every(interval))
1206    }
1207
1208    /// See [`Select::actions`].
1209    pub fn actions(self, actions: Actions) -> Self {
1210        MultiSelect(self.0.actions(actions))
1211    }
1212
1213    /// See [`Select::with_mouse`].
1214    pub fn with_mouse(self, on: bool) -> Self {
1215        MultiSelect(self.0.with_mouse(on))
1216    }
1217
1218    /// See [`Select::reload_on`]: marks stay with their items.
1219    pub fn reload_on(
1220        self,
1221        key: Key,
1222        source: impl FnMut() -> Vec<Item<T>> + Send + 'static,
1223    ) -> Self {
1224        MultiSelect(self.0.reload_on(key, source))
1225    }
1226
1227    /// See [`Select::reload_from`].
1228    pub fn reload_from(self, feed: Receiver<Vec<Item<T>>>) -> Self {
1229        MultiSelect(self.0.reload_from(feed))
1230    }
1231
1232    /// Replace the items, keeping the query, the focus and the marks: see
1233    /// [`Select::reload`].
1234    pub fn reload(&mut self, items: Vec<Item<T>>) {
1235        self.0.reload(items);
1236    }
1237
1238    /// Mark these items to begin with; they are also the default without a
1239    /// terminal.
1240    pub fn marked(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
1241        for index in indices {
1242            if index < self.0.items.len() {
1243                self.0.list.set_selected(index, true);
1244            }
1245        }
1246        self
1247    }
1248
1249    pub fn action(&self) -> Option<&str> {
1250        self.0.action()
1251    }
1252
1253    pub fn items(&self) -> &[Item<T>] {
1254        self.0.items()
1255    }
1256}
1257
1258impl<T: Clone> Component for MultiSelect<T> {
1259    type Output = Vec<T>;
1260
1261    fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<Vec<T>> {
1262        match self.0.event(event, context) {
1263            Some(Flow::Done(indices)) => Flow::Done(
1264                indices
1265                    .into_iter()
1266                    .map(|index| self.0.items[index].value.clone())
1267                    .collect(),
1268            ),
1269            Some(Flow::Cancel) => Flow::Cancel,
1270            Some(Flow::Ignored) => Flow::Ignored,
1271            _ => Flow::Continue,
1272        }
1273    }
1274
1275    fn render(&self, context: &Context<'_>) -> View {
1276        self.0.render_view(context)
1277    }
1278
1279    fn keymap(&self) -> Keymap {
1280        self.0.visible_keymap()
1281    }
1282
1283    fn tick(&self) -> Option<Duration> {
1284        self.0.tick_interval()
1285    }
1286
1287    fn mouse(&self) -> bool {
1288        self.0.mouse
1289    }
1290
1291    fn default_value(&self) -> Option<Vec<T>> {
1292        Some(
1293            self.0
1294                .list
1295                .selected()
1296                .into_iter()
1297                .map(|index| self.0.items[index].value.clone())
1298                .collect(),
1299        )
1300    }
1301
1302    fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<Vec<T>>, NotInteractive> {
1303        Ok(self.0.ask(io)?.map(|indices| {
1304            indices
1305                .into_iter()
1306                .map(|index| self.0.items[index].value.clone())
1307                .collect()
1308        }))
1309    }
1310}
1311
1312#[cfg(test)]
1313mod tests {
1314    use super::*;
1315
1316    #[test]
1317    fn a_dragged_split_keeps_both_panes_usable() {
1318        assert_eq!(clamp_split(5, 80), MIN_PANE);
1319        assert_eq!(clamp_split(40, 80), 40);
1320        assert_eq!(clamp_split(79, 80), 80 - MIN_PANE - 3);
1321        // Too narrow for both minimums: the list keeps what it can.
1322        assert!(clamp_split(3, 20) <= 20);
1323    }
1324}