use std::sync::Arc;
use std::time::Duration;
use rich::cells::cell_len;
use rich::{Segment, Style};
use crate::clipboard::{self, CopyFormat};
use crate::component::{Component, Context, Flow, View};
use crate::components::{fit, plain, shown, text, PreviewLayout, Select, Theme};
use crate::event::{Event, Key, KeyCode};
use crate::item::{Actions, Item, TargetKind};
use crate::keymap::{keys, Keymap};
use crate::policy::{LineIo, NotInteractive};
macro_rules! select_builders {
() => {
pub fn height(mut self, rows: usize) -> Self {
self.select = self.select.height(rows);
self
}
pub fn preview(mut self, layout: PreviewLayout) -> Self {
self.select = self.select.preview(layout);
self
}
pub fn theme(mut self, theme: Theme) -> Self {
self.select = self.select.theme(theme);
self
}
pub fn query(mut self, query: impl Into<String>) -> Self {
self.select = self.select.query(query);
self
}
pub fn default(mut self, index: usize) -> Self {
self.select = self.select.default(index);
self
}
pub fn actions(mut self, actions: Actions) -> Self {
self.select = self.select.actions(actions);
self
}
pub fn menu_key(mut self, key: Key) -> Self {
self.select = self.select.menu_key(key);
self
}
pub fn with_mouse(mut self, on: bool) -> Self {
self.select = self.select.with_mouse(on);
self
}
pub fn action(&self) -> Option<&str> {
self.select.action()
}
pub fn items(&self) -> &[Item<T>] {
self.select.items()
}
pub fn select(&self) -> &Select<T> {
&self.select
}
pub fn set_icons(&mut self, icons: Vec<Option<rich::Text>>) {
self.select.set_icons(icons);
}
};
}
pub fn table_keymap() -> Keymap {
Keymap::new("table")
.bind("copy-row", keys("ctrl+y"), "copy the row")
.bind("copy-cell", keys("alt+y"), "copy the focused cell")
.bind("next-column", keys("ctrl+right"), "focus the next column")
.bind(
"previous-column",
keys("ctrl+left"),
"focus the previous column",
)
.bind("copy-format", keys("alt+f"), "copy as text, CSV or JSON")
}
pub struct TableSelect<T> {
select: Select<T>,
headers: Vec<String>,
raw_headers: Vec<String>,
cells: Vec<Vec<String>>,
widths: Vec<usize>,
column: Option<usize>,
format: CopyFormat,
keymap: Keymap,
}
impl<T> TableSelect<T> {
pub fn new<H, S, R, V>(prompt: impl Into<String>, headers: H, rows: R) -> TableSelect<T>
where
H: IntoIterator<Item = S>,
S: Into<String>,
R: IntoIterator<Item = (V, Vec<String>)>,
V: Into<Item<T>>,
{
let raw_headers: Vec<String> = headers.into_iter().map(Into::into).collect();
let headers: Vec<String> = raw_headers.iter().map(|h| shown(h)).collect();
let rows: Vec<(Item<T>, Vec<String>)> = rows
.into_iter()
.map(|(item, cells)| (item.into(), cells))
.collect();
let columns = rows
.iter()
.map(|(_, cells)| cells.len())
.chain([headers.len()])
.max()
.unwrap_or(0);
let mut widths = vec![0; columns];
for (column, cell) in headers.iter().enumerate() {
widths[column] = widths[column].max(cell_len(cell));
}
for (_, cells) in &rows {
for (column, cell) in cells.iter().enumerate() {
widths[column] = widths[column].max(cell_len(&shown(cell)));
}
}
let line = |cells: &[String]| -> String {
let mut out = String::new();
for (column, width) in widths.iter().enumerate() {
let cell = cells.get(column).map_or("", String::as_str);
out.push_str(cell);
if column + 1 < widths.len() {
out.push_str(&" ".repeat(width - cell_len(cell) + 2));
}
}
out.trim_end().to_string()
};
let mut values = Vec::with_capacity(rows.len());
let mut table = Vec::with_capacity(rows.len());
let items: Vec<Item<T>> = rows
.into_iter()
.map(|(mut item, cells)| {
let drawn: Vec<String> = cells.iter().map(|cell| shown(cell)).collect();
item.label = line(&drawn);
values.push(cells.join("\t"));
table.push(cells);
item
})
.collect();
let mut select = Select::new(prompt, items);
select.set_kind(TargetKind::Row);
select.set_values(values);
let mut view = TableSelect {
select,
headers,
raw_headers,
cells: table,
widths,
column: None,
format: CopyFormat::Text,
keymap: table_keymap(),
};
view.heading();
view
}
select_builders!();
pub fn copy_format(mut self, format: CopyFormat) -> Self {
self.format = format;
self
}
pub fn column(&self) -> Option<usize> {
self.column
}
pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
self.keymap.rebind(action, keys);
self
}
fn heading(&mut self) {
let style = Theme::default().prompt;
let Some(focused_column) = self.column else {
let mut heading = String::new();
for (column, width) in self.widths.iter().enumerate() {
let cell = self.headers.get(column).map_or("", String::as_str);
heading.push_str(cell);
if column + 1 < self.widths.len() {
heading.push_str(&" ".repeat(width - cell_len(cell) + 2));
}
}
self.select.heading = Some(vec![text(heading.trim_end(), &style)]);
return;
};
let focused = style.combine(&Style::parse("underline").expect("a style"));
let mut line: Vec<Segment> = Vec::new();
let last = self.widths.len().saturating_sub(1);
for (column, width) in self.widths.iter().enumerate() {
let cell = self.headers.get(column).map_or("", String::as_str);
let style = if column == focused_column {
&focused
} else {
&style
};
if !cell.is_empty() {
line.push(text(cell, style));
}
if column < last {
line.push(plain(" ".repeat(width - cell_len(cell) + 2)));
}
}
while line
.last()
.is_some_and(|segment| segment.style.is_none() && segment.text.trim().is_empty())
{
line.pop();
}
self.select.heading = Some(line);
}
fn copy(&mut self, cell: bool) {
let Some(index) = self.select.focused() else {
return;
};
let row = &self.cells[index];
let (what, copied) = if cell {
let column = self.column.unwrap_or(0);
let value = row.get(column).map_or("", String::as_str);
("cell", self.format.cell(value))
} else {
("row", self.format.row(&self.raw_headers, row))
};
let result = clipboard::copy(copied);
let what = format!("{what} as {}", self.format.name());
self.select
.set_status(Some(clipboard::report(&what, &result)));
}
fn table_key(&mut self, key: Key) -> bool {
match self.keymap.action(key) {
Some("copy-row") => self.copy(false),
Some("copy-cell") => self.copy(true),
Some(step @ ("next-column" | "previous-column")) => {
let columns = self.widths.len().max(1);
let at = self.column.unwrap_or(0);
self.column = Some(if step == "next-column" {
(at + 1).min(columns - 1)
} else {
at.saturating_sub(1)
});
self.heading();
}
Some("copy-format") => {
self.format = self.format.next();
self.select
.set_status(Some(format!("copy as {}", self.format.name())));
}
_ => return false,
}
true
}
}
impl<T: Clone> Component for TableSelect<T> {
type Output = T;
fn keymap(&self) -> Keymap {
let mut keymap = if self.select.menu_open() {
Keymap::default()
} else {
self.keymap.clone()
};
keymap.extend(self.select.visible_keymap());
keymap
}
fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<T> {
if let Some(key) = event.key() {
if !self.select.menu_open() && self.table_key(key) {
return Flow::Continue;
}
}
self.select.handle(event, context)
}
fn render(&self, context: &Context<'_>) -> View {
self.select.render(context)
}
fn tick(&self) -> Option<Duration> {
self.select.tick()
}
fn mouse(&self) -> bool {
Component::mouse(&self.select)
}
fn default_value(&self) -> Option<T> {
self.select.default_value()
}
fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<T>, NotInteractive> {
self.select.prompt(io)
}
}
pub fn tree_keymap() -> Keymap {
Keymap::new("tree")
.bind("expand", keys("right"), "expand")
.bind("collapse", keys("left"), "collapse, or go to the parent")
.bind("copy-path", keys("ctrl+y"), "copy the path")
}
pub struct TreeSelect<T> {
select: Select<T>,
parents: Arc<[Option<usize>]>,
last: Vec<bool>,
parent_of_any: Vec<bool>,
collapsed: Vec<bool>,
crumbs: Vec<String>,
breadcrumbs: bool,
width: usize,
filtered: bool,
keymap: Keymap,
}
impl<T> TreeSelect<T> {
pub fn new<I, V>(prompt: impl Into<String>, nodes: I) -> TreeSelect<T>
where
I: IntoIterator<Item = (usize, V)>,
V: Into<Item<T>>,
{
let mut items: Vec<Item<T>> = Vec::new();
let mut parents: Vec<Option<usize>> = Vec::new();
let mut open: Vec<usize> = Vec::new();
for (depth, node) in nodes {
let depth = depth.min(open.len());
open.truncate(depth);
let index = items.len();
parents.push(open.last().copied());
open.push(index);
let mut item: Item<T> = node.into();
item.label = shown(&item.label);
items.push(item);
}
let count = items.len();
let mut parent_of_any = vec![false; count];
for parent in parents.iter().flatten() {
parent_of_any[*parent] = true;
}
let last = last_children(&parents, None);
let parents: Arc<[Option<usize>]> = parents.into();
let mut select = Select::new(prompt, items);
select.set_kind(TargetKind::Node);
select.hints = Some("←→ fold".into());
select.set_tree(Some(parents.to_vec()));
let mut tree = TreeSelect {
select,
parents,
last,
parent_of_any,
collapsed: vec![false; count],
crumbs: Vec::new(),
breadcrumbs: false,
width: usize::MAX,
filtered: false,
keymap: tree_keymap(),
};
tree.update();
tree
}
pub fn breadcrumbs(mut self, on: bool) -> Self {
self.breadcrumbs = on;
self.crumb_line();
self
}
pub fn crumbs(mut self, crumbs: Vec<String>) -> Self {
let count = self.parents.len();
self.crumbs = crumbs
.into_iter()
.take(count)
.map(|crumb| shown(&crumb))
.collect();
self.crumb_line();
self
}
pub fn paths(mut self, paths: Vec<String>) -> Self {
self.select.set_values(paths);
self
}
pub fn paths_with(mut self, path: impl Fn(usize) -> String + Send + Sync + 'static) -> Self {
self.select.set_values(Vec::new());
self.select.value_of = Some(Arc::new(path));
self
}
pub fn fold_below(mut self, depth: usize) -> Self {
let mut depths = vec![0usize; self.parents.len()];
for index in 0..self.parents.len() {
depths[index] = self.parents[index].map_or(0, |parent| depths[parent] + 1);
self.collapsed[index] = depths[index] >= depth && self.parent_of_any[index];
}
self.update();
self
}
pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Self {
self.keymap.rebind(action, keys);
self
}
pub fn parents(&self) -> &[Option<usize>] {
&self.parents
}
pub fn focused(&self) -> Option<usize> {
self.select.focused()
}
pub fn path_to(&self, index: usize) -> Vec<&str> {
let mut path: Vec<&str> = crate::kit::ancestors(&self.parents, index)
.map(|ancestor| self.crumb(ancestor))
.collect();
path.reverse();
path.push(self.crumb(index));
path
}
fn crumb(&self, index: usize) -> &str {
match self.crumbs.get(index) {
Some(crumb) => crumb,
None => &self.select.items()[index].label,
}
}
pub fn set_status(&mut self, status: Option<String>) {
self.select.set_status(status);
}
pub fn set_hints(&mut self, hints: Option<String>) {
self.select.set_hints(hints);
}
fn crumb_line(&mut self) {
if !self.breadcrumbs {
return;
}
let path = self.select.focused().map(|index| self.path_to(index));
let line = breadcrumb_line(path.as_deref().unwrap_or(&[]), self.width);
self.select.heading = Some(line);
}
fn copy_path(&mut self) {
let Some(index) = self.select.focused() else {
return;
};
let path = self.select.target(index).value;
let result = clipboard::copy(path);
self.select
.set_status(Some(clipboard::report("path", &result)));
}
pub fn collapsed(mut self, collapsed: bool) -> Self {
for (index, parent) in self.parent_of_any.iter().enumerate() {
self.collapsed[index] = collapsed && *parent;
}
self.update();
self
}
select_builders!();
pub fn is_collapsed(&self, index: usize) -> bool {
self.collapsed.get(index).copied().unwrap_or(false)
}
pub fn set_collapsed(&mut self, index: usize, collapsed: bool) {
if self.parent_of_any.get(index).copied().unwrap_or(false) {
self.collapsed[index] = collapsed;
self.update();
}
}
fn update(&mut self) {
let mut hidden = vec![false; self.parents.len()];
for index in 0..self.parents.len() {
hidden[index] =
self.parents[index].is_some_and(|parent| self.collapsed[parent] || hidden[parent]);
}
self.select.guides = Some(self.guides(None));
self.select.hidden = hidden;
self.select.refilter();
self.filtered = false;
self.refresh_guides();
self.crumb_line();
}
fn guides(&self, shown: Option<&[bool]>) -> Guides {
let count = self.parents.len();
let (last, open) = match shown {
None => (self.last.clone(), None),
Some(shown) => {
let mut open = vec![false; count];
for (index, parent) in self.parents.iter().enumerate() {
if let Some(parent) = parent.filter(|_| shown[index]) {
open[parent] = true;
}
}
(last_children(&self.parents, Some(shown)), Some(open))
}
};
let markers = (0..count)
.map(|index| {
let collapsed = match &open {
Some(open) => !open[index],
None => self.collapsed[index],
};
match (self.parent_of_any[index], collapsed) {
(true, true) => Marker::Folded,
(true, false) => Marker::Open,
_ => Marker::Leaf,
}
})
.collect();
Guides {
parents: Arc::clone(&self.parents),
last,
markers,
}
}
fn refresh_guides(&mut self) {
let filtering = !self.select.query_text().is_empty();
if !filtering && !self.filtered {
return;
}
let guides = if filtering {
let mut shown = vec![false; self.parents.len()];
for (index, _) in self.select.filter().matches() {
shown[*index] = true;
}
self.guides(Some(&shown))
} else {
self.guides(None)
};
self.select.guides = Some(guides);
self.filtered = filtering;
}
fn fold(&mut self, key: Key) -> bool {
if !self.select.query_text().is_empty() || key.modifiers != Default::default() {
return false;
}
let Some(index) = self.select.focused() else {
return false;
};
match key.code {
KeyCode::Right if self.collapsed[index] => {
self.collapsed[index] = false;
self.update();
}
KeyCode::Left if self.parent_of_any[index] && !self.collapsed[index] => {
self.collapsed[index] = true;
self.update();
}
KeyCode::Left => {
if let Some(parent) = self.parents[index] {
self.select.focus_item(parent);
}
}
KeyCode::Right => {}
_ => return false,
}
true
}
}
fn last_children(parents: &[Option<usize>], shown: Option<&[bool]>) -> Vec<bool> {
let count = parents.len();
let mut last = vec![true; count];
let mut seen = vec![false; count + 1];
for index in (0..count).rev() {
if shown.is_some_and(|shown| !shown[index]) {
continue;
}
let slot = parents[index].unwrap_or(count);
last[index] = !seen[slot];
seen[slot] = true;
}
last
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Marker {
Leaf,
Folded,
Open,
}
pub(crate) struct Guides {
parents: Arc<[Option<usize>]>,
last: Vec<bool>,
markers: Vec<Marker>,
}
impl Guides {
pub(crate) fn prefix(&self, index: usize) -> String {
if index >= self.parents.len() {
return String::new();
}
let up: Vec<usize> = crate::kit::ancestors(&self.parents, index).collect();
let mut guides = String::new();
for &ancestor in up.iter().rev().skip(1) {
guides.push_str(if self.last[ancestor] {
" "
} else {
"│ "
});
}
if self.parents[index].is_some() {
guides.push_str(if self.last[index] {
"└── "
} else {
"├── "
});
}
guides.push_str(match self.markers[index] {
Marker::Folded => "▸ ",
Marker::Open => "▾ ",
Marker::Leaf => "",
});
guides
}
pub(crate) fn path<'a>(&self, index: usize, name: impl Fn(usize) -> &'a str) -> String {
let mut path: Vec<&str> = crate::kit::ancestors(&self.parents, index)
.map(&name)
.collect();
path.reverse();
path.push(name(index));
path.join("/")
}
}
impl<T: Clone> Component for TreeSelect<T> {
type Output = T;
fn keymap(&self) -> Keymap {
let mut keymap = if self.select.menu_open() {
Keymap::default()
} else {
self.keymap.clone()
};
keymap.extend(self.select.visible_keymap());
keymap
}
fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<T> {
self.width = context.width;
let flow = match event.key() {
Some(key) if !self.select.menu_open() && self.keymap.is(key, "copy-path") => {
self.select.set_status(None);
self.copy_path();
Flow::Continue
}
Some(key) if !self.select.menu_open() && self.fold(key) => {
self.select.set_status(None);
Flow::Continue
}
_ => self.select.handle(event, context),
};
self.refresh_guides();
self.crumb_line();
flow
}
fn render(&self, context: &Context<'_>) -> View {
self.select.render(context)
}
fn tick(&self) -> Option<Duration> {
self.select.tick()
}
fn mouse(&self) -> bool {
Component::mouse(&self.select)
}
fn default_value(&self) -> Option<T> {
self.select.default_value()
}
fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<T>, NotInteractive> {
self.select.prompt(io)
}
}
fn breadcrumb_line(path: &[&str], width: usize) -> Vec<Segment> {
let theme = Theme::default();
let separator = " › ";
let room = width.saturating_sub(2);
let mut first = 0;
let total = |from: usize| -> usize {
let names: usize = path[from..].iter().map(|name| cell_len(name)).sum();
let gaps = path.len().saturating_sub(from + 1) * cell_len(separator);
names
+ gaps
+ if from > 0 {
cell_len("…") + cell_len(separator)
} else {
0
}
};
while first + 1 < path.len() && total(first) > room {
first += 1;
}
let mut line = Vec::new();
if first > 0 {
line.push(text("…", &theme.hint));
line.push(text(separator, &theme.hint));
}
for (offset, name) in path[first..].iter().enumerate() {
if offset > 0 {
line.push(text(separator, &theme.hint));
}
let last = first + offset + 1 == path.len();
line.push(if last {
text(*name, &theme.focused)
} else {
text(*name, &theme.hint)
});
}
fit(line, room)
}