use crate::styles::{paint, ACCENT, DIM, OK, WHITE};
use super::core::{Frame, Key, Prompt, RenderCtx, Step};
use super::error::{PromptError, Result};
use super::theme;
#[derive(Clone, Copy)]
enum Row {
Header { group_idx: usize },
Item { group_idx: usize, item_idx: usize },
}
#[derive(Default)]
struct ItemData {
value: String,
hint: Option<String>,
checked: bool,
}
struct GroupData {
name: String,
items: Vec<ItemData>,
}
impl GroupData {
fn all_checked(&self) -> bool {
self.items.iter().all(|i| i.checked)
}
fn any_checked(&self) -> bool {
self.items.iter().any(|i| i.checked)
}
fn toggle_all(&mut self) {
let t = !self.all_checked();
for item in &mut self.items {
item.checked = t;
}
}
}
#[derive(Debug, Clone)]
pub struct GroupItem {
pub group: String,
pub value: String,
pub index: usize,
}
#[derive(Debug, Clone, Default)]
pub struct GroupSelected {
items: Vec<GroupItem>,
}
impl GroupSelected {
pub fn iter(&self) -> impl Iterator<Item = &GroupItem> {
self.items.iter()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn values(&self) -> Vec<&str> {
self.items.iter().map(|i| i.value.as_str()).collect()
}
pub fn group_values(&self, group: &str) -> Vec<&str> {
self.items
.iter()
.filter(|i| i.group == group)
.map(|i| i.value.as_str())
.collect()
}
}
pub struct GroupMultiSelectPrompt {
label: String,
groups: Vec<GroupData>,
required: bool,
current_grp: Option<usize>,
rows: Vec<Row>,
cur: usize,
}
pub fn groupmultiselect(label: impl Into<String>) -> GroupMultiSelectPrompt {
GroupMultiSelectPrompt {
label: label.into(),
groups: Vec::new(),
required: false,
current_grp: None,
rows: Vec::new(),
cur: 0,
}
}
impl GroupMultiSelectPrompt {
pub fn group(mut self, name: impl Into<String>) -> Self {
self.groups.push(GroupData {
name: name.into(),
items: Vec::new(),
});
self.current_grp = Some(self.groups.len() - 1);
self
}
pub fn item(mut self, value: impl Into<String>) -> Self {
let idx = self.current_grp.expect("call .group() before .item()");
self.groups[idx].items.push(ItemData {
value: value.into(),
hint: None,
checked: false,
});
self
}
pub fn hint(mut self, text: impl Into<String>) -> Self {
if let Some(idx) = self.current_grp {
if let Some(last) = self.groups[idx].items.last_mut() {
last.hint = Some(text.into());
}
}
self
}
pub fn checked(mut self) -> Self {
if let Some(idx) = self.current_grp {
if let Some(last) = self.groups[idx].items.last_mut() {
last.checked = true;
}
}
self
}
pub fn required(mut self, r: bool) -> Self {
self.required = r;
self
}
pub fn run(mut self) -> Result<GroupSelected> {
assert!(!self.groups.is_empty(), "groupmultiselect: no groups added");
assert!(
self.groups.iter().all(|g| !g.items.is_empty()),
"groupmultiselect: every group must have at least one item"
);
self.rows = build_rows(&self.groups);
super::core::run(self)
}
fn collect(&self) -> GroupSelected {
GroupSelected {
items: self
.groups
.iter()
.flat_map(|g| {
g.items
.iter()
.enumerate()
.filter(|(_, it)| it.checked)
.map(|(i, it)| GroupItem {
group: g.name.clone(),
value: it.value.clone(),
index: i,
})
.collect::<Vec<_>>()
})
.collect(),
}
}
}
impl Prompt for GroupMultiSelectPrompt {
type Output = GroupSelected;
fn handle(&mut self, key: Key) -> Step<GroupSelected> {
let last = self.rows.len() - 1;
match key {
Key::Up => {
if self.cur > 0 {
self.cur -= 1;
}
Step::Continue
}
Key::Down => {
if self.cur < last {
self.cur += 1;
}
Step::Continue
}
Key::Char(' ') => {
match self.rows[self.cur] {
Row::Header { group_idx } => self.groups[group_idx].toggle_all(),
Row::Item {
group_idx,
item_idx,
} => {
self.groups[group_idx].items[item_idx].checked ^= true;
}
}
Step::Continue
}
Key::Enter => {
let result = self.collect();
if self.required && result.is_empty() {
Step::Reject("Select at least one option".into())
} else {
Step::Submit(result)
}
}
Key::Escape | Key::Interrupt => Step::Cancel,
_ => Step::Continue,
}
}
fn render(&self, _ctx: RenderCtx) -> Frame {
let mut f: Frame = Vec::new();
f.push(theme::label(&self.label));
f.push(theme::hint(""));
let mut row_i = 0usize;
for g in &self.groups {
let header_active = row_i == self.cur;
let gbullet = if g.all_checked() {
paint(OK, "◼")
} else if g.any_checked() {
paint(ACCENT, "◼")
} else {
paint(DIM, "◻")
};
if header_active {
f.push(format!(
"{} {} {} {}",
paint(DIM, "│"),
paint(ACCENT, "❯"),
gbullet,
paint(WHITE, &g.name)
));
} else {
f.push(format!(
"{} {} {}",
paint(DIM, "│"),
gbullet,
paint(DIM, &g.name)
));
}
row_i += 1;
let count = g.items.len();
for ii in 0..count {
let it = &g.items[ii];
let is_last = ii == count - 1;
let is_cursor = row_i == self.cur;
let connector = if is_last {
paint(DIM, " └")
} else {
paint(DIM, " │")
};
let bullet = if it.checked {
paint(OK, "◼")
} else {
paint(DIM, "◻")
};
let hint_sfx = theme::hint_inline(it.hint.as_deref());
if is_cursor {
f.push(format!(
"{} {} {} {} {}{}",
paint(DIM, "│"),
connector,
paint(ACCENT, "❯"),
bullet,
paint(WHITE, &it.value),
hint_sfx
));
} else {
f.push(format!(
"{} {} {} {}{}",
paint(DIM, "│"),
connector,
bullet,
paint(DIM, &it.value),
paint(DIM, &hint_sfx)
));
}
row_i += 1;
}
}
f.push(theme::hint(
"Space to toggle · ↑/↓ to navigate · Enter to confirm",
));
f.push(theme::frame_bot(None));
f
}
fn render_answered(&self, value: &GroupSelected) -> Frame {
let summary = if value.is_empty() {
"none".to_string()
} else {
value
.items
.iter()
.map(|i| i.value.as_str())
.collect::<Vec<_>>()
.join(", ")
};
theme::answered(&self.label, &summary)
.split("\r\n")
.map(String::from)
.collect()
}
fn run_fallback(self) -> Result<GroupSelected> {
use std::io::Write;
let mut out = std::io::stderr();
writeln!(out, " {}", self.label).map_err(PromptError::Io)?;
let mut all: Vec<(String, String)> = Vec::new();
let mut n = 1usize;
for g in &self.groups {
writeln!(out, " [ {} ]", g.name).map_err(PromptError::Io)?;
for it in &g.items {
match &it.hint {
Some(h) => writeln!(out, " {}. {} ({})", n, it.value, h),
None => writeln!(out, " {}. {}", n, it.value),
}
.map_err(PromptError::Io)?;
all.push((g.name.clone(), it.value.clone()));
n += 1;
}
}
write!(out, " Enter numbers (space-separated): ").map_err(PromptError::Io)?;
out.flush().map_err(PromptError::Io)?;
let line = super::engine::fallback::read_line_raw()?;
let indices: Vec<usize> = line
.split_whitespace()
.filter_map(|t| t.parse::<usize>().ok())
.filter(|&i| i >= 1 && i < n)
.map(|i| i - 1)
.collect();
Ok(GroupSelected {
items: indices
.into_iter()
.map(|i| GroupItem {
group: all[i].0.clone(),
value: all[i].1.clone(),
index: i,
})
.collect(),
})
}
}
fn build_rows(groups: &[GroupData]) -> Vec<Row> {
let mut rows = Vec::new();
for (gi, g) in groups.iter().enumerate() {
rows.push(Row::Header { group_idx: gi });
for ii in 0..g.items.len() {
rows.push(Row::Item {
group_idx: gi,
item_idx: ii,
});
}
}
rows
}