use std::cell::Cell;
use std::sync::mpsc::{Receiver, TryRecvError};
use std::time::Duration;
use rich::Segment;
use crate::component::{Component, Context, Flow, View};
use crate::components::{Theme, fit, highlight, pad, plain, question, text};
use crate::event::{Button, Event, Key, KeyCode, Modifiers, Mouse, MouseKind};
use crate::fuzzy::rank;
use crate::item::{Action, ActionTarget, Actions, Item, TargetKind};
use crate::keymap::{Keymap, keys};
use crate::kit::{ActionMenu, Divider, FilterState, ListState, MenuReply};
use crate::policy::{LineIo, NotInteractive};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PreviewLayout {
#[default]
Auto,
Right,
Below,
Hidden,
}
pub(crate) type ValueOf = std::sync::Arc<dyn Fn(usize) -> String + Send + Sync>;
const MIN_PANE: usize = 12;
pub fn select_keymap() -> Keymap {
Keymap::new("select")
.bind("pick", keys("enter"), "pick")
.bind("cancel", keys("escape"), "cancel")
.bind("up", keys("up ctrl+p"), "move up")
.bind("down", keys("down ctrl+n"), "move down")
.bind("page-up", keys("pageup"), "page up")
.bind("page-down", keys("pagedown"), "page down")
.bind("first", keys("home"), "first")
.bind("last", keys("end"), "last")
.bind("mark", keys("tab"), "mark and move down")
.bind("mark-up", keys("shift+tab"), "mark and move up")
.bind("mark-all", keys("ctrl+a"), "mark every match")
.bind("clear", keys("ctrl+u"), "clear the filter")
.bind("delete", keys("backspace"), "delete a character")
.bind("actions", keys("ctrl+k"), "actions")
.bind("reload", Vec::new(), "reload the items")
}
const FEED_POLL: Duration = Duration::from_millis(100);
type Source<T> = Box<dyn FnMut() -> Vec<Item<T>> + Send>;
const MARKING: [&str; 3] = ["mark", "mark-up", "mark-all"];
#[must_use = "a component does nothing until it is run or mounted"]
pub struct Select<T> {
prompt: String,
items: Vec<Item<T>>,
filter: FilterState,
list: ListState,
height: usize,
multi: bool,
preview: PreviewLayout,
preview_height: usize,
theme: Theme,
action: Option<String>,
default: Option<usize>,
repaint: Option<Duration>,
answer: Option<Option<String>>,
kind: TargetKind,
actions: Actions,
menu_key: Key,
menu: Option<ActionMenu>,
keymap: Keymap,
values: Vec<String>,
pub(crate) value_of: Option<ValueOf>,
pub(crate) guides: Option<crate::components::views::Guides>,
pub(crate) heading: Option<Vec<Segment>>,
pub(crate) prefixes: Vec<String>,
pub(crate) prefix_plain: bool,
pub(crate) icons: Vec<Option<Vec<Segment>>>,
pub(crate) hidden: Vec<bool>,
pub(crate) hints: Option<String>,
pub(crate) steady: bool,
mouse: bool,
clicked: Option<usize>,
space: Cell<usize>,
divider: Divider,
status: Option<Vec<Segment>>,
source: Option<Source<T>>,
feed: Option<Receiver<Vec<Item<T>>>>,
pub(crate) pan: Cell<Option<crate::components::views::Pan>>,
}
impl<T> Select<T> {
pub fn new<I, V>(prompt: impl Into<String>, items: I) -> Select<T>
where
I: IntoIterator<Item = V>,
V: Into<Item<T>>,
{
let items: Vec<Item<T>> = items.into_iter().map(Into::into).collect();
let mut list = ListState::new();
list.clear_selection(items.len());
let mut select = Select {
prompt: prompt.into(),
items,
filter: FilterState::default(),
list,
height: 10,
multi: false,
preview: PreviewLayout::Auto,
preview_height: 10,
theme: Theme::default(),
action: None,
default: None,
repaint: None,
answer: None,
kind: TargetKind::Item,
actions: Actions::new(),
menu_key: Key::ctrl('k'),
menu: None,
keymap: select_keymap(),
values: Vec::new(),
value_of: None,
guides: None,
heading: None,
prefixes: Vec::new(),
prefix_plain: false,
icons: Vec::new(),
hidden: Vec::new(),
hints: None,
steady: false,
mouse: false,
clicked: None,
space: Cell::new(usize::MAX),
divider: Divider::new(3).min(MIN_PANE),
status: None,
source: None,
feed: None,
pan: Cell::new(None),
};
select.refilter();
select
}
pub fn height(mut self, rows: usize) -> Self {
self.height = rows.max(1);
self
}
pub fn preview(mut self, layout: PreviewLayout) -> Self {
self.preview = layout;
self
}
pub fn preview_height(mut self, rows: usize) -> Self {
self.preview_height = rows.max(1);
self
}
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn query(mut self, query: impl Into<String>) -> Self {
*self.filter.query_mut() = query.into();
self.refilter();
self
}
pub fn repaint_every(mut self, interval: Duration) -> Self {
self.repaint = Some(interval);
self
}
pub fn default(mut self, index: usize) -> Self {
if index < self.items.len() {
self.default = Some(index);
if let Some(position) = self.filter.position_of(index) {
self.list.set_cursor(position);
}
}
self
}
pub fn actions(mut self, actions: Actions) -> Self {
self.actions = actions;
self
}
pub fn menu_key(mut self, key: Key) -> Self {
self.menu_key = key;
self.keymap.rebind("actions", [key]);
self
}
pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
self.keymap.rebind(action, keys);
self
}
pub fn with_mouse(mut self, on: bool) -> Self {
self.mouse = on;
self
}
pub fn reload_on(
mut self,
key: Key,
source: impl FnMut() -> Vec<Item<T>> + Send + 'static,
) -> Self {
self.keymap.rebind("reload", [key]);
self.source = Some(Box::new(source));
self
}
pub fn reload_from(mut self, feed: Receiver<Vec<Item<T>>>) -> Self {
self.feed = Some(feed);
self
}
pub fn split(mut self, columns: usize) -> Self {
self.divider.set_position(Some(columns));
self
}
pub fn action(&self) -> Option<&str> {
self.action.as_deref()
}
pub fn items(&self) -> &[Item<T>] {
&self.items
}
pub fn focused(&self) -> Option<usize> {
self.filter.index(self.list.cursor())
}
pub fn split_width(&self) -> Option<usize> {
self.divider.position()
}
pub fn filter(&self) -> &FilterState {
&self.filter
}
pub fn list(&self) -> &ListState {
&self.list
}
pub fn divider(&self) -> &Divider {
&self.divider
}
pub fn menu(&self) -> Option<&ActionMenu> {
self.menu.as_ref()
}
pub fn select_keymap(&self) -> &Keymap {
&self.keymap
}
pub fn is_multi(&self) -> bool {
self.multi
}
pub fn set_kind(&mut self, kind: TargetKind) {
self.kind = kind;
}
pub fn set_values(&mut self, values: Vec<String>) {
self.values = values;
}
pub fn set_heading(&mut self, heading: Option<Vec<Segment>>) {
self.heading = heading;
}
pub fn set_prefixes(&mut self, prefixes: Vec<String>, plain: bool) {
self.prefixes = prefixes;
self.prefix_plain = plain;
}
pub fn set_icons(&mut self, icons: Vec<Option<rich::Text>>) {
self.icons = icons
.iter()
.map(|icon| icon.as_ref().map(crate::kit::icon))
.collect();
}
pub fn set_hidden(&mut self, hidden: Vec<bool>) {
self.hidden = hidden;
}
pub fn set_hints(&mut self, hints: Option<String>) {
self.hints = hints;
}
pub fn set_steady(&mut self, steady: bool) {
self.steady = steady;
}
pub fn reload(&mut self, items: Vec<Item<T>>) {
let focused = self.focused().map(|index| self.items[index].label.clone());
let marked: std::collections::HashSet<String> = self
.list
.selected()
.into_iter()
.map(|index| self.items[index].label.clone())
.collect();
let offset = self.list.offset();
self.list.clear_selection(items.len());
for (index, item) in items.iter().enumerate() {
if marked.contains(&item.label) {
self.list.set_selected(index, true);
}
}
self.items = items;
self.values.clear();
self.value_of = None;
self.guides = None;
self.prefixes.clear();
self.icons.clear();
self.hidden.clear();
if self.default.is_some_and(|index| index >= self.items.len()) {
self.default = None;
}
self.menu = None;
self.clicked = None;
self.refilter();
let target = focused
.and_then(|label| self.items.iter().position(|item| item.label == label))
.and_then(|index| self.filter.position_of(index));
if let Some(position) = target {
self.list.set_offset(offset.min(position));
self.list.set_cursor(position);
self.scroll();
}
}
fn poll_feed(&mut self) {
let Some(feed) = &self.feed else { return };
let mut latest = None;
loop {
match feed.try_recv() {
Ok(items) => latest = Some(items),
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
self.feed = None;
break;
}
}
}
if let Some(items) = latest {
self.reload(items);
}
}
pub fn set_status(&mut self, status: Option<String>) {
self.status = status.map(|status| vec![text(format!(" {status}"), &self.theme.hint)]);
}
pub fn status(&self) -> Option<&[Segment]> {
self.status.as_deref()
}
pub fn set_tree(&mut self, parents: Option<Vec<Option<usize>>>) {
self.filter.set_tree(parents);
self.refilter();
}
pub fn tick_interval(&self) -> Option<Duration> {
match (self.repaint, self.feed.is_some()) {
(Some(repaint), true) => Some(repaint.min(FEED_POLL)),
(None, true) => Some(FEED_POLL),
(repaint, false) => repaint,
}
}
pub fn replace_items(&mut self, items: Vec<Item<T>>) {
self.list.clear_selection(items.len());
self.items = items;
self.values.clear();
self.value_of = None;
self.guides = None;
self.prefixes.clear();
self.icons.clear();
self.hidden.clear();
self.filter.query_mut().clear();
self.default = None;
self.menu = None;
self.list.set_cursor(0);
self.clicked = None;
self.refilter();
}
pub fn menu_open(&self) -> bool {
self.menu.is_some()
}
pub fn query_text(&self) -> &str {
self.filter.query()
}
pub fn set_answer(&mut self, answer: Option<String>) {
self.answer = Some(answer);
}
pub fn reopen(&mut self) {
self.answer = None;
self.action = None;
}
pub fn focus_item(&mut self, index: usize) {
if let Some(position) = self.filter.position_of(index) {
self.list.set_cursor(position);
self.scroll();
}
}
pub fn target(&self, index: usize) -> ActionTarget {
let label = self.items[index].label.clone();
ActionTarget {
kind: self.kind,
index,
value: self
.values
.get(index)
.cloned()
.or_else(|| self.value_of.as_ref().map(|value| value(index)))
.or_else(|| {
let guides = self.guides.as_ref()?;
Some(guides.path(index, |node| self.items[node].label.as_str()))
})
.unwrap_or_else(|| label.clone()),
label,
}
}
pub fn actions_for(&self, index: usize) -> Vec<Action> {
self.actions
.for_target(&self.target(index), &self.items[index].actions)
}
pub fn refilter(&mut self) {
let focused = self.focused();
self.filter.set_hidden(self.hidden.clone());
self.filter.set_highlight_limits(
self.items
.iter()
.map(|item| item.label.chars().count())
.collect(),
);
self.filter
.set_candidates(self.items.iter().map(Item::search_text));
let cursor = focused
.and_then(|index| self.filter.position_of(index))
.filter(|_| !self.filter.is_filtering())
.or_else(|| self.filter.best())
.unwrap_or(0);
self.list.set_len(self.filter.len());
self.list.reset(cursor, self.shown());
}
fn scroll(&mut self) {
self.list.follow(self.shown());
}
fn step(&mut self, delta: isize) {
self.list.step(delta, self.shown());
}
pub fn event(&mut self, event: &Event, context: &Context<'_>) -> Option<Flow<Vec<usize>>> {
self.space.set(context.height);
self.poll_feed();
if matches!(event, Event::Key(_) | Event::Paste(_) | Event::Mouse(_)) {
self.status = None;
}
let flow = self.event_inner(event, context.width);
match &flow {
Some(Flow::Done(indices)) => {
let labels: Vec<&str> = indices
.iter()
.map(|&index| self.items[index].label.as_str())
.collect();
self.answer = Some(Some(labels.join(", ")));
}
Some(Flow::Cancel) => self.answer = Some(None),
_ => {}
}
flow
}
fn run_action(&mut self, index: usize, action: &Action) -> Option<Flow<Vec<usize>>> {
self.menu = None;
self.action = Some(action.id.clone());
Some(Flow::Done(vec![index]))
}
fn pick_focused(&mut self) -> Option<Flow<Vec<usize>>> {
let index = self.focused()?;
let marked = self.list.selected();
Some(Flow::Done(if self.multi && !marked.is_empty() {
marked
} else {
vec![index]
}))
}
fn menu_event(&mut self, event: &Event, width: usize) -> Option<Flow<Vec<usize>>> {
let index = self.focused()?;
let (x, y, outer_width, outer_height) = self.menu_box(width)?;
if let Some(mouse) = event.mouse() {
let (column, row) = (mouse.column as usize, mouse.row as usize);
let inside = (x..x + outer_width).contains(&column)
&& (y + 1..y + outer_height - 1).contains(&row);
if mouse.is_click() && !inside {
self.menu = None;
return None;
}
}
match self.menu.as_mut()?.handle(event, y + 1) {
MenuReply::Stay => None,
MenuReply::Close => {
self.menu = None;
None
}
MenuReply::Run(action) => self.run_action(index, &action),
}
}
fn key_action(&self, key: Key) -> Option<&str> {
let action = self.keymap.action(key).or_else(|| {
let typing = matches!(key.code, KeyCode::Char(_));
(!typing && key.modifiers != Modifiers::NONE)
.then(|| self.keymap.action(Key::new(key.code)))
.flatten()
})?;
(self.multi || !MARKING.contains(&action)).then_some(action)
}
fn event_inner(&mut self, event: &Event, width: usize) -> Option<Flow<Vec<usize>>> {
if self.menu.is_some() {
return self.menu_event(event, width);
}
if let Event::Mouse(mouse) = event {
return self.mouse_event(*mouse, width);
}
if let Event::Paste(text) = event {
self.clicked = None;
self.filter
.query_mut()
.push_str(&crate::components::pasted(text, " "));
self.refilter();
return None;
}
let key = event.key()?;
self.clicked = None;
if let Some(index) = self.focused() {
let actions = self.actions_for(index);
if self.keymap.is(key, "actions") && !actions.is_empty() {
self.menu = Some(ActionMenu::new(actions, key));
return None;
}
if let Some(at) = key.pick(actions.iter().map(|action| action.key.as_slice())) {
let action = actions[at].clone();
return self.run_action(index, &action);
}
}
match self.key_action(key) {
Some("reload") if self.source.is_some() => {
if let Some(source) = self.source.as_mut() {
let items = source();
self.reload(items);
}
}
Some("pick") => return self.pick_focused(),
Some("cancel") => return Some(Flow::Cancel),
Some("up") => self.step(-1),
Some("down") => self.step(1),
Some("page-up") => self.step(-(self.shown() as isize)),
Some("page-down") => self.step(self.shown() as isize),
Some("first") => self.step(isize::MIN / 2),
Some("last") => self.step(isize::MAX / 2),
Some(mark @ ("mark" | "mark-up")) => {
let down = mark == "mark";
if let Some(index) = self.focused() {
self.list.toggle(index);
self.step(if down { 1 } else { -1 });
}
}
Some("mark-all") => {
let matches: Vec<usize> = self.filter.matches().iter().map(|(i, _)| *i).collect();
let all = matches.iter().all(|&index| self.list.is_selected(index));
for index in matches {
self.list.set_selected(index, !all);
}
}
Some("clear") => {
self.filter.query_mut().clear();
self.refilter();
}
Some("delete") => {
if self.filter.query_mut().pop().is_some() {
self.refilter();
}
}
_ => match key.code {
KeyCode::Char(c) if !key.modifiers.ctrl && !key.modifiers.alt => {
self.filter.query_mut().push(c);
self.refilter();
}
_ => return Some(Flow::Ignored),
},
}
None
}
fn top(&self) -> usize {
1 + usize::from(self.heading.is_some())
}
fn shown(&self) -> usize {
self.height
.min(self.space.get().saturating_sub(self.top() + 1))
.max(1)
}
fn rows(&self) -> usize {
if self.steady {
return self.shown();
}
self.shown().min(self.items.len()).max(1)
}
fn menu_box(&self, width: usize) -> Option<(usize, usize, usize, usize)> {
let menu = self.menu.as_ref()?;
let (inner, rows) = menu.size();
let outer = (inner + 2).min(width.max(2));
Some(((width - outer) / 2, self.top(), outer, rows + 2))
}
pub(crate) fn list_width(&self, width: usize) -> usize {
match self.layout(width) {
(PreviewLayout::Right, left) => left,
_ => width,
}
}
fn layout(&self, width: usize) -> (PreviewLayout, usize) {
let preview = self
.focused()
.is_some_and(|index| self.items[index].preview.is_some())
&& self.preview != PreviewLayout::Hidden;
if !preview {
return (PreviewLayout::Hidden, width);
}
let layout = match self.preview {
PreviewLayout::Auto if width >= 72 => PreviewLayout::Right,
PreviewLayout::Auto => PreviewLayout::Below,
layout => layout,
};
let left = self
.divider
.resolve(width, (width * 45 / 100).max(20).min(width));
(layout, left)
}
fn mouse_event(&mut self, mouse: Mouse, width: usize) -> Option<Flow<Vec<usize>>> {
let (layout, left) = self.layout(width);
let (row, column) = (mouse.row as usize, mouse.column as usize);
let top = self.top();
let in_list = row >= top && row < top + self.rows();
match mouse.kind {
MouseKind::ScrollUp => self.step(-1),
MouseKind::ScrollDown => self.step(1),
MouseKind::Down(Button::Left) => {
let border = layout == PreviewLayout::Right && self.divider.hit(column, left);
if border && in_list {
self.divider.press(column, left);
} else if in_list && (layout != PreviewLayout::Right || column < left) {
let position = self.list.offset() + row - top;
if position < self.filter.len() {
if self.clicked == Some(position) && position == self.list.cursor() {
return self.pick_focused();
}
self.list.set_cursor(position);
self.clicked = Some(position);
}
}
}
MouseKind::Drag(Button::Left) if self.divider.dragging() => {
self.divider.drag(column, width);
}
MouseKind::Up(_) => self.divider.release(),
_ => {}
}
None
}
fn row(&self, position: usize, width: usize) -> Vec<Segment> {
let (index, positions) = &self.filter.matches()[position];
let item = &self.items[*index];
let theme = &self.theme;
let focused = position == self.list.cursor();
let mut line = if focused {
vec![text(format!("{} ", theme.pointer), &theme.pointer_style)]
} else {
vec![plain(" ")]
};
if self.multi {
line.push(if self.list.is_selected(*index) {
text(format!("{} ", theme.checked), &theme.checked_style)
} else {
text(format!("{} ", theme.unchecked), &theme.hint)
});
}
let prefix = match &self.guides {
Some(guides) => Some(guides.prefix(*index)),
None => self.prefixes.get(*index).cloned(),
};
if let Some(prefix) = prefix.filter(|p| !p.is_empty()) {
line.push(if self.prefix_plain {
plain(prefix)
} else {
text(prefix, &theme.hint)
});
}
if let Some(Some(icon)) = self.icons.get(*index) {
line.extend(icon.iter().cloned());
line.push(plain(" "));
}
let base = if self.filter.is_context(position) {
Some(&theme.hint)
} else {
focused.then_some(&theme.focused)
};
let label = highlight(&item.label, positions, base, &theme.matched);
match self.pan.get() {
Some(pan) => line.extend(pan.window(&label)),
None => line.extend(label),
}
if let Some(description) = &item.description {
line.push(text(format!(" {description}"), &theme.hint));
}
fit(line, width)
}
pub fn list_lines(&self, width: usize) -> Vec<Vec<Segment>> {
let rows = self.rows();
let mut lines: Vec<Vec<Segment>> = (self.list.offset()..self.filter.len())
.take(rows)
.map(|position| self.row(position, width))
.collect();
if self.filter.is_empty() {
lines.push(vec![text(" no matches", &self.theme.hint)]);
}
lines.resize_with(rows, Vec::new);
lines
}
pub fn footer(&self, width: usize) -> Vec<Segment> {
let mut hint = format!("{}/{}", self.filter.len(), self.items.len());
if self.multi {
let marked = self.list.selected_count();
hint.push_str(&format!(" · {marked} marked · tab mark"));
}
hint.push_str(" · ↑↓ move · enter pick");
if let Some(extra) = &self.hints {
hint.push_str(&format!(" · {extra}"));
}
if self
.focused()
.is_some_and(|index| !self.actions_for(index).is_empty())
{
hint.push_str(&format!(" · {} actions", self.menu_key));
}
hint.push_str(" · esc cancel");
fit(vec![text(format!(" {hint}"), &self.theme.hint)], width)
}
fn render_view(&self, context: &Context<'_>) -> View {
self.space.set(context.height);
let width = context.width;
let theme = &self.theme;
let mut header = question(theme, &self.prompt);
if let Some(answer) = &self.answer {
header.push(match answer {
Some(answer) => text(answer.clone(), &theme.answer),
None => text("cancelled", &theme.hint),
});
return View::new(vec![fit(header, width)]);
}
let query = self.filter.query();
let column = crate::components::width(&header) + rich::cells::cell_len(query);
header.push(plain(query.to_string()));
let mut lines = vec![fit(header, width)];
if let Some(heading) = &self.heading {
let indent = if self.multi { " " } else { " " };
match self.pan.get() {
Some(pan) => lines.push(pan.heading(indent, heading, self.list_width(width))),
None => {
let mut line = vec![plain(indent)];
line.extend(heading.iter().cloned());
lines.push(fit(line, width));
}
}
}
let footer = |this: &Self| match &this.status {
Some(status) => vec![fit(status.clone(), width)],
None => vec![this.footer(width)],
};
let preview = self
.focused()
.and_then(|index| self.items[index].preview.as_ref());
match (preview, self.layout(width)) {
(Some(preview), (PreviewLayout::Right, left)) => {
let right = width.saturating_sub(left + 3).max(1);
let list = self.list_lines(left);
let shown = context.lines_at(&*preview.renderable(), right);
for (row, line) in list.into_iter().enumerate() {
let mut line = pad(line, left);
line.push(text(" │ ", &theme.border));
if let Some(preview) = shown.get(row) {
line.extend(fit(preview.clone(), right));
}
lines.push(line);
}
lines.extend(footer(self));
}
(Some(preview), (PreviewLayout::Below, _)) => {
lines.extend(self.list_lines(width));
lines.extend(footer(self));
lines.push(vec![text("─".repeat(width), &theme.border)]);
let shown = context.lines_at(&*preview.renderable(), width);
lines.extend(
shown
.into_iter()
.take(self.preview_height)
.map(|line| fit(line, width)),
);
}
_ => {
lines.extend(self.list_lines(width));
lines.extend(footer(self));
}
}
if let (Some(menu), Some((x, y, outer, _))) = (&self.menu, self.menu_box(width)) {
let inner = outer.saturating_sub(2);
let title = self
.focused()
.map(|index| format!("Actions · {}", self.items[index].label));
let boxed = crate::kit::frame(
menu.render(theme, inner),
inner,
title.as_deref(),
&theme.border,
);
let dim = rich::Style::parse("dim").expect("a built-in style");
crate::kit::place(&mut lines, x, y, &boxed, Some(&dim));
return View::new(lines);
}
View::new(lines).with_cursor(0, column.min(width.saturating_sub(1)))
}
pub fn visible_keymap(&self) -> Keymap {
if self.menu.is_some() {
return Keymap::new("menu")
.bind("up", keys("up"), "move up")
.bind("down", keys("down tab"), "move down")
.bind("run", keys("enter"), "run")
.bind("close", keys("escape"), "close");
}
let mut keymap = Keymap::new("select");
for binding in self.keymap.bindings() {
let action = binding.action.as_str();
let shown = (self.multi || !MARKING.contains(&action))
&& (action != "reload" || self.source.is_some());
if shown {
keymap.add(binding);
}
}
keymap
}
fn ask(&mut self, io: &mut dyn LineIo) -> Result<Option<Vec<usize>>, NotInteractive> {
io.write(&format!("{}\n", self.prompt));
for (index, item) in self.items.iter().enumerate() {
let mut row = format!(" {}) {}", index + 1, item.label);
if let Some(description) = &item.description {
row.push_str(&format!(" — {description}"));
}
io.write(&format!("{row}\n"));
}
io.write(if self.multi {
"Numbers or names, separated by commas: "
} else {
"Number or name: "
});
let default: Vec<usize> = if self.multi {
self.list.selected()
} else {
self.default.into_iter().collect()
};
let Some(line) = io.read_line() else {
if default.is_empty() {
return Err(NotInteractive::Ended);
}
return Ok(Some(default));
};
if line.trim().is_empty() && !default.is_empty() {
return Ok(Some(default));
}
let answers: Vec<&str> = if self.multi {
line.split(',')
.map(str::trim)
.filter(|a| !a.is_empty())
.collect()
} else {
vec![line.trim()]
};
let texts: Vec<String> = self.items.iter().map(Item::search_text).collect();
let mut chosen = Vec::new();
for answer in answers {
let index = match answer.parse::<usize>() {
Ok(number) if (1..=self.items.len()).contains(&number) => number - 1,
_ => rank(answer, texts.iter().map(String::as_str))
.first()
.map(|(index, _)| *index)
.filter(|_| !answer.is_empty())
.ok_or_else(|| {
NotInteractive::Invalid(format!("no item matches {answer:?}"))
})?,
};
if !chosen.contains(&index) {
chosen.push(index);
}
}
if chosen.is_empty() {
return Err(NotInteractive::Invalid("nothing chosen".into()));
}
Ok(Some(chosen))
}
}
#[cfg(test)]
fn clamp_split(column: usize, width: usize) -> usize {
Divider::new(3).min(MIN_PANE).clamp(column, width)
}
impl<T: Clone> Component for Select<T> {
type Output = T;
fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<T> {
match self.event(event, context) {
Some(Flow::Done(indices)) => Flow::Done(self.items[indices[0]].value.clone()),
Some(Flow::Cancel) => Flow::Cancel,
Some(Flow::Ignored) => Flow::Ignored,
_ => Flow::Continue,
}
}
fn render(&self, context: &Context<'_>) -> View {
self.render_view(context)
}
fn tick(&self) -> Option<Duration> {
self.tick_interval()
}
fn mouse(&self) -> bool {
self.mouse
}
fn keymap(&self) -> Keymap {
self.visible_keymap()
}
fn default_value(&self) -> Option<T> {
self.default.map(|index| self.items[index].value.clone())
}
fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<T>, NotInteractive> {
Ok(self
.ask(io)?
.map(|indices| self.items[indices[0]].value.clone()))
}
}
#[must_use = "a component does nothing until it is run or mounted"]
pub struct MultiSelect<T>(Select<T>);
impl<T> MultiSelect<T> {
pub fn new<I, V>(prompt: impl Into<String>, items: I) -> MultiSelect<T>
where
I: IntoIterator<Item = V>,
V: Into<Item<T>>,
{
let mut select = Select::new(prompt, items);
select.multi = true;
MultiSelect(select)
}
pub fn height(self, rows: usize) -> Self {
MultiSelect(self.0.height(rows))
}
pub fn preview(self, layout: PreviewLayout) -> Self {
MultiSelect(self.0.preview(layout))
}
pub fn theme(self, theme: Theme) -> Self {
MultiSelect(self.0.theme(theme))
}
pub fn query(self, query: impl Into<String>) -> Self {
MultiSelect(self.0.query(query))
}
pub fn repaint_every(self, interval: Duration) -> Self {
MultiSelect(self.0.repaint_every(interval))
}
pub fn actions(self, actions: Actions) -> Self {
MultiSelect(self.0.actions(actions))
}
pub fn with_mouse(self, on: bool) -> Self {
MultiSelect(self.0.with_mouse(on))
}
pub fn reload_on(
self,
key: Key,
source: impl FnMut() -> Vec<Item<T>> + Send + 'static,
) -> Self {
MultiSelect(self.0.reload_on(key, source))
}
pub fn reload_from(self, feed: Receiver<Vec<Item<T>>>) -> Self {
MultiSelect(self.0.reload_from(feed))
}
pub fn reload(&mut self, items: Vec<Item<T>>) {
self.0.reload(items);
}
pub fn marked(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
for index in indices {
if index < self.0.items.len() {
self.0.list.set_selected(index, true);
}
}
self
}
pub fn action(&self) -> Option<&str> {
self.0.action()
}
pub fn items(&self) -> &[Item<T>] {
self.0.items()
}
}
impl<T: Clone> Component for MultiSelect<T> {
type Output = Vec<T>;
fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<Vec<T>> {
match self.0.event(event, context) {
Some(Flow::Done(indices)) => Flow::Done(
indices
.into_iter()
.map(|index| self.0.items[index].value.clone())
.collect(),
),
Some(Flow::Cancel) => Flow::Cancel,
Some(Flow::Ignored) => Flow::Ignored,
_ => Flow::Continue,
}
}
fn render(&self, context: &Context<'_>) -> View {
self.0.render_view(context)
}
fn keymap(&self) -> Keymap {
self.0.visible_keymap()
}
fn tick(&self) -> Option<Duration> {
self.0.tick_interval()
}
fn mouse(&self) -> bool {
self.0.mouse
}
fn default_value(&self) -> Option<Vec<T>> {
Some(
self.0
.list
.selected()
.into_iter()
.map(|index| self.0.items[index].value.clone())
.collect(),
)
}
fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<Vec<T>>, NotInteractive> {
Ok(self.0.ask(io)?.map(|indices| {
indices
.into_iter()
.map(|index| self.0.items[index].value.clone())
.collect()
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_dragged_split_keeps_both_panes_usable() {
assert_eq!(clamp_split(5, 80), MIN_PANE);
assert_eq!(clamp_split(40, 80), 40);
assert_eq!(clamp_split(79, 80), 80 - MIN_PANE - 3);
assert!(clamp_split(3, 20) <= 20);
}
}