ralph/commands/tutorial/
prompter.rs1use anyhow::{Context, Result};
12
13pub trait TutorialPrompter {
15 fn pause(&self, message: &str) -> Result<()>;
17
18 fn confirm(&self, prompt: &str, default: bool) -> Result<bool>;
20
21 fn select(&self, prompt: &str, items: &[&str], default: usize) -> Result<usize>;
23
24 fn info(&self, message: &str);
26}
27
28pub struct DialoguerTutorialPrompter;
30
31impl TutorialPrompter for DialoguerTutorialPrompter {
32 fn pause(&self, message: &str) -> Result<()> {
33 dialoguer::Confirm::new()
34 .with_prompt(message)
35 .default(true)
36 .show_default(false)
37 .interact()
38 .context("failed to get pause confirmation")?;
39 Ok(())
40 }
41
42 fn confirm(&self, prompt: &str, default: bool) -> Result<bool> {
43 dialoguer::Confirm::new()
44 .with_prompt(prompt)
45 .default(default)
46 .interact()
47 .context("failed to get confirmation")
48 }
49
50 fn select(&self, prompt: &str, items: &[&str], default: usize) -> Result<usize> {
51 dialoguer::Select::new()
52 .with_prompt(prompt)
53 .items(items)
54 .default(default)
55 .interact()
56 .context("failed to get selection")
57 }
58
59 fn info(&self, message: &str) {
60 println!("{}", message);
61 }
62}
63
64#[derive(Debug)]
66pub struct ScriptedTutorialPrompter {
67 pub responses: Vec<ScriptedResponse>,
69 index: std::cell::Cell<usize>,
71 pub info_messages: std::cell::RefCell<Vec<String>>,
73}
74
75#[derive(Debug, Clone)]
76pub enum ScriptedResponse {
77 Pause,
78 Confirm(bool),
79 Select(usize),
80}
81
82impl ScriptedTutorialPrompter {
83 pub fn new(responses: Vec<ScriptedResponse>) -> Self {
84 Self {
85 responses,
86 index: std::cell::Cell::new(0),
87 info_messages: std::cell::RefCell::new(Vec::new()),
88 }
89 }
90
91 fn next_response(&self) -> Result<ScriptedResponse> {
92 let idx = self.index.get();
93 if idx >= self.responses.len() {
94 anyhow::bail!(
95 "Scripted prompter ran out of responses (requested #{}, have {})",
96 idx + 1,
97 self.responses.len()
98 );
99 }
100 self.index.set(idx + 1);
101 Ok(self.responses[idx].clone())
102 }
103}
104
105impl TutorialPrompter for ScriptedTutorialPrompter {
106 fn pause(&self, _message: &str) -> Result<()> {
107 match self.next_response()? {
108 ScriptedResponse::Pause => Ok(()),
109 other => anyhow::bail!("Expected Pause response, got {:?}", other),
110 }
111 }
112
113 fn confirm(&self, _prompt: &str, _default: bool) -> Result<bool> {
114 match self.next_response()? {
115 ScriptedResponse::Confirm(val) => Ok(val),
116 other => anyhow::bail!("Expected Confirm response, got {:?}", other),
117 }
118 }
119
120 fn select(&self, _prompt: &str, items: &[&str], _default: usize) -> Result<usize> {
121 match self.next_response()? {
122 ScriptedResponse::Select(idx) => {
123 if idx >= items.len() {
124 anyhow::bail!("Select index {} out of range ({} items)", idx, items.len());
125 }
126 Ok(idx)
127 }
128 other => anyhow::bail!("Expected Select response, got {:?}", other),
129 }
130 }
131
132 fn info(&self, message: &str) {
133 self.info_messages.borrow_mut().push(message.to_string());
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn scripted_prompter_handles_pause() {
143 let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Pause]);
144 assert!(prompter.pause("test").is_ok());
145 }
146
147 #[test]
148 fn scripted_prompter_handles_confirm() {
149 let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Confirm(true)]);
150 assert!(prompter.confirm("test", false).unwrap());
151 }
152
153 #[test]
154 fn scripted_prompter_handles_select() {
155 let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Select(1)]);
156 assert_eq!(prompter.select("test", &["a", "b", "c"], 0).unwrap(), 1);
157 }
158
159 #[test]
160 fn scripted_prompter_select_out_of_range_errors() {
161 let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Select(5)]);
162 assert!(prompter.select("test", &["a", "b"], 0).is_err());
163 }
164
165 #[test]
166 fn scripted_prompter_runs_out_of_responses() {
167 let prompter = ScriptedTutorialPrompter::new(vec![]);
168 assert!(prompter.pause("test").is_err());
169 }
170
171 #[test]
172 fn scripted_prompter_captures_info_messages() {
173 let prompter = ScriptedTutorialPrompter::new(vec![]);
174 prompter.info("message 1");
175 prompter.info("message 2");
176 let messages = prompter.info_messages.borrow();
177 assert_eq!(messages.len(), 2);
178 assert_eq!(messages[0], "message 1");
179 assert_eq!(messages[1], "message 2");
180 }
181}