ouch/src/utils/question.rs
2021-12-07 21:40:03 +00:00

142 lines
5.4 KiB
Rust

//! Utils related to asking [Y/n] questions to the user.
//!
//! Example:
//! "Do you want to overwrite 'archive.tar.gz'? [Y/n]"
use std::{
borrow::Cow,
io::{self, Write},
path::Path,
};
use fs_err as fs;
use super::{strip_cur_dir, to_utf};
use crate::{
error::{Error, Result},
utils::colors,
};
#[derive(Debug, PartialEq, Clone, Copy)]
/// Determines if overwrite questions should be skipped or asked to the user
pub enum QuestionPolicy {
/// Ask the user every time
Ask,
/// Set by `--yes`, will say 'Y' to all overwrite questions
AlwaysYes,
/// Set by `--no`, will say 'N' to all overwrite questions
AlwaysNo,
}
/// Check if QuestionPolicy flags were set, otherwise, ask user if they want to overwrite.
pub fn user_wants_to_overwrite(path: &Path, question_policy: QuestionPolicy) -> crate::Result<bool> {
match question_policy {
QuestionPolicy::AlwaysYes => Ok(true),
QuestionPolicy::AlwaysNo => Ok(false),
QuestionPolicy::Ask => {
let path = to_utf(strip_cur_dir(path));
let path = Some(path.as_str());
let placeholder = Some("FILE");
Confirmation::new("Do you want to overwrite 'FILE'?", placeholder).ask(path)
}
}
}
/// Create the file if it doesn't exist and if it does then ask to overwrite it.
/// If the user doesn't want to overwrite then we return [`Ok(None)`]
pub fn create_or_ask_overwrite(path: &Path, question_policy: QuestionPolicy) -> Result<Option<fs::File>> {
match fs::OpenOptions::new().write(true).create_new(true).open(path) {
Ok(w) => Ok(Some(w)),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if user_wants_to_overwrite(path, question_policy)? {
if path.is_dir() {
// We can't just use `fs::File::create(&path)` because it would return io::ErrorKind::IsADirectory
// ToDo: Maybe we should emphasise that `path` is a directory and everything inside it will be gone?
fs::remove_dir_all(path)?;
}
Ok(Some(fs::File::create(path)?))
} else {
Ok(None)
}
}
Err(e) => Err(Error::from(e)),
}
}
/// Check if QuestionPolicy flags were set, otherwise, ask the user if they want to continue compressing.
pub fn user_wants_to_continue_compressing(path: &Path, question_policy: QuestionPolicy) -> crate::Result<bool> {
match question_policy {
QuestionPolicy::AlwaysYes => Ok(true),
QuestionPolicy::AlwaysNo => Ok(false),
QuestionPolicy::Ask => {
let path = to_utf(strip_cur_dir(path));
let path = Some(path.as_str());
let placeholder = Some("FILE");
Confirmation::new("Do you want to continue compressing 'FILE'?", placeholder).ask(path)
}
}
}
/// Check if QuestionPolicy flags were set, otherwise, ask the user if they want to continue decompressing.
pub fn user_wants_to_continue_decompressing(path: &Path, question_policy: QuestionPolicy) -> crate::Result<bool> {
match question_policy {
QuestionPolicy::AlwaysYes => Ok(true),
QuestionPolicy::AlwaysNo => Ok(false),
QuestionPolicy::Ask => {
let path = to_utf(strip_cur_dir(path));
let path = Some(path.as_str());
let placeholder = Some("FILE");
Confirmation::new("Do you want to continue decompressing 'FILE'?", placeholder).ask(path)
}
}
}
/// Confirmation dialog for end user with [Y/n] question.
///
/// If the placeholder is found in the prompt text, it will be replaced to form the final message.
pub struct Confirmation<'a> {
/// The message to be displayed with the placeholder text in it.
/// e.g.: "Do you want to overwrite 'FILE'?"
pub prompt: &'a str,
/// The placeholder text that will be replaced in the `ask` function:
/// e.g.: Some("FILE")
pub placeholder: Option<&'a str>,
}
impl<'a> Confirmation<'a> {
/// Creates a new Confirmation.
pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
Self { prompt, placeholder: pattern }
}
/// Creates user message and receives a boolean input to be used on the program
pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
let message = match (self.placeholder, substitute) {
(None, _) => Cow::Borrowed(self.prompt),
(Some(_), None) => unreachable!("dev error, should be reported, we checked this won't happen"),
(Some(placeholder), Some(subs)) => Cow::Owned(self.prompt.replace(placeholder, subs)),
};
// Ask the same question to end while no valid answers are given
loop {
if *crate::cli::ACCESSIBLE.get().unwrap() {
print!("{} {}yes{}/{}no{}: ", message, *colors::GREEN, *colors::RESET, *colors::RED, *colors::RESET);
} else {
print!("{} [{}Y{}/{}n{}] ", message, *colors::GREEN, *colors::RESET, *colors::RED, *colors::RESET);
}
io::stdout().flush()?;
let mut answer = String::new();
io::stdin().read_line(&mut answer)?;
answer.make_ascii_lowercase();
match answer.trim() {
"" | "y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => continue, // Try again
}
}
}
}