use derive_builder::Builder;
mod screen;
mod routine;
#[cfg(test)]
extern crate self as pick_a_boo;
pub use pick_a_boo_macros::item;
#[derive(Debug, Clone)]
pub struct Item {
pub long_label: String,
pub short_label: String,
pub key: char,
pub description: Option<String>,
}
impl Item {
pub fn new_full<S: AsRef<str>>(long_label: S, short_label: S, key: char, description: Option<S>) -> Self {
let long_label = long_label.as_ref().to_string();
let short_label = short_label.as_ref().to_string();
let description = description.map(|d| d.as_ref().to_string());
log::info!("create Item instance with new_full({long_label}, {short_label}, {key}, {description:?})");
Item {
long_label,
short_label,
key,
description,
}
}
pub fn new<S: AsRef<str>>(long_label: S, short_label: S, key: char) -> Self {
Item::new_full(long_label, short_label, key, None)
}
pub fn parse(input: impl Into<String>) -> Self {
let from_string = input.into();
let (head, description) = match from_string.find(":") {
Some(index) => {
let head = from_string[..index].trim_end().to_string();
let desc = from_string[index + 1..].trim().to_string();
(head, Some(desc))
}
None => (from_string.to_string(), None),
};
if head.ends_with(")") {
if let Some(start) = head.rfind("(") {
let long_label = head[..start].trim_end().to_string();
let short_label = head[start + 1..head.len() - 1].trim().to_string();
let key = short_label.chars().next().unwrap_or('\0').to_ascii_lowercase();
Item::new_full(long_label, short_label, key, description)
} else {
let long_label = head;
let key = long_label.chars().next().unwrap_or('\0').to_ascii_lowercase();
Item::new_full(long_label, key.to_string(), key, description)
}
} else {
let long_label = head;
let key = long_label.chars().next().unwrap_or('\0').to_ascii_lowercase();
Item::new_full(long_label, key.to_string(), key, description)
}
}
}
impl From<&str> for Item {
fn from(s: &str) -> Self {
Item::parse(s)
}
}
impl From<String> for Item {
fn from(s: String) -> Self {
Item::parse(s)
}
}
type ErrBox = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, Builder)]
#[builder(build_fn(validate = "validate_options", error = "ErrBox"))]
pub struct Options {
#[builder(setter(each(name="item", into)))]
items: Vec<Item>,
#[builder(default = 0)]
current: usize,
}
fn validate_options(options: &OptionsBuilder) -> Result<(), ErrBox> {
let items = options.items.as_ref().ok_or("items must be set")?;
let current = options.current.unwrap_or(0);
validate_option_items(items, current)
}
fn validate_option_items(items: &[Item], current: usize) -> Result<(), ErrBox> {
if items.is_empty() {
return Err("items cannot be empty".into());
}
if current >= items.len() {
return Err(format!("{current}: current index is out of bounds (len: {})", items.len()).into());
}
if let Some(key) = find_duplicate_keys(items) {
return Err(format!("{key}: duplicate key found").into());
}
Ok(())
}
fn find_duplicate_keys(items: &[Item]) -> Option<char> {
use std::collections::HashSet;
let mut keys = HashSet::new();
for item in items {
if !keys.insert(item.key) {
return Some(item.key);
}
}
None
}
impl Options {
pub fn from<S: AsRef<str>>(items: &[S]) -> Result<Self, ErrBox> {
let item_vec = items.iter().map(|s| Item::parse(s.as_ref())).collect::<Vec<_>>();
validate_option_items(&item_vec, 0)?;
Ok(Options {
items: item_vec,
current: 0,
})
}
fn next(&self, picker: &Picker) -> usize {
let new_index = self.current + 1;
if picker.allow_wrap {
new_index % self.items.len()
} else {
std::cmp::min(new_index, self.items.len() - 1)
}
}
fn previous(&self, picker: &Picker) -> usize {
if self.current == 0 {
if picker.allow_wrap {
self.items.len() - 1
} else {
0
}
} else {
self.current - 1
}
}
pub fn iter(&self) -> std::slice::Iter<'_, Item> {
self.items.iter()
}
pub fn current_item(&self) -> &Item {
&self.items[self.current]
}
pub fn display<'b>(&self, picker: &'b Picker) -> Display<'_, 'b> {
Display(self, picker)
}
fn current_name(&self) -> String {
self.items[self.current].long_label.clone()
}
fn update_current(self, index: usize) -> Self {
Self {
current: index,
..self
}
}
}
pub struct Display<'a, 'b>(&'a Options, &'b Picker);
impl std::fmt::Display for Display<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let picker = self.1;
let display = self.0.iter().enumerate()
.map(|(size, item)| {
if size == self.0.current {
format!(" {} ", item.long_label)
} else {
item.key.to_string()
}
}).collect::<Vec<_>>().join(&picker.delimiter);
write!(f, "{display}")
}
}
#[derive(Debug, Clone)]
pub enum DescriptionShowMode {
Never,
CurrentOnly,
All,
}
#[derive(Debug, Clone)]
pub enum DescriptionNameWidth {
Never,
Fixed(usize),
Auto,
}
#[derive(Debug, Builder)]
#[builder(build_fn(error = "ErrBox"))]
pub struct Picker {
#[builder(default = "/".to_string(), setter(into))]
pub delimiter: String,
#[builder(default = false)]
pub alternate_screen: bool,
#[builder(default = false)]
pub allow_wrap: bool,
#[builder(default = None, setter(strip_option, into, custom))]
pub paren: Option<(String, String)>,
#[builder(default = DescriptionShowMode::Never)]
pub description_show_mode: DescriptionShowMode,
#[builder(default = DescriptionNameWidth::Auto, setter(into))]
pub description_name_width: DescriptionNameWidth,
}
impl PickerBuilder {
pub fn paren<T: AsRef<str>>(&mut self, paren: T) -> &mut Self {
let paren = paren.as_ref().to_string();
log::info!("Setting paren: {paren}");
if paren.is_empty() {
self.paren = Some(None);
self
} else if paren.len() % 2 != 0 {
self.paren = Some(Some((paren, "".to_string())));
self
} else {
let len = paren.len() / 2;
let l = paren.chars().take(len).collect::<String>();
let r = paren.chars().skip(len).collect::<String>();
self.paren = Some(Some((l, r)));
self
}
}
}
impl Default for Picker {
fn default() -> Self {
log::info!("Building default Picker");
PickerBuilder::default()
.build().expect("Failed to build Picker")
}
}
impl Picker {
pub fn choose(&mut self, prompt: &str, options: Options) -> std::io::Result<Option<String>> {
log::info!("Picker choosing with prompt: {prompt}");
routine::choose(self, prompt, options)
}
pub fn yes_or_no(&mut self, prompt: &str, default_yes: bool) -> std::io::Result<Option<bool>> {
log::info!("Picker yes_or_no with prompt: {prompt}");
let yes_item = Item::new_full("Yes", "y", 'y', None);
let no_item = Item::new_full("No", "n", 'n', None);
let options = OptionsBuilder::default()
.item(yes_item)
.item(no_item)
.current(if default_yes { 0 } else { 1 })
.build().map_err(std::io::Error::other)?;
let answer = self.choose(prompt, options);
match answer {
Ok(Some(choice)) if choice == "Yes" => Ok(Some(true)),
Ok(Some(choice)) if choice == "No" => Ok(Some(false)),
Ok(Some(_)) => Ok(None),
Ok(None) => Ok(None),
Err(e) => Err(e),
}
}
}
pub fn yes_or_no(prompt: &str, default_yes: bool) -> std::io::Result<Option<bool>> {
Picker::default()
.yes_or_no(prompt, default_yes)
}
pub fn choose(prompt: &str, options: Options) -> std::io::Result<Option<String>> {
Picker::default()
.choose(prompt, options)
}
#[cfg(test)]
mod tests {
use crate::item;
#[test]
fn test_optionsbuilder_duplicate_keys() {
let result = crate::OptionsBuilder::default()
.item(item!("Option 1", "o", "description 1"))
.item(item!("Option 2", "o", "description 2")) .build();
assert!(result.is_err());
}
#[test]
fn test_optionsbuilder_out_of_bounds_current() {
let result = crate::OptionsBuilder::default()
.item(item!("Option 1", "1"))
.item(item!("Option 2", "2"))
.current(10) .build();
assert!(result.is_err());
}
#[test]
fn test_optionsbuilder_empty_items() {
let result = crate::OptionsBuilder::default()
.build();
assert!(result.is_err());
}
#[test]
fn test_optionsbuilder_no_items() {
let result = crate::OptionsBuilder::default()
.build();
assert!(result.is_err());
}
#[test]
fn test_from_str() {
let it: crate::Item = "Sample".into();
assert_eq!(it.long_label, "Sample");
assert_eq!(it.key, 's');
assert!(it.description.is_none());
}
#[test]
fn test_from_string() {
let it: crate::Item = String::from("Example: This is example").into();
assert_eq!(it.long_label, "Example");
assert_eq!(it.key, 'e');
assert_eq!(it.description.as_deref(), Some("This is example"));
}
#[test]
fn test_macro_item_1() {
let it = item!("Alpha");
assert_eq!(it.long_label, "Alpha");
assert_eq!(it.short_label, "a");
assert_eq!(it.key, 'a');
assert!(it.description.is_none());
}
#[test]
fn test_macro_item_2() {
let it = item!("Beta", "2");
assert_eq!(it.long_label, "Beta");
assert_eq!(it.short_label, "2");
assert_eq!(it.key, '2');
assert!(it.description.is_none())
}
#[test]
fn test_macro_item_3() {
let it = item!("Gamma", "3", "The third letter");
assert_eq!(it.long_label, "Gamma");
assert_eq!(it.short_label, "3");
assert_eq!(it.key, '3');
assert_eq!(it.description.as_deref(), Some("The third letter"));
}
#[test]
fn test_macro_item_4() {
let it = item!("Delta", "4", 'F', "The fourth letter");
assert_eq!(it.long_label, "Delta");
assert_eq!(it.short_label, "4");
assert_eq!(it.key, 'F');
assert_eq!(it.description.as_deref(), Some("The fourth letter"));
}
#[test]
fn test_macro_item_5() {
let it = item!("Epsilon", key = 'G');
assert_eq!(it.long_label, "Epsilon");
assert_eq!(it.short_label, "G");
assert_eq!(it.key, 'G');
assert!(it.description.is_none());
}
#[test]
fn test_macro_item_6() {
let it = item!("Zeta", description = "The sixth letter");
assert_eq!(it.long_label, "Zeta");
assert_eq!(it.short_label, "z");
assert_eq!(it.key, 'z');
assert_eq!(it.description.as_deref(), Some("The sixth letter"));
}
#[test]
fn test_macro_item_7() {
let it = item!("Eta", short = "7");
assert_eq!(it.long_label, "Eta");
assert_eq!(it.short_label, "7");
assert_eq!(it.key, '7');
assert!(it.description.is_none())
}
#[test]
fn test_macro_item_8() {
let it = item!("Theta", short = "A", key = '8');
assert_eq!(it.long_label, "Theta");
assert_eq!(it.short_label, "A");
assert_eq!(it.key, '8');
assert!(it.description.is_none());
}
#[test]
fn test_macro_item_9() {
let it = item!("Iota", short = "A", description = "The ninth letter");
assert_eq!(it.long_label, "Iota");
assert_eq!(it.short_label, "A");
assert_eq!(it.key, 'A');
assert_eq!(it.description.as_deref(), Some("The ninth letter"));
}
#[test]
fn test_macro_item_10() {
let it = item!("Kappa", short = "10", description = "The tenth letter", key = 'u');
assert_eq!(it.long_label, "Kappa");
assert_eq!(it.short_label, "10");
assert_eq!(it.key, 'u');
assert_eq!(it.description.as_deref(), Some("The tenth letter"));
}
#[test]
fn test_macro_item_11() {
let it = item!("Lambda", key = 'u', short = "11");
assert_eq!(it.long_label, "Lambda");
assert_eq!(it.short_label, "11");
assert_eq!(it.key, 'u');
assert!(it.description.is_none());
}
#[test]
fn test_macro_item_12() {
let it = item!("Mu", key = 'i', short = "12", description = "The twelveth letter");
assert_eq!(it.long_label, "Mu");
assert_eq!(it.short_label, "12");
assert_eq!(it.key, 'i');
assert_eq!(it.description.as_deref(), Some("The twelveth letter"));
}
#[test]
fn test_macro_item_13() { let it = item!("Nu", key = 'i', description = "The thirteenth letter", short = "13");
assert_eq!(it.long_label, "Nu");
assert_eq!(it.short_label, "13");
assert_eq!(it.key, 'i');
assert_eq!(it.description.as_deref(), Some("The thirteenth letter"));
}
#[test]
fn test_macro_item_14() {
let it = item!("Xi", key = 'o', description = "The fourteenth letter");
assert_eq!(it.long_label, "Xi");
assert_eq!(it.short_label, "o");
assert_eq!(it.key, 'o');
assert_eq!(it.description.as_deref(), Some("The fourteenth letter"));
}
#[test]
fn test_macro_item_15() {
let it = item!("Omicron", short = "15", key = 'u', description = "The fifteenth letter");
assert_eq!(it.long_label, "Omicron");
assert_eq!(it.short_label, "15");
assert_eq!(it.key, 'u');
assert_eq!(it.description.as_deref(), Some("The fifteenth letter"));
}
#[test]
fn test_macro_item_16() {
let it = item!("Pi", description = "The sixteenth letter", short = "16");
assert_eq!(it.long_label, "Pi");
assert_eq!(it.short_label, "16");
assert_eq!(it.key, '1');
assert_eq!(it.description.as_deref(), Some("The sixteenth letter"));
}
#[test]
fn test_macro_item_17() {
let it = item!("Rho", description = "The seventeenth letter", key = 'K');
assert_eq!(it.long_label, "Rho");
assert_eq!(it.short_label, "K");
assert_eq!(it.key, 'K');
assert_eq!(it.description.as_deref(), Some("The seventeenth letter"));
}
#[test]
fn test_macro_item_18() {
let it = item!("Sigma", description = "The eighteenth letter", key = 'K', short = "k");
assert_eq!(it.long_label, "Sigma");
assert_eq!(it.short_label, "k");
assert_eq!(it.key, 'K');
assert_eq!(it.description.as_deref(), Some("The eighteenth letter"));
}
#[test]
fn test_macro_item_19() {
let it = item!("Tau", description = "The nineteenth letter", short = "k", key = 'K');
assert_eq!(it.long_label, "Tau");
assert_eq!(it.short_label, "k");
assert_eq!(it.key, 'K');
assert_eq!(it.description.as_deref(), Some("The nineteenth letter"));
}
#[test]
fn test_macro_parse_with_short_and_description() {
let it = item!("Upsilon(20): The twentieth letter");
assert_eq!(it.long_label, "Upsilon");
assert_eq!(it.short_label, "20");
assert_eq!(it.key, '2');
assert_eq!(it.description.as_deref(), Some("The twentieth letter"));
}
#[test]
fn test_item_parse_without_description() {
let it = crate::Item::parse("Phi");
assert_eq!(it.long_label, "Phi");
assert_eq!(it.short_label, "p");
assert_eq!(it.key, 'p');
assert!(it.description.is_none());
}
#[test]
fn test_item_parse_with_description() {
let it = crate::Item::parse("Chi: This is just test");
assert_eq!(it.long_label, "Chi");
assert_eq!(it.short_label, "c");
assert_eq!(it.key, 'c');
assert_eq!(it.description.as_deref(), Some("This is just test"));
}
#[test]
fn test_item_parse_with_short_without_description() {
let it = crate::Item::parse("Psi(Isp)");
assert_eq!(it.long_label, "Psi");
assert_eq!(it.short_label, "Isp");
assert_eq!(it.key, 'i');
assert!(it.description.is_none());
}
#[test]
fn test_macro_item_with_empty_name() {
let it = item!("");
assert_eq!(it.long_label, "");
assert_eq!(it.key, '\0');
assert!(it.description.is_none())
}
}