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}