use std::{
borrow::Cow,
io::{self, BufRead, stdin},
path::{Path, PathBuf},
};
use fs_err as fs;
use crate::{
accessible::is_running_in_accessible_mode,
error::{Error, FinalError, Result},
utils::{
self, colors,
formatting::PathFmt,
io::{is_stdin_dev_null, lock_and_flush_output_stdio},
},
};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum QuestionPolicy {
Ask,
AlwaysYes,
AlwaysNo,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum QuestionAction {
Compression,
Decompression,
}
#[derive(Default)]
pub enum FileConflitOperation {
#[default]
Cancel,
Overwrite,
Rename,
Merge,
}
pub fn user_wants_to_overwrite(
path: &Path,
question_policy: QuestionPolicy,
question_action: QuestionAction,
) -> Result<FileConflitOperation> {
use FileConflitOperation as Op;
match question_policy {
QuestionPolicy::AlwaysYes => match question_action {
QuestionAction::Decompression => Ok(Op::Merge),
QuestionAction::Compression => Ok(Op::Overwrite),
},
QuestionPolicy::AlwaysNo => Ok(Op::Cancel),
QuestionPolicy::Ask => prompt_user_for_file_conflict_resolution(path, question_action),
}
}
pub fn prompt_user_for_file_conflict_resolution(
path: &Path,
question_action: QuestionAction,
) -> Result<FileConflitOperation> {
use FileConflitOperation as Op;
match question_action {
QuestionAction::Compression => ChoicePrompt::new(
format!("Do you want to overwrite {}?", PathFmt(path)),
[
("yes", Op::Overwrite, *colors::GREEN),
("no", Op::Cancel, *colors::RED),
("rename", Op::Rename, *colors::BLUE),
],
)
.ask(),
QuestionAction::Decompression => ChoicePrompt::new(
format!("Do you want to overwrite {}?", PathFmt(path)),
[
("yes", Op::Overwrite, *colors::GREEN),
("no", Op::Cancel, *colors::RED),
("rename", Op::Rename, *colors::BLUE),
("merge", Op::Merge, *colors::ORANGE),
],
)
.ask(),
}
}
pub fn create_file_or_prompt_on_conflict(
path: &Path,
question_policy: QuestionPolicy,
question_action: QuestionAction,
) -> Result<Option<(fs::File, PathBuf)>> {
let path = path.to_owned();
match fs::OpenOptions::new().write(true).create_new(true).open(&path) {
Ok(file) => return Ok(Some((file, path))),
Err(e) if e.kind() != io::ErrorKind::AlreadyExists => return Err(Error::from(e)),
Err(_file_already_exists) => {
}
}
let action = match question_policy {
QuestionPolicy::AlwaysYes => FileConflitOperation::Overwrite,
QuestionPolicy::AlwaysNo => FileConflitOperation::Cancel,
QuestionPolicy::Ask => prompt_user_for_file_conflict_resolution(&path, question_action)?,
};
let path_to_create_file = match action {
FileConflitOperation::Cancel => return Ok(None),
FileConflitOperation::Merge => path,
FileConflitOperation::Overwrite => {
utils::remove_file_or_dir(&path)?;
path
}
FileConflitOperation::Rename => utils::find_available_filename_by_renaming(&path)?,
};
let file = fs::File::create(&path_to_create_file)?;
Ok(Some((file, path_to_create_file)))
}
pub fn user_wants_to_continue(
path: &Path,
question_policy: QuestionPolicy,
question_action: QuestionAction,
) -> Result<bool> {
match question_policy {
QuestionPolicy::AlwaysYes => Ok(true),
QuestionPolicy::AlwaysNo => Ok(false),
QuestionPolicy::Ask => {
let action = match question_action {
QuestionAction::Compression => "compress",
QuestionAction::Decompression => "decompress",
};
let path = format!("{}", PathFmt(path));
let path = Some(&*path);
let placeholder = Some("FILE");
Confirmation::new(&format!("Do you want to {action} 'FILE'?"), placeholder).ask(path)
}
}
}
pub struct ChoicePrompt<'a, T: Default> {
pub prompt: String,
pub choises: Vec<Choice<'a, T>>,
}
pub struct Choice<'a, T: Default> {
label: &'a str,
value: T,
color: &'a str,
}
impl<'a, T: Default> ChoicePrompt<'a, T> {
pub fn new(prompt: impl Into<String>, choises: impl IntoIterator<Item = (&'a str, T, &'a str)>) -> Self {
Self {
prompt: prompt.into(),
choises: choises
.into_iter()
.map(|(label, value, color)| Choice { label, value, color })
.collect(),
}
}
pub fn ask(mut self) -> Result<T> {
let message = self.prompt;
if is_stdin_dev_null()? {
eprintln!("{message}");
eprintln!("Stdin is null, can't read user input (bypass with --yes, but be careful)");
return Ok(T::default());
}
let _locks = lock_and_flush_output_stdio()?;
let mut stdin_lock = stdin().lock();
loop {
let choice_prompt = if is_running_in_accessible_mode() {
self.choises
.iter()
.map(|choise| format!("{}{}{}", choise.color, choise.label, *colors::RESET))
.collect::<Vec<_>>()
.join("/")
} else {
let choises = self
.choises
.iter()
.map(|choise| {
format!(
"{}{}{}",
choise.color,
choise
.label
.chars()
.nth(0)
.expect("dev error, should be reported, we checked this won't happen"),
*colors::RESET
)
})
.collect::<Vec<_>>()
.join("/");
format!("[{choises}]")
};
eprintln!("{message} {choice_prompt}");
let mut answer = String::new();
let bytes_read = stdin_lock.read_line(&mut answer)?;
if bytes_read == 0 {
let error = FinalError::with_title("Unexpected EOF when asking question.")
.detail("When asking the user:")
.detail(format!(" \"{message}\""))
.detail("Expected one of the options as answer, but found EOF instead.")
.hint("If using Ouch in scripting, consider using `--yes` and `--no`.");
return Err(error.into());
}
answer.make_ascii_lowercase();
let answer = answer.trim();
let chosen_index = self.choises.iter().position(|choise| choise.label.starts_with(answer));
if let Some(i) = chosen_index {
return Ok(self.choises.remove(i).value);
}
}
}
}
pub struct Confirmation<'a> {
pub prompt: &'a str,
pub placeholder: Option<&'a str>,
}
impl<'a> Confirmation<'a> {
pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
Self {
prompt,
placeholder: pattern,
}
}
pub fn ask(&self, substitute: Option<&'a str>) -> 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)),
};
if is_stdin_dev_null()? {
eprintln!("{message}");
eprintln!("Stdin is null, can't read user input (bypass with --yes, but be careful)");
return Ok(false);
}
let _locks = lock_and_flush_output_stdio()?;
let mut stdin_lock = stdin().lock();
loop {
if is_running_in_accessible_mode() {
eprintln!(
"{} {}yes{}/{}no{}: ",
message,
*colors::GREEN,
*colors::RESET,
*colors::RED,
*colors::RESET
);
} else {
eprintln!(
"{} [{}Y{}/{}n{}] ",
message,
*colors::GREEN,
*colors::RESET,
*colors::RED,
*colors::RESET
);
}
let mut answer = String::new();
let bytes_read = stdin_lock.read_line(&mut answer)?;
if bytes_read == 0 {
let error = FinalError::with_title("Unexpected EOF when asking question.")
.detail("When asking the user:")
.detail(format!(" \"{message}\""))
.detail("Expected 'y' or 'n' as answer, but found EOF instead.")
.hint("If using Ouch in scripting, consider using `--yes` and `--no`.");
return Err(error.into());
}
answer.make_ascii_lowercase();
match answer.trim() {
"" | "y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => continue, }
}
}
}