Skip to main content

pick_a_boo/
lib.rs

1//! # pick-a-boo
2//! A very simple option picker for CLI tools!
3//!
4//! ## Installation
5//!
6//! Add `pick-a-boo` to your project's dependencies:
7//! 
8//! ```toml
9//! [dependencies]
10//! pick-a-boo = "0.1"
11//! ```
12//!
13//! ## Usage
14//! 
15//! Here is just a simple example of how to use `pick-a-boo` in your Rust project:
16//! 
17//! ```rust,no_run
18//! fn main() -> std::io::Result<()> {
19//!     let options = pick_a_boo::Options::from(&["Yes", "So so", "Maybe", "No"])
20//!         .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
21//!     match pick_a_boo::choose("🦀 Do you like Rust? 🦀", options) {
22//!         Ok(Some(choice)) if choice == "Yes"   => println!("You like Rust! 🤩"),
23//!         Ok(Some(choice)) if choice == "So so" => println!("You feel so so about Rust."),
24//!         Ok(Some(choice)) if choice == "Maybe" => println!("You are unsure about Rust."),
25//!         Ok(Some(choice)) if choice == "No"    => println!("You don't like Rust... 😭"),
26//!         Ok(Some(_)) => panic!("Unknown choice. never reach here!"),
27//!         Ok(None) => println!("Cancelled."),
28//!         Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
29//!     }
30//!     Ok(())
31//! }
32//! ```
33//! 
34//! When you run the program, you'll be presented with a prompt in your terminal:
35//! 
36//! ```text
37//! Do you like Rust? [Yes /s/m/n]
38//! Do you like Rust? [y/ So So /m/n]
39//! Do you like Rust? [y/s/ Maybe /n]
40//! Do you like Rust? [y/s/m/ No]
41//! ```
42//! 
43//! - Navigate between "Yes" and "No" using the left and right arrow keys.
44//! - Press Enter to select your choice.
45//! - Press the corresponding key (y/s/m/n) to select an option directly.
46//! - Press Ctrl+C or Escape to cancel (returns `None`).
47
48use derive_builder::Builder;
49
50mod screen;
51mod routine;
52
53#[cfg(test)]
54extern crate self as pick_a_boo;
55
56/// Macro to create an [Item] instance with flexible arguments.
57/// The first positional argument is the long name (label) of the item.
58/// Following arguments can be provided as either positional or named arguments:
59/// 
60/// ## Positional arguments:
61/// 
62/// The number of positional arguments can be from 1 to 4.
63/// 
64/// ```rust
65/// use pick_a_boo::item;
66/// item!("LongName");                                  // only long name
67/// item!("LongName", "ShortName");                     // long name and short name
68/// item!("LongName", "ShortName", "Description");      // long name, short name, and description
69/// item!("LongName", "ShortName", 'k', "Description"); // four positional arguments
70/// ```
71/// 
72/// ## Named arguments:
73/// 
74/// The named arguments can be provided in any order after the first positional argument (long name).
75/// 
76/// ```rust
77/// use pick_a_boo::item;
78/// item!("LongName");                              // only long name
79/// item!("LongName", key = 'k');                   // long name and key
80/// item!("LongName", short = "ShortName");         // long name and short name
81/// item!("LongName", description = "Description"); // long name and description
82/// ```
83///
84/// ## Usage examples:
85/// 
86/// Exhaustive examples of various argument combinations:
87///
88/// ```rust
89/// use pick_a_boo::item;
90/// let a = item!("Alpha");                                  // Item::parse("Alpha")
91/// let b = item!("Beta", "2");                              // Item::new_full("Beta", "2", "2", None)
92/// let c = item!("Gamma", "3", "The third letter");         // Item::new_full("Gamma", "3", '3', Some("The third letter"))
93/// let d = item!("Delta", "4", 'F', "The fourth letter");   // Item::new_full("Delta", "4", 'F', Some("The fourth letter"))
94/// let e = item!("Epsilon", key = 'G');                     // Item::new_full("Epsilon", "G", 'G', None)
95/// let f = item!("Zeta", description = "The sixth letter"); // Item::new_full("Zeta", "z", 'z', Some("The sixth letter"))
96/// let g = item!("Eta", short = "7");                       // Item::new_full("Eta", "7", '7', None)
97/// let h = item!("Theta", short = "A", key = '8');          // Item::new_full("Theta", "A", '8', None)
98/// let i = item!("Iota", short = "A", description = "The nineth letter");
99/// // Item::new_full("Iota", "A", 'A', Some("The nineth letter"))
100/// 
101/// let j = item!("Kappa", short = "10", description = "The tenth letter", key = 'u');
102/// // Item::new_full("Kappa", "10", 'u', Some("The tenth letter"))
103/// 
104/// let k = item!("Lambda", key = 'u', short = "11");        // Item::new_full("Lambda", "11", 'u', None)
105/// 
106/// let l = item!("Mu", key = 'i', short = "12", description = "The twelveth letter");
107/// // Item::new_full("Mu", "12", 'i', Some("The twelveth letter"))
108/// 
109/// let m = item!("Nu", key = 'i', description = "The thirteenth letter", short = "13");
110/// // Item::new_full("Nu", "13", 'i', Some("The thirteenth letter"))
111/// 
112/// let n = item!("Xi", key = 'o', description = "The fourteenth letter");
113/// // Item::new_full("Xi", "o", 'o', Some("The fourteenth letter"))
114/// 
115/// let o = item!("Omicron", short = "15", key = 'u', description = "the fifteenth letter");
116/// // Item::new_full("Omicron", "15", 'u', Some("The fifteenth letter"))
117/// 
118/// let p = item!("Pi", description = "The sixteenth letter", short = "16");
119/// // Item::new_full("Pi", "16", '1', Some("The sixteenth letter"))
120/// 
121/// let q = item!("Rho", description = "The seventeenth letter", key = 'K');
122/// // Item::new_full("Rho", "K", 'K', Some("The seventeenth letter"))
123/// 
124/// let r = item!("Sigma", description = "The eighteenth letter", key = 'K', short = "k");
125/// // Item::new_full("Sigma", "k", 'K', Some("The eighteenth letter"))
126/// 
127/// let s = item!("Tau", description = "The nineteenth letter", short = "k", key = 'K');
128/// // Item::new_full("Tau", "k", 'K', Some("The nineteenth letter"))
129/// 
130/// let i = item!("", description = "empty");        // empty name then key and short are '\0'
131/// ```
132pub use pick_a_boo_macros::item;
133
134/// Item struct represents a selectable option with a name, key, and optional description.
135#[derive(Debug, Clone)]
136pub struct Item {
137    pub long_label: String,
138    pub short_label: String,
139    pub key: char,
140    pub description: Option<String>,
141}
142
143impl Item {
144    /// Create a new Item instance.
145    pub fn new_full<S: AsRef<str>>(long_label: S, short_label: S, key: char, description: Option<S>) -> Self {
146        let long_label = long_label.as_ref().to_string();
147        let short_label = short_label.as_ref().to_string();
148        let description = description.map(|d| d.as_ref().to_string());
149        log::info!("create Item instance with new_full({long_label}, {short_label}, {key}, {description:?})");
150        Item {
151            long_label,
152            short_label,
153            key,
154            description,
155        }
156    }
157
158    pub fn new<S: AsRef<str>>(long_label: S, short_label: S, key: char) -> Self {
159        Item::new_full(long_label, short_label, key, None)
160    }
161
162    /// Parse an item from a string.
163    /// The key is derived from the first character of the name, converted to lowercase.
164    /// If an uppercase key is desired, use the [`Item::new`] method or the [`item!`] macro.
165    /// 
166    /// The given string should be formatted as "LongLabel[(ShortKey)][: Description]".
167    /// If the colon `:` is present, the part after it is treated as the description.
168    /// If not, the description is `None`.
169    /// Also, `ShortKey` is optional and if it is not provided, the `ShortKey` is derived from the first character of the `LongLabel`.
170    /// 
171    /// ### Example
172    /// 
173    /// ```rust
174    /// use pick_a_boo::Item;
175    /// let item1 = Item::parse("Example");                     //  Item::new_full("Example", "e", 'e', None)
176    /// let item2 = Item::parse("Test: This is just test");     //  Item::new_full("Test",    "t", 't', Some("This is just test"))
177    /// let item3 = Item::parse("Colon: Its:too:many:colons!"); //  Item::new_full("Colon",   "c", 'c', Some("Its:too:many:colons!"))
178    /// let item4 = Item::parse("Label(S): With short key");    //  Item::new_full("Label",   "S", 'S', Some("With short key"))
179    /// ```
180    pub fn parse(input: impl Into<String>) -> Self {
181        let from_string = input.into();
182        let (head, description) = match from_string.find(":") {
183            Some(index) => {
184                let head = from_string[..index].trim_end().to_string();
185                let desc = from_string[index + 1..].trim().to_string();
186                (head, Some(desc))
187            }
188            None => (from_string.to_string(), None),
189        };
190        if head.ends_with(")") {
191            if let Some(start) = head.rfind("(") {
192                let long_label = head[..start].trim_end().to_string();
193                let short_label = head[start + 1..head.len() - 1].trim().to_string();
194                let key = short_label.chars().next().unwrap_or('\0').to_ascii_lowercase();
195                Item::new_full(long_label, short_label, key, description)
196            } else {
197                let long_label = head;
198                let key = long_label.chars().next().unwrap_or('\0').to_ascii_lowercase();
199                Item::new_full(long_label, key.to_string(), key, description)
200            }
201        } else {
202            let long_label = head;
203            let key = long_label.chars().next().unwrap_or('\0').to_ascii_lowercase();
204            Item::new_full(long_label, key.to_string(), key, description)
205        }
206    }
207}
208
209impl From<&str> for Item {
210    fn from(s: &str) -> Self {
211        Item::parse(s)
212    }
213}
214
215impl From<String> for Item {
216    fn from(s: String) -> Self {
217        Item::parse(s)
218    }
219}
220
221type ErrBox = Box<dyn std::error::Error + Send + Sync>;
222
223/// Options struct holds a list of items and the current selection index.
224/// To create an instance, use the `OptionBuilder` or the [`Options::from`] helper method.
225/// 
226/// ### Example: Create an instance with the builder
227/// 
228/// Each item can be created with the [`item!`] macro or the [`Item::new`] method.
229/// The [`item!`] macro receives the item name as the first positional argument,
230/// and optional key and description as either positional or named arguments.
231/// 
232/// ```rust
233/// use pick_a_boo::{item, Item, OptionsBuilder};
234/// let options = OptionsBuilder::default()
235///     .item(Item::new_full("Yes", "y", 'y', Some("I love it")))
236///     .item(item!("So so", description = "I like it, but sometimes it's hard")) 
237///     .item(item!("Maybe", key = 'm', description = "I haven't tried it yet"))
238///     .item(item!("No", "n", "I don't like it"))
239///     .current(1) // set the default selected index to 1 ("So so")
240///     .build().expect("Failed to build Options");
241/// ```
242/// 
243/// ### Example: Create an instance from a slice of strings
244/// 
245/// In this case, each string is converted to an `Item` instance with the [`Item::parse`] method.
246/// 
247/// ```rust
248/// use pick_a_boo::Options;
249/// let options = Options::from(&["Yes", "So so", "Maybe", "No"]);
250/// ```
251/// 
252/// ### Errors
253/// 
254/// The builder will return an error if:
255/// - No items are provided.
256/// - The current index is out of bounds.
257/// - There are duplicate keys among the items.
258/// 
259#[derive(Debug, Builder)]
260#[builder(build_fn(validate = "validate_options", error = "ErrBox"))]
261pub struct Options {
262    #[builder(setter(each(name="item", into)))]
263    items: Vec<Item>,
264    #[builder(default = 0)]
265    current: usize,
266}
267
268fn validate_options(options: &OptionsBuilder) -> Result<(), ErrBox> {
269    let items = options.items.as_ref().ok_or("items must be set")?;
270    let current = options.current.unwrap_or(0);
271    validate_option_items(items, current)
272}
273
274fn validate_option_items(items: &[Item], current: usize) -> Result<(), ErrBox> {
275    if items.is_empty() {
276        return Err("items cannot be empty".into());
277    }
278    if current >= items.len() {
279        return Err(format!("{current}: current index is out of bounds (len: {})", items.len()).into());
280    }
281    if let Some(key) = find_duplicate_keys(items) {
282        return Err(format!("{key}: duplicate key found").into());
283    }
284    Ok(())
285}
286
287fn find_duplicate_keys(items: &[Item]) -> Option<char> {
288    use std::collections::HashSet;
289    let mut keys = HashSet::new();
290    for item in items {
291        if !keys.insert(item.key) {
292            return Some(item.key);
293        }
294    }
295    None
296}
297
298impl Options {
299    /// Helper method to create Options instance from a slice of strings.
300    /// Each item of the slice is converted with [`Item::parse`] method.
301    pub fn from<S: AsRef<str>>(items: &[S]) -> Result<Self, ErrBox> {
302        let item_vec = items.iter().map(|s| Item::parse(s.as_ref())).collect::<Vec<_>>();
303        validate_option_items(&item_vec, 0)?;
304        Ok(Options {
305            items: item_vec,
306            current: 0,
307        })
308    }
309
310    fn next(&self, picker: &Picker) -> usize {
311        let new_index = self.current + 1;
312        if picker.allow_wrap {
313            new_index % self.items.len()
314        } else {
315            std::cmp::min(new_index, self.items.len() - 1)
316        }
317    }
318
319    fn previous(&self, picker: &Picker) -> usize {
320        if self.current == 0 {
321            if picker.allow_wrap {
322                self.items.len() - 1
323            } else {
324                0
325            }
326        } else {
327            self.current - 1
328        }
329    }
330
331    /// Returns an iterator over the items.
332    pub fn iter(&self) -> std::slice::Iter<'_, Item> {
333        self.items.iter()
334    }
335
336    /// Returns the currently selected item.
337    pub fn current_item(&self) -> &Item {
338        &self.items[self.current]
339    }
340
341    /// Returns a Display struct for formatting the options for display with [Picker].
342    pub fn display<'b>(&self, picker: &'b Picker) -> Display<'_, 'b> {
343        Display(self, picker)
344    }
345
346    fn current_name(&self) -> String {
347        self.items[self.current].long_label.clone()
348    }
349
350    fn update_current(self, index: usize) -> Self {
351        Self {
352            current: index,
353            ..self
354        }
355    }
356}
357
358/// Display struct for formatting the options for display with [Picker].
359pub struct Display<'a, 'b>(&'a Options, &'b Picker);
360impl std::fmt::Display for Display<'_, '_> {
361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362        let picker = self.1;
363        let display = self.0.iter().enumerate()
364            .map(|(size, item)| {
365                if size == self.0.current {
366                    format!(" {} ", item.long_label)
367                } else {
368                    item.key.to_string()
369                }
370            }).collect::<Vec<_>>().join(&picker.delimiter);
371        write!(f, "{display}")
372    }
373}
374
375/// DescriptionShowMode enum defines how item descriptions are displayed.
376/// 
377/// ### Example
378/// 
379/// Based following definition of options, it illustrates the differences in display modes.
380/// 
381/// ```rust
382/// use pick_a_boo::{item, OptionsBuilder};
383/// let opts = OptionsBuilder::default()
384///     .item(item!("Yes", "y", "I love it"))
385///     .item(item!("So so", description = "I like it, but sometimes it's hard"))
386///     .item(item!("Maybe", key = 'm', description = "I haven't tried it yet"))
387///     .item(item!("No", "n", "I don't like it"))
388///     .build().expect("Failed to build Options");
389/// ```
390#[derive(Debug, Clone)]
391pub enum DescriptionShowMode {
392    /// Descriptions are never shown.
393    /// Default mode. 
394    /// 
395    /// ```text
396    /// Do you like Rust?  Yes /s/m/n
397    /// ```
398    Never,
399    /// Only the current item's description is shown.
400    /// 
401    /// ```text
402    /// Do you like Rust?  Yes /s/m/n
403    ///   Yes    I love it
404    /// ```
405    /// 
406    /// Other item is selected:
407    /// ```text
408    /// Do you like Rust?  y/s/ Maybe /n
409    ///   Maybe  I haven't tried it yet 
410    /// ```
411    CurrentOnly,
412    /// All item descriptions are shown.
413    /// 
414    /// ```text
415    /// Do you like Rust?  Yes /s/m/n
416    /// > Yes    I love it
417    ///   So so  I like it, but sometimes it's hard
418    ///   Maybe  I haven't tried it yet
419    ///   No     I don't like it
420    /// ```
421    /// 
422    /// Other item is selected:
423    /// ```text
424    /// Do you like Rust?  y/s/ Maybe /n
425    ///   Yes    I love it
426    ///   So so  I like it, but sometimes it's hard
427    /// > Maybe  I haven't tried it yet
428    ///   No     I don't like it
429    /// ```
430    All,
431}
432
433/// DescriptionNameWidth enum defines how the width of item names is determined.
434/// This enum is used when displaying item descriptions ([`DescriptionShowMode::CurrentOnly`], and [`DescriptionShowMode::All`]).
435#[derive(Debug, Clone)]
436pub enum DescriptionNameWidth {
437    /// no width adjustment is made.
438    Never,
439    /// fixed width adjustment is applied.
440    Fixed(usize),
441    /// auto width adjustment based on the maximum name length.
442    Auto,
443}
444
445/// Picker struct is the main interface for choosing options.
446/// It holds the following configuration for the picker behavior.
447/// 
448/// ### Example
449/// 
450/// ```text
451/// Do you like Rust? [Yes /s/m/n]
452/// > Yes    I love it
453///   So so  I like it, but sometimes it's hard
454///   Maybe  I haven't tried it yet
455///   No     I don't like it
456/// ```
457#[derive(Debug, Builder)]
458#[builder(build_fn(error = "ErrBox"))]
459pub struct Picker {
460    /// Delimiter string used to separate options in the display.
461    /// Defaults to "/".
462    /// 
463    /// The following example is using " | " as the delimiter:
464    /// 
465    /// ```text
466    /// Do you like Rust? [Yes |s|m|n]
467    /// ```
468    #[builder(default = "/".to_string(), setter(into))]
469    pub delimiter: String,
470    #[builder(default = false)]
471    /// Whether to use the alternate screen for the picker.
472    /// Default is `false`.
473    /// If `true`, the picker will switch to the alternate screen.
474    /// 
475    /// For more details, see [crossterm::terminal::EnterAlternateScreen].
476    pub alternate_screen: bool,
477    #[builder(default = false)]
478    /// Whether to allow wrapping around when navigating options.
479    /// Default is `false`.
480    /// If `true`, navigating past the last option will wrap around to the first option,
481    /// and vice versa.
482    pub allow_wrap: bool,
483    #[builder(default = None, setter(strip_option, into, custom))]
484    /// Parentheses to enclose the options display.
485    /// If `None`, no parentheses are used.
486    /// If `Some((left, right))`, the options will be enclosed with the specified left and right strings.
487    /// 
488    /// In the [`PickerBuilder`], use the `paren(AsRef<str>)` method to set this field.
489    /// see [PickerBuilder::paren] for details.
490    pub paren: Option<(String, String)>,
491    /// Mode for showing item descriptions. Default is [`DescriptionShowMode::Never`].
492    /// see [`DescriptionShowMode`] for details.
493    #[builder(default = DescriptionShowMode::Never)]
494    pub description_show_mode: DescriptionShowMode,
495    /// Width setting for item names when displaying descriptions.
496    /// Default is [`DescriptionNameWidth::Auto`].
497    /// see [`DescriptionNameWidth`] for details.
498    #[builder(default = DescriptionNameWidth::Auto, setter(into))]
499    pub description_name_width: DescriptionNameWidth,
500}
501
502impl PickerBuilder {
503    /// If the given string has an even length, it will be split into two equal halves for left and right parentheses.
504    /// Otherwise, the entire string will be used as the left parenthesis, and the right parenthesis will be an empty string.
505    /// 
506    /// For example:
507    /// - `paren("()")` sets parentheses to `Some(("(", ")"))`.
508    /// - `paren("[]")` sets parentheses to `Some(("[", "]"))`.
509    /// - `paren("[[]]")` sets parentheses to `Some(("[[", "]]"))`.
510    /// - `paren(":")` sets parentheses to `Some((":", ""))`.
511    /// - `paren(":::")` sets parentheses to `Some((":::", ""))`.
512    /// - `paren("")` sets parentheses to `Some(("", ""))`.
513    /// - Not calling `paren` leaves it as `None`.
514    pub fn paren<T: AsRef<str>>(&mut self, paren: T) -> &mut Self {
515        let paren = paren.as_ref().to_string();
516        log::info!("Setting paren: {paren}");
517        if paren.is_empty() {
518            self.paren = Some(None);
519            self
520        } else if paren.len() % 2 != 0 {
521            self.paren = Some(Some((paren, "".to_string())));
522            self
523        } else {
524            let len = paren.len() / 2;
525            let l = paren.chars().take(len).collect::<String>();
526            let r = paren.chars().skip(len).collect::<String>();
527            self.paren = Some(Some((l, r)));
528            self
529        }
530    }
531}
532
533impl Default for Picker {
534    /// Create a default Picker instance, it equivalent the following code.
535    /// 
536    /// ```rust
537    /// pick_a_boo::PickerBuilder::default()
538    ///     .delimiter("/")
539    ///     .alternate_screen(false)
540    ///     .allow_wrap(false)
541    ///     // .paren("")  // None by default
542    ///     .description_show_mode(pick_a_boo::DescriptionShowMode::Never)
543    ///     .description_name_width(pick_a_boo::DescriptionNameWidth::Auto)
544    ///     .build().expect("Failed to build Picker");
545    /// ```
546    fn default() -> Self {
547        log::info!("Building default Picker");
548        PickerBuilder::default()
549            .build().expect("Failed to build Picker")
550    }
551}
552
553impl Picker {
554    /// Choose an option from the provided [Options] with the given prompt.
555    /// Returns `Ok(Some(String))` for the selected option name, and `Ok(None)` if cancelled.
556    pub fn choose(&mut self, prompt: &str, options: Options) -> std::io::Result<Option<String>> {
557        log::info!("Picker choosing with prompt: {prompt}");
558        routine::choose(self, prompt, options)
559    }
560
561    /// Ask a yes-or-no question with the given prompt.
562    /// The `default_yes` parameter determines the default selection.
563    /// Returns `Ok(Some(true))` for "Yes", `Ok(Some(false))` for "No", and `Ok(None)` if cancelled.
564    pub fn yes_or_no(&mut self, prompt: &str, default_yes: bool) -> std::io::Result<Option<bool>> {
565        log::info!("Picker yes_or_no with prompt: {prompt}");
566        let yes_item = Item::new_full("Yes", "y", 'y', None);
567        let no_item = Item::new_full("No", "n", 'n', None);
568        let options = OptionsBuilder::default()
569            .item(yes_item)
570            .item(no_item)
571            .current(if default_yes { 0 } else { 1 })
572            .build().map_err(std::io::Error::other)?;
573        let answer = self.choose(prompt, options);
574        match answer {
575            Ok(Some(choice)) if choice == "Yes" => Ok(Some(true)),
576            Ok(Some(choice)) if choice == "No" => Ok(Some(false)),
577            Ok(Some(_)) => Ok(None),
578            Ok(None) => Ok(None),
579            Err(e) => Err(e),
580        }
581    }
582}
583
584/// Helper function to ask a yes-or-no question with the given prompt.
585/// This routine is a shortcut for creating a default [Picker] instance and
586/// calling its [Picker::yes_or_no] method.
587/// 
588/// ```rust
589/// fn run_yes_or_no(prompt: &str, default_yes: bool) -> std::io::Result<Option<bool>> {
590///     pick_a_boo::Picker::default()
591///         .yes_or_no(prompt, default_yes)
592/// }
593/// ```
594pub fn yes_or_no(prompt: &str, default_yes: bool) -> std::io::Result<Option<bool>> {
595    Picker::default()
596        .yes_or_no(prompt, default_yes)
597}
598
599/// Hellper function to choose an option from the provided [Options] with the given prompt.
600/// This routine is a shortcut for creating a default [Picker] instance and
601/// calling its [Picker::choose] method.
602/// 
603/// ```rust
604/// fn run_pick_a_boo(prompt: &str, options: pick_a_boo::Options) -> std::io::Result<Option<String>> {
605///     pick_a_boo::Picker::default()
606///         .choose(prompt, options)
607/// }
608/// ```
609pub fn choose(prompt: &str, options: Options) -> std::io::Result<Option<String>> {
610    Picker::default()
611        .choose(prompt, options)
612}
613
614#[cfg(test)]
615mod tests {
616    use crate::item;
617
618    #[test]
619    fn test_optionsbuilder_duplicate_keys() {
620        let result = crate::OptionsBuilder::default()
621            .item(item!("Option 1", "o", "description 1"))
622            .item(item!("Option 2", "o", "description 2")) // duplicate key
623            .build();
624        assert!(result.is_err());
625    }
626
627    #[test]
628    fn test_optionsbuilder_out_of_bounds_current() {
629        let result = crate::OptionsBuilder::default()
630            .item(item!("Option 1", "1"))
631            .item(item!("Option 2", "2"))
632            .current(10) // out of bounds
633            .build();
634        assert!(result.is_err());
635    }
636
637    #[test]
638    fn test_optionsbuilder_empty_items() {
639        let result = crate::OptionsBuilder::default()
640            .build();
641        assert!(result.is_err());
642    }
643
644    #[test]
645    fn test_optionsbuilder_no_items() {
646        let result = crate::OptionsBuilder::default()
647            .build();
648        assert!(result.is_err());
649    }
650
651    #[test]
652    fn test_from_str() {
653        let it: crate::Item = "Sample".into();
654        assert_eq!(it.long_label, "Sample");
655        assert_eq!(it.key, 's');
656        assert!(it.description.is_none());
657    }
658
659    #[test]
660    fn test_from_string() {
661        let it: crate::Item = String::from("Example: This is example").into();
662        assert_eq!(it.long_label, "Example");
663        assert_eq!(it.key, 'e');
664        assert_eq!(it.description.as_deref(), Some("This is example"));
665    }
666
667    #[test]
668    fn test_macro_item_1() {
669        let it = item!("Alpha");
670        assert_eq!(it.long_label, "Alpha");
671        assert_eq!(it.short_label, "a");
672        assert_eq!(it.key, 'a');
673        assert!(it.description.is_none());
674    }
675
676    #[test]
677    fn test_macro_item_2() {
678        let it = item!("Beta", "2");
679        assert_eq!(it.long_label, "Beta");
680        assert_eq!(it.short_label, "2");
681        assert_eq!(it.key, '2');
682        assert!(it.description.is_none())
683    }
684
685    #[test]
686    fn test_macro_item_3() {
687        let it = item!("Gamma", "3", "The third letter");
688        assert_eq!(it.long_label, "Gamma");
689        assert_eq!(it.short_label, "3");
690        assert_eq!(it.key, '3');
691        assert_eq!(it.description.as_deref(), Some("The third letter"));
692    }
693
694    #[test]
695    fn test_macro_item_4() {
696        let it = item!("Delta", "4", 'F', "The fourth letter");
697        assert_eq!(it.long_label, "Delta");
698        assert_eq!(it.short_label, "4");
699        assert_eq!(it.key, 'F');
700        assert_eq!(it.description.as_deref(), Some("The fourth letter"));
701    }
702
703    #[test]
704    fn test_macro_item_5() {
705        let it = item!("Epsilon", key = 'G');
706        assert_eq!(it.long_label, "Epsilon");
707        assert_eq!(it.short_label, "G");
708        assert_eq!(it.key, 'G');
709        assert!(it.description.is_none());
710    }
711
712    #[test]
713    fn test_macro_item_6() {
714        let it = item!("Zeta", description = "The sixth letter");
715        assert_eq!(it.long_label, "Zeta");
716        assert_eq!(it.short_label, "z");
717        assert_eq!(it.key, 'z');
718        assert_eq!(it.description.as_deref(), Some("The sixth letter"));
719    }
720
721    #[test]
722    fn test_macro_item_7() {
723        let it = item!("Eta", short = "7");
724        assert_eq!(it.long_label, "Eta");
725        assert_eq!(it.short_label, "7");
726        assert_eq!(it.key, '7');
727        assert!(it.description.is_none())
728    }
729
730    #[test]
731    fn test_macro_item_8() {
732        let it = item!("Theta", short = "A", key = '8');
733        assert_eq!(it.long_label, "Theta");
734        assert_eq!(it.short_label, "A");
735        assert_eq!(it.key, '8');
736        assert!(it.description.is_none());
737    }
738
739    #[test]
740    fn test_macro_item_9() {
741        let it = item!("Iota", short = "A", description = "The ninth letter");
742        assert_eq!(it.long_label, "Iota");
743        assert_eq!(it.short_label, "A");
744        assert_eq!(it.key, 'A');
745        assert_eq!(it.description.as_deref(), Some("The ninth letter"));
746    }
747
748    #[test]
749    fn test_macro_item_10() {
750        let it = item!("Kappa", short = "10", description = "The tenth letter", key = 'u');
751        assert_eq!(it.long_label, "Kappa");
752        assert_eq!(it.short_label, "10");
753        assert_eq!(it.key, 'u');
754        assert_eq!(it.description.as_deref(), Some("The tenth letter"));
755    }
756
757    #[test]
758    fn test_macro_item_11() {
759        let it = item!("Lambda", key = 'u', short = "11");
760        assert_eq!(it.long_label, "Lambda");
761        assert_eq!(it.short_label, "11");
762        assert_eq!(it.key, 'u');
763        assert!(it.description.is_none());
764    }
765
766    #[test]
767    fn test_macro_item_12() {
768        let it = item!("Mu", key = 'i', short = "12", description = "The twelveth letter");
769        assert_eq!(it.long_label, "Mu");
770        assert_eq!(it.short_label, "12");
771        assert_eq!(it.key, 'i');
772        assert_eq!(it.description.as_deref(), Some("The twelveth letter"));
773    }
774
775    #[test]
776    fn test_macro_item_13() { // Nu
777        let it = item!("Nu", key = 'i', description = "The thirteenth letter", short = "13");
778        assert_eq!(it.long_label, "Nu");
779        assert_eq!(it.short_label, "13");
780        assert_eq!(it.key, 'i');
781        assert_eq!(it.description.as_deref(), Some("The thirteenth letter"));
782    }
783
784    #[test]
785    fn test_macro_item_14() {
786        let it = item!("Xi", key = 'o', description = "The fourteenth letter");
787        assert_eq!(it.long_label, "Xi");
788        assert_eq!(it.short_label, "o");
789        assert_eq!(it.key, 'o');
790        assert_eq!(it.description.as_deref(), Some("The fourteenth letter"));
791    }
792
793    #[test]
794    fn test_macro_item_15() {
795        let it = item!("Omicron", short = "15", key = 'u', description = "The fifteenth letter");
796        assert_eq!(it.long_label, "Omicron");
797        assert_eq!(it.short_label, "15");
798        assert_eq!(it.key, 'u');
799        assert_eq!(it.description.as_deref(), Some("The fifteenth letter"));
800    }
801
802    #[test]
803    fn test_macro_item_16() {
804        let it = item!("Pi", description = "The sixteenth letter", short = "16");
805        assert_eq!(it.long_label, "Pi");
806        assert_eq!(it.short_label, "16");
807        assert_eq!(it.key, '1');
808        assert_eq!(it.description.as_deref(), Some("The sixteenth letter"));
809    }
810
811    #[test]
812    fn test_macro_item_17() {
813        let it = item!("Rho", description = "The seventeenth letter", key = 'K');
814        assert_eq!(it.long_label, "Rho");
815        assert_eq!(it.short_label, "K");
816        assert_eq!(it.key, 'K');
817        assert_eq!(it.description.as_deref(), Some("The seventeenth letter"));
818    }
819
820    #[test]
821    fn test_macro_item_18() {
822        let it = item!("Sigma", description = "The eighteenth letter", key = 'K', short = "k");
823        assert_eq!(it.long_label, "Sigma");
824        assert_eq!(it.short_label, "k");
825        assert_eq!(it.key, 'K');
826        assert_eq!(it.description.as_deref(), Some("The eighteenth letter"));
827    }
828
829    #[test]
830    fn test_macro_item_19() {
831        let it = item!("Tau", description = "The nineteenth letter", short = "k", key = 'K');
832        assert_eq!(it.long_label, "Tau");
833        assert_eq!(it.short_label, "k");
834        assert_eq!(it.key, 'K');
835        assert_eq!(it.description.as_deref(), Some("The nineteenth letter"));
836    }
837
838    #[test]
839    fn test_macro_parse_with_short_and_description() {
840        let it = item!("Upsilon(20): The twentieth letter");
841        assert_eq!(it.long_label, "Upsilon");
842        assert_eq!(it.short_label, "20");
843        assert_eq!(it.key, '2');
844        assert_eq!(it.description.as_deref(), Some("The twentieth letter"));
845    }
846
847    #[test]
848    fn test_item_parse_without_description() {
849        let it = crate::Item::parse("Phi");
850        assert_eq!(it.long_label, "Phi");
851        assert_eq!(it.short_label, "p");
852        assert_eq!(it.key, 'p');
853        assert!(it.description.is_none());
854    }
855
856    #[test]
857    fn test_item_parse_with_description() {
858        let it = crate::Item::parse("Chi: This is just test");
859        assert_eq!(it.long_label, "Chi");
860        assert_eq!(it.short_label, "c");
861        assert_eq!(it.key, 'c');
862        assert_eq!(it.description.as_deref(), Some("This is just test"));
863    }
864
865    #[test]
866    fn test_item_parse_with_short_without_description() {
867        let it = crate::Item::parse("Psi(Isp)");
868        assert_eq!(it.long_label, "Psi");
869        assert_eq!(it.short_label, "Isp");
870        assert_eq!(it.key, 'i');
871        assert!(it.description.is_none());
872    }
873
874    #[test]
875    fn test_macro_item_with_empty_name() {
876        let it = item!("");
877        assert_eq!(it.long_label, "");
878        assert_eq!(it.key, '\0');
879        assert!(it.description.is_none())
880    }
881}