refactor: improve code formatting in `mod.rs` and `logger.rs`
[ouch.git] / src / utils / question.rs
blobee36e741e865055e8c8969ff84f3c9e34417b779
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::{stdin, BufRead, IsTerminal},
9     path::Path,
12 use fs_err as fs;
14 use crate::{
15     accessible::is_running_in_accessible_mode,
16     error::{Error, FinalError, Result},
17     utils::{self, colors, formatting::path_to_str, io::lock_and_flush_output_stdio, strip_cur_dir},
20 #[derive(Debug, PartialEq, Eq, 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, Eq, 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 = path_to_str(strip_cur_dir(path));
47             let path = Some(&*path);
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 ask_to_create_file(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                 utils::remove_file_or_dir(path)?;
62                 Ok(Some(fs::File::create(path)?))
63             } else {
64                 Ok(None)
65             }
66         }
67         Err(e) => Err(Error::from(e)),
68     }
71 /// Check if QuestionPolicy flags were set, otherwise, ask the user if they want to continue.
72 pub fn user_wants_to_continue(
73     path: &Path,
74     question_policy: QuestionPolicy,
75     question_action: QuestionAction,
76 ) -> crate::Result<bool> {
77     match question_policy {
78         QuestionPolicy::AlwaysYes => Ok(true),
79         QuestionPolicy::AlwaysNo => Ok(false),
80         QuestionPolicy::Ask => {
81             let action = match question_action {
82                 QuestionAction::Compression => "compress",
83                 QuestionAction::Decompression => "decompress",
84             };
85             let path = path_to_str(strip_cur_dir(path));
86             let path = Some(&*path);
87             let placeholder = Some("FILE");
88             Confirmation::new(&format!("Do you want to {action} 'FILE'?"), placeholder).ask(path)
89         }
90     }
93 /// Confirmation dialog for end user with [Y/n] question.
94 ///
95 /// If the placeholder is found in the prompt text, it will be replaced to form the final message.
96 pub struct Confirmation<'a> {
97     /// The message to be displayed with the placeholder text in it.
98     /// e.g.: "Do you want to overwrite 'FILE'?"
99     pub prompt: &'a str,
101     /// The placeholder text that will be replaced in the `ask` function:
102     /// e.g.: Some("FILE")
103     pub placeholder: Option<&'a str>,
106 impl<'a> Confirmation<'a> {
107     /// Creates a new Confirmation.
108     pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
109         Self {
110             prompt,
111             placeholder: pattern,
112         }
113     }
115     /// Creates user message and receives a boolean input to be used on the program
116     pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
117         let message = match (self.placeholder, substitute) {
118             (None, _) => Cow::Borrowed(self.prompt),
119             (Some(_), None) => unreachable!("dev error, should be reported, we checked this won't happen"),
120             (Some(placeholder), Some(subs)) => Cow::Owned(self.prompt.replace(placeholder, subs)),
121         };
123         if !stdin().is_terminal() {
124             eprintln!("{}", message);
125             eprintln!("Pass --yes to proceed");
126             return Ok(false);
127         }
129         let _locks = lock_and_flush_output_stdio()?;
130         let mut stdin_lock = stdin().lock();
132         // Ask the same question to end while no valid answers are given
133         loop {
134             if is_running_in_accessible_mode() {
135                 eprintln!(
136                     "{} {}yes{}/{}no{}: ",
137                     message,
138                     *colors::GREEN,
139                     *colors::RESET,
140                     *colors::RED,
141                     *colors::RESET
142                 );
143             } else {
144                 eprintln!(
145                     "{} [{}Y{}/{}n{}] ",
146                     message,
147                     *colors::GREEN,
148                     *colors::RESET,
149                     *colors::RED,
150                     *colors::RESET
151                 );
152             }
154             let mut answer = String::new();
155             let bytes_read = stdin_lock.read_line(&mut answer)?;
157             if bytes_read == 0 {
158                 let error = FinalError::with_title("Unexpected EOF when asking question.")
159                     .detail("When asking the user:")
160                     .detail(format!("  \"{message}\""))
161                     .detail("Expected 'y' or 'n' as answer, but found EOF instead.")
162                     .hint("If using Ouch in scripting, consider using `--yes` and `--no`.");
164                 return Err(error.into());
165             }
167             answer.make_ascii_lowercase();
168             match answer.trim() {
169                 "" | "y" | "yes" => return Ok(true),
170                 "n" | "no" => return Ok(false),
171                 _ => continue, // Try again
172             }
173         }
174     }