1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub enum PreviewLayout {
41 #[default]
43 Auto,
44 Right,
45 Below,
46 Hidden,
47}
48
49pub(crate) type ValueOf = std::sync::Arc<dyn Fn(usize) -> String + Send + Sync>;
51
52const MIN_PANE: usize = 12;
54
55pub 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
76const FEED_POLL: Duration = Duration::from_millis(100);
78
79type Source<T> = Box<dyn FnMut() -> Vec<Item<T>> + Send>;
81
82const MARKING: [&str; 3] = ["mark", "mark-up", "mark-all"];
84
85#[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 filter: FilterState,
94 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: Option<Duration>,
106 answer: Option<Option<String>>,
109 kind: TargetKind,
112 actions: Actions,
113 menu_key: Key,
114 menu: Option<ActionMenu>,
115 keymap: Keymap,
116 values: Vec<String>,
119 pub(crate) value_of: Option<ValueOf>,
122 pub(crate) guides: Option<crate::components::views::Guides>,
125 pub(crate) heading: Option<Vec<Segment>>,
127 pub(crate) prefixes: Vec<String>,
129 pub(crate) prefix_plain: bool,
131 pub(crate) icons: Vec<Option<Vec<Segment>>>,
135 pub(crate) hidden: Vec<bool>,
137 pub(crate) hints: Option<String>,
139 pub(crate) steady: bool,
142 mouse: bool,
143 clicked: Option<usize>,
146 space: Cell<usize>,
148 divider: Divider,
150 status: Option<Vec<Segment>>,
153 source: Option<Source<T>>,
155 feed: Option<Receiver<Vec<Item<T>>>>,
157 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 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 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 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 pub fn repaint_every(mut self, interval: Duration) -> Self {
246 self.repaint = Some(interval);
247 self
248 }
249
250 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 pub fn actions(mut self, actions: Actions) -> Self {
265 self.actions = actions;
266 self
267 }
268
269 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 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
279 self.keymap.rebind(action, keys);
280 self
281 }
282
283 pub fn with_mouse(mut self, on: bool) -> Self {
286 self.mouse = on;
287 self
288 }
289
290 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 pub fn reload_from(mut self, feed: Receiver<Vec<Item<T>>>) -> Self {
308 self.feed = Some(feed);
309 self
310 }
311
312 pub fn split(mut self, columns: usize) -> Self {
315 self.divider.set_position(Some(columns));
316 self
317 }
318
319 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 pub fn focused(&self) -> Option<usize> {
331 self.filter.index(self.list.cursor())
332 }
333
334 pub fn split_width(&self) -> Option<usize> {
336 self.divider.position()
337 }
338
339 pub fn filter(&self) -> &FilterState {
341 &self.filter
342 }
343
344 pub fn list(&self) -> &ListState {
346 &self.list
347 }
348
349 pub fn divider(&self) -> &Divider {
351 &self.divider
352 }
353
354 pub fn menu(&self) -> Option<&ActionMenu> {
356 self.menu.as_ref()
357 }
358
359 pub fn select_keymap(&self) -> &Keymap {
361 &self.keymap
362 }
363
364 pub fn is_multi(&self) -> bool {
366 self.multi
367 }
368
369 pub fn set_kind(&mut self, kind: TargetKind) {
371 self.kind = kind;
372 }
373
374 pub fn set_values(&mut self, values: Vec<String>) {
377 self.values = values;
378 }
379
380 pub fn set_heading(&mut self, heading: Option<Vec<Segment>>) {
382 self.heading = heading;
383 }
384
385 pub fn set_prefixes(&mut self, prefixes: Vec<String>, plain: bool) {
389 self.prefixes = prefixes;
390 self.prefix_plain = plain;
391 }
392
393 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 pub fn set_hidden(&mut self, hidden: Vec<bool>) {
407 self.hidden = hidden;
408 }
409
410 pub fn set_hints(&mut self, hints: Option<String>) {
412 self.hints = hints;
413 }
414
415 pub fn set_steady(&mut self, steady: bool) {
418 self.steady = steady;
419 }
420
421 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 self.list.set_offset(offset.min(position));
461 self.list.set_cursor(position);
462 self.scroll();
463 }
464 }
465
466 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 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 pub fn status(&self) -> Option<&[Segment]> {
493 self.status.as_deref()
494 }
495
496 pub fn set_tree(&mut self, parents: Option<Vec<Option<usize>>>) {
499 self.filter.set_tree(parents);
500 self.refilter();
501 }
502
503 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 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 self.clicked = None;
530 self.refilter();
531 }
532
533 pub fn menu_open(&self) -> bool {
535 self.menu.is_some()
536 }
537
538 pub fn query_text(&self) -> &str {
540 self.filter.query()
541 }
542
543 pub fn set_answer(&mut self, answer: Option<String>) {
546 self.answer = Some(answer);
547 }
548
549 pub fn reopen(&mut self) {
551 self.answer = None;
552 self.action = None;
553 }
554
555 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 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 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 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 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 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 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 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 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 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 fn top(&self) -> usize {
778 1 + usize::from(self.heading.is_some())
779 }
780
781 fn shown(&self) -> usize {
784 self.height
785 .min(self.space.get().saturating_sub(self.top() + 1))
786 .max(1)
787 }
788
789 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 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 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 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 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 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 lines.resize_with(rows, Vec::new);
929 lines
930 }
931
932 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 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 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 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 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#[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#[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 pub fn repaint_every(self, interval: Duration) -> Self {
1205 MultiSelect(self.0.repaint_every(interval))
1206 }
1207
1208 pub fn actions(self, actions: Actions) -> Self {
1210 MultiSelect(self.0.actions(actions))
1211 }
1212
1213 pub fn with_mouse(self, on: bool) -> Self {
1215 MultiSelect(self.0.with_mouse(on))
1216 }
1217
1218 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 pub fn reload_from(self, feed: Receiver<Vec<Item<T>>>) -> Self {
1229 MultiSelect(self.0.reload_from(feed))
1230 }
1231
1232 pub fn reload(&mut self, items: Vec<Item<T>>) {
1235 self.0.reload(items);
1236 }
1237
1238 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 assert!(clamp_split(3, 20) <= 20);
1323 }
1324}