use anyhow::{Context, Result};
pub trait TutorialPrompter {
fn pause(&self, message: &str) -> Result<()>;
fn confirm(&self, prompt: &str, default: bool) -> Result<bool>;
fn select(&self, prompt: &str, items: &[&str], default: usize) -> Result<usize>;
fn info(&self, message: &str);
}
pub struct DialoguerTutorialPrompter;
impl TutorialPrompter for DialoguerTutorialPrompter {
fn pause(&self, message: &str) -> Result<()> {
dialoguer::Confirm::new()
.with_prompt(message)
.default(true)
.show_default(false)
.interact()
.context("failed to get pause confirmation")?;
Ok(())
}
fn confirm(&self, prompt: &str, default: bool) -> Result<bool> {
dialoguer::Confirm::new()
.with_prompt(prompt)
.default(default)
.interact()
.context("failed to get confirmation")
}
fn select(&self, prompt: &str, items: &[&str], default: usize) -> Result<usize> {
dialoguer::Select::new()
.with_prompt(prompt)
.items(items)
.default(default)
.interact()
.context("failed to get selection")
}
fn info(&self, message: &str) {
println!("{}", message);
}
}
#[derive(Debug)]
pub struct ScriptedTutorialPrompter {
pub responses: Vec<ScriptedResponse>,
index: std::cell::Cell<usize>,
pub info_messages: std::cell::RefCell<Vec<String>>,
}
#[derive(Debug, Clone)]
pub enum ScriptedResponse {
Pause,
Confirm(bool),
Select(usize),
}
impl ScriptedTutorialPrompter {
pub fn new(responses: Vec<ScriptedResponse>) -> Self {
Self {
responses,
index: std::cell::Cell::new(0),
info_messages: std::cell::RefCell::new(Vec::new()),
}
}
fn next_response(&self) -> Result<ScriptedResponse> {
let idx = self.index.get();
if idx >= self.responses.len() {
anyhow::bail!(
"Scripted prompter ran out of responses (requested #{}, have {})",
idx + 1,
self.responses.len()
);
}
self.index.set(idx + 1);
Ok(self.responses[idx].clone())
}
}
impl TutorialPrompter for ScriptedTutorialPrompter {
fn pause(&self, _message: &str) -> Result<()> {
match self.next_response()? {
ScriptedResponse::Pause => Ok(()),
other => anyhow::bail!("Expected Pause response, got {:?}", other),
}
}
fn confirm(&self, _prompt: &str, _default: bool) -> Result<bool> {
match self.next_response()? {
ScriptedResponse::Confirm(val) => Ok(val),
other => anyhow::bail!("Expected Confirm response, got {:?}", other),
}
}
fn select(&self, _prompt: &str, items: &[&str], _default: usize) -> Result<usize> {
match self.next_response()? {
ScriptedResponse::Select(idx) => {
if idx >= items.len() {
anyhow::bail!("Select index {} out of range ({} items)", idx, items.len());
}
Ok(idx)
}
other => anyhow::bail!("Expected Select response, got {:?}", other),
}
}
fn info(&self, message: &str) {
self.info_messages.borrow_mut().push(message.to_string());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scripted_prompter_handles_pause() {
let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Pause]);
assert!(prompter.pause("test").is_ok());
}
#[test]
fn scripted_prompter_handles_confirm() {
let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Confirm(true)]);
assert!(prompter.confirm("test", false).unwrap());
}
#[test]
fn scripted_prompter_handles_select() {
let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Select(1)]);
assert_eq!(prompter.select("test", &["a", "b", "c"], 0).unwrap(), 1);
}
#[test]
fn scripted_prompter_select_out_of_range_errors() {
let prompter = ScriptedTutorialPrompter::new(vec![ScriptedResponse::Select(5)]);
assert!(prompter.select("test", &["a", "b"], 0).is_err());
}
#[test]
fn scripted_prompter_runs_out_of_responses() {
let prompter = ScriptedTutorialPrompter::new(vec![]);
assert!(prompter.pause("test").is_err());
}
#[test]
fn scripted_prompter_captures_info_messages() {
let prompter = ScriptedTutorialPrompter::new(vec![]);
prompter.info("message 1");
prompter.info("message 2");
let messages = prompter.info_messages.borrow();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0], "message 1");
assert_eq!(messages[1], "message 2");
}
}