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}