Reuse zip warning message
[ouch.git] / src / utils / question.rs
blob65039440eec25ad7d28fb07c339ed4621605200b
1 //! Utils related to asking [Y/n] questions to the user.
2 //!
3 //! Example:
4 //!   "Do you want to overwrite 'archive.tar.gz'? [Y/n]"
6 use std::{
7     borrow::Cow,
8     io::{self, Write},
9     path::Path,
12 use fs_err as fs;
14 use super::{strip_cur_dir, to_utf};
15 use crate::{
16     error::{Error, Result},
17     utils::colors,
20 #[derive(Debug, PartialEq, Clone, Copy)]
21 /// Determines if overwrite questions should be skipped or asked to the user
22 pub enum QuestionPolicy {
23     /// Ask the user every time
24     Ask,
25     /// Set by `--yes`, will say 'Y' to all overwrite questions
26     AlwaysYes,
27     /// Set by `--no`, will say 'N' to all overwrite questions
28     AlwaysNo,
31 #[derive(Debug, PartialEq, Clone, Copy)]
32 /// Determines which action is being questioned
33 pub enum QuestionAction {
34     /// question called from a compression function
35     Compression,
36     /// question called from a decompression function
37     Decompression,
40 /// Check if QuestionPolicy flags were set, otherwise, ask user if they want to overwrite.
41 pub fn user_wants_to_overwrite(path: &Path, question_policy: QuestionPolicy) -> crate::Result<bool> {
42     match question_policy {
43         QuestionPolicy::AlwaysYes => Ok(true),
44         QuestionPolicy::AlwaysNo => Ok(false),
45         QuestionPolicy::Ask => {
46             let path = to_utf(strip_cur_dir(path));
47             let path = Some(path.as_str());
48             let placeholder = Some("FILE");
49             Confirmation::new("Do you want to overwrite 'FILE'?", placeholder).ask(path)
50         }
51     }
54 /// Create the file if it doesn't exist and if it does then ask to overwrite it.
55 /// If the user doesn't want to overwrite then we return [`Ok(None)`]
56 pub fn create_or_ask_overwrite(path: &Path, question_policy: QuestionPolicy) -> Result<Option<fs::File>> {
57     match fs::OpenOptions::new().write(true).create_new(true).open(path) {
58         Ok(w) => Ok(Some(w)),
59         Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
60             if user_wants_to_overwrite(path, question_policy)? {
61                 if path.is_dir() {
62                     // We can't just use `fs::File::create(&path)` because it would return io::ErrorKind::IsADirectory
63                     // ToDo: Maybe we should emphasise that `path` is a directory and everything inside it will be gone?
64                     fs::remove_dir_all(path)?;
65                 }
66                 Ok(Some(fs::File::create(path)?))
67             } else {
68                 Ok(None)
69             }
70         }
71         Err(e) => Err(Error::from(e)),
72     }
75 /// Check if QuestionPolicy flags were set, otherwise, ask the user if they want to continue.
76 pub fn user_wants_to_continue(
77     path: &Path,
78     question_policy: QuestionPolicy,
79     question_action: QuestionAction,
80 ) -> crate::Result<bool> {
81     match question_policy {
82         QuestionPolicy::AlwaysYes => Ok(true),
83         QuestionPolicy::AlwaysNo => Ok(false),
84         QuestionPolicy::Ask => {
85             let action = match question_action {
86                 QuestionAction::Compression => "compressing",
87                 QuestionAction::Decompression => "decompressing",
88             };
89             let path = to_utf(strip_cur_dir(path));
90             let path = Some(path.as_str());
91             let placeholder = Some("FILE");
92             Confirmation::new(&format!("Do you want to continue {} 'FILE'?", action), placeholder).ask(path)
93         }
94     }
97 /// Confirmation dialog for end user with [Y/n] question.
98 ///
99 /// If the placeholder is found in the prompt text, it will be replaced to form the final message.
100 pub struct Confirmation<'a> {
101     /// The message to be displayed with the placeholder text in it.
102     /// e.g.: "Do you want to overwrite 'FILE'?"
103     pub prompt: &'a str,
105     /// The placeholder text that will be replaced in the `ask` function:
106     /// e.g.: Some("FILE")
107     pub placeholder: Option<&'a str>,
110 impl<'a> Confirmation<'a> {
111     /// Creates a new Confirmation.
112     pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
113         Self { prompt, placeholder: pattern }
114     }
116     /// Creates user message and receives a boolean input to be used on the program
117     pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
118         let message = match (self.placeholder, substitute) {
119             (None, _) => Cow::Borrowed(self.prompt),
120             (Some(_), None) => unreachable!("dev error, should be reported, we checked this won't happen"),
121             (Some(placeholder), Some(subs)) => Cow::Owned(self.prompt.replace(placeholder, subs)),
122         };
124         // Ask the same question to end while no valid answers are given
125         loop {
126             if *crate::cli::ACCESSIBLE.get().unwrap() {
127                 print!("{} {}yes{}/{}no{}: ", message, *colors::GREEN, *colors::RESET, *colors::RED, *colors::RESET);
128             } else {
129                 print!("{} [{}Y{}/{}n{}] ", message, *colors::GREEN, *colors::RESET, *colors::RED, *colors::RESET);
130             }
131             io::stdout().flush()?;
133             let mut answer = String::new();
134             io::stdin().read_line(&mut answer)?;
136             answer.make_ascii_lowercase();
137             match answer.trim() {
138                 "" | "y" | "yes" => return Ok(true),
139                 "n" | "no" => return Ok(false),
140                 _ => continue, // Try again
141             }
142         }
143     }