Skip to main content

cli_ui/prompt/
group.rs

1#![allow(missing_docs)]
2//! Sequentially-chained prompts — ports `@clack/prompts` `group()`.
3//!
4//! Each step is a closure that receives the map of results so far. The whole
5//! group bails out on the first cancellation unless `on_cancel` returns true.
6//!
7//! # Example
8//! ```rust,no_run
9//! use cli_ui::prompt::{group, text, confirm};
10//!
11//! let answers = group()
12//!     .step("name",   |_| Ok(text("Your name").run()?))
13//!     .step("public", |_| Ok(confirm("Make profile public?").run()?.to_string()))
14//!     .run()
15//!     .unwrap();
16//!
17//! println!("{:?}", answers.get("name"));
18//! ```
19
20use super::error::{PromptError, Result};
21use std::collections::BTreeMap;
22
23type Answers = BTreeMap<String, String>;
24type StepFn = Box<dyn FnOnce(&Answers) -> Result<String>>;
25
26pub struct GroupBuilder {
27    steps: Vec<(String, StepFn)>,
28}
29
30pub fn group() -> GroupBuilder {
31    GroupBuilder { steps: Vec::new() }
32}
33
34impl GroupBuilder {
35    /// Add a step. The closure may inspect previously collected answers.
36    pub fn step<F>(mut self, name: impl Into<String>, f: F) -> Self
37    where
38        F: FnOnce(&Answers) -> Result<String> + 'static,
39    {
40        self.steps.push((name.into(), Box::new(f)));
41        self
42    }
43
44    pub fn run(self) -> Result<Answers> {
45        let mut answers = Answers::new();
46        for (name, step) in self.steps {
47            match step(&answers) {
48                Ok(v) => {
49                    answers.insert(name, v);
50                }
51                Err(PromptError::Interrupted) => {
52                    super::cancel("Cancelled");
53                    return Err(PromptError::Interrupted);
54                }
55                Err(e) => return Err(e),
56            }
57        }
58        Ok(answers)
59    }
60}