use std::collections::BTreeSet;
use ratatui::layout::Constraint;
use crate::ui::BrowserState;
const TICK: &str = "✓";
pub struct Picker {
order: Vec<usize>,
names: Vec<String>,
shown: BTreeSet<usize>,
pinned: BTreeSet<usize>,
pub state: BrowserState,
}
impl Picker {
pub fn new(names: Vec<String>, view_order: Vec<usize>, pinned: BTreeSet<usize>) -> Self {
let shown: BTreeSet<usize> = view_order.iter().copied().collect();
let order: Vec<usize> = view_order
.into_iter()
.chain((0..names.len()).filter(|col| !shown.contains(col)))
.collect();
Self {
order,
names,
shown,
pinned,
state: BrowserState::default(),
}
}
pub fn len(&self) -> usize {
self.order.len()
}
pub fn is_empty(&self) -> bool {
self.order.is_empty()
}
pub fn cursor_column(&self) -> Option<usize> {
self.order.get(self.state.selected).copied()
}
pub fn toggle_shown(&mut self) -> Result<(), String> {
let Some(column) = self.cursor_column() else {
return Ok(());
};
if self.shown.contains(&column) {
if self.shown.len() == 1 {
return Err("that would hide every column".to_string());
}
self.shown.remove(&column);
} else {
self.shown.insert(column);
}
Ok(())
}
pub fn show_all(&mut self) {
self.shown = self.order.iter().copied().collect();
}
pub fn show_only_cursor(&mut self) {
let Some(column) = self.cursor_column() else {
return;
};
self.shown.clear();
self.shown.insert(column);
}
pub fn toggle_pinned(&mut self) {
let Some(column) = self.cursor_column() else {
return;
};
if !self.pinned.remove(&column) {
self.pinned.insert(column);
}
}
pub fn selection(&self) -> Vec<usize> {
self.order
.iter()
.copied()
.filter(|column| self.shown.contains(column))
.collect()
}
pub fn pins(&self) -> BTreeSet<usize> {
self.pinned.clone()
}
pub fn title(&self) -> String {
format!(" columns — {} of {} shown ", self.shown.len(), self.len())
}
pub fn headers() -> &'static [&'static str] {
&["column", "shown", "pinned"]
}
pub fn widths() -> &'static [Constraint] {
&[
Constraint::Length(32),
Constraint::Length(7),
Constraint::Length(8),
]
}
pub fn rows(&self) -> Vec<Vec<String>> {
self.order
.iter()
.map(|&column| {
let tick = |on: bool| if on { TICK.to_string() } else { String::new() };
vec![
self.names
.get(column)
.cloned()
.unwrap_or_else(|| column.to_string()),
tick(self.shown.contains(&column)),
tick(self.pinned.contains(&column)),
]
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn names() -> Vec<String> {
["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect()
}
#[test]
fn the_list_opens_in_view_order_with_the_hidden_ones_after() {
let picker = Picker::new(names(), vec![2, 0], BTreeSet::new());
let listed: Vec<String> = picker
.rows()
.into_iter()
.map(|row| row[0].clone())
.collect();
assert_eq!(listed, ["c", "a", "b", "d"]);
assert_eq!(picker.selection(), [2, 0], "and applying changes nothing");
}
#[test]
fn space_toggles_a_column_in_and_out_of_the_selection() {
let mut picker = Picker::new(names(), vec![0, 1, 2, 3], BTreeSet::new());
picker.state.go_to(1, 4); picker.toggle_shown().unwrap();
assert_eq!(picker.selection(), [0, 2, 3]);
picker.toggle_shown().unwrap();
assert_eq!(picker.selection(), [0, 1, 2, 3], "and back again");
}
#[test]
fn a_column_comes_back_where_it_is_listed() {
let mut picker = Picker::new(names(), vec![0, 2], BTreeSet::new());
picker.state.go_to(2, 4); picker.toggle_shown().unwrap();
assert_eq!(picker.selection(), [0, 2, 1], "in its listed position");
}
#[test]
fn a_and_shift_a_work_over_the_whole_list() {
let mut picker = Picker::new(names(), vec![0, 1, 2, 3], BTreeSet::new());
picker.state.go_to(2, 4);
picker.show_only_cursor();
assert_eq!(picker.selection(), [2], "everything but the cursor column");
picker.state.go_to(0, 4);
picker.toggle_shown().unwrap();
assert_eq!(picker.selection(), [0, 2], "in listed order");
picker.show_all();
assert_eq!(picker.selection(), [0, 1, 2, 3]);
}
#[test]
fn shift_a_on_a_hidden_column_leaves_that_one_showing() {
let mut picker = Picker::new(names(), vec![0, 1], BTreeSet::new());
picker.state.go_to(3, 4); picker.show_only_cursor();
assert_eq!(picker.selection(), [3]);
}
#[test]
fn shift_a_leaves_a_view_that_is_still_legal() {
let mut picker = Picker::new(names(), vec![0, 1, 2, 3], BTreeSet::new());
picker.show_only_cursor();
assert_eq!(picker.selection().len(), 1);
assert!(picker.toggle_shown().is_err(), "and the last one still holds");
}
#[test]
fn unticking_the_last_column_is_refused() {
let mut picker = Picker::new(names(), vec![0], BTreeSet::new());
let refusal = picker.toggle_shown().unwrap_err();
assert!(refusal.contains("hide every column"), "{refusal}");
assert_eq!(picker.selection(), [0], "and it stays on show");
}
#[test]
fn a_hidden_column_can_still_be_pinned() {
let mut picker = Picker::new(names(), vec![0], BTreeSet::new());
picker.state.go_to(1, 4); picker.toggle_pinned();
assert_eq!(picker.pins().into_iter().collect::<Vec<_>>(), [1]);
let marks = &picker.rows()[1];
assert_eq!(marks[1], "", "not shown");
assert_eq!(marks[2], TICK, "but pinned");
}
#[test]
fn pins_come_in_from_the_app_and_toggle_off_again() {
let mut picker = Picker::new(names(), vec![0, 1], [0].into_iter().collect());
assert_eq!(picker.rows()[0][2], TICK, "already pinned when it opens");
picker.toggle_pinned();
assert!(picker.pins().is_empty());
}
}