1use crate::core::Process;
26use crate::error::{ProcError, Result};
27use crate::ui::{interrupt, out};
28use crossterm::{cursor, execute};
29use dialoguer::Confirm;
30
31pub fn ask(
39 action: &str,
40 processes: &[Process],
41 not_found: Option<&[String]>,
42 question: &str,
43 show: impl FnOnce(),
44) -> Result<bool> {
45 let shown = list_shown(out::stdout_is_terminal(), show);
46 answer_if_shown(shown, action, processes, not_found, || interact(question))
47}
48
49fn list_shown(stdout_is_terminal: bool, show: impl FnOnce()) -> bool {
51 if stdout_is_terminal {
52 show();
53 out::stdout_usable()
54 } else {
55 out::on_stderr(show);
56 out::stderr_usable()
57 }
58}
59
60fn answer_if_shown(
62 shown: bool,
63 action: &str,
64 processes: &[Process],
65 not_found: Option<&[String]>,
66 answer: impl FnOnce() -> Result<bool>,
67) -> Result<bool> {
68 if !shown {
69 return Err(ProcError::ConfirmationRequired {
70 message: format!(
71 "{action} asks before changing processes, and the list of what it would \
72 change could not be shown; pass --yes (or --dry-run to preview)"
73 ),
74 processes: processes.to_vec(),
75 not_found: not_found.map(<[String]>::to_vec),
76 });
77 }
78 answer()
79}
80
81#[allow(clippy::disallowed_methods)]
83fn interact(question: &str) -> Result<bool> {
84 let _protected = interrupt::protect_prompt();
85 let answer = Confirm::new()
86 .with_prompt(question)
87 .default(false)
88 .interact();
89 if let Err(dialoguer::Error::IO(error)) = &answer {
91 if error.kind() == std::io::ErrorKind::Interrupted {
92 interrupt::ctrl_c();
93 }
94 }
95 let _ = execute!(out::stderr_unrecorded(), cursor::Show);
98 Ok(answer?)
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use std::cell::Cell;
105
106 #[test]
107 fn the_list_is_shown_on_either_stream_and_the_question_waits_for_it() {
108 for stdout_is_terminal in [true, false] {
109 let shown = Cell::new(0);
110 assert!(list_shown(stdout_is_terminal, || shown.set(shown.get() + 1)));
111 assert_eq!(shown.get(), 1);
112 }
113 }
114
115 #[test]
116 fn a_list_that_was_not_shown_is_a_refusal_with_what_would_have_changed() {
117 let missing = vec!["ghost".to_string()];
118 let refused = answer_if_shown(false, "kill", &[], Some(&missing), || panic!("asked"))
119 .expect_err("nothing is asked when the list was not shown");
120 assert_eq!(crate::error::ExitCode::from(&refused) as i32, 4);
121 assert_eq!(refused.reason(), Some("needs_confirmation"));
122 assert!(
123 refused.to_string().contains("could not be shown"),
124 "{refused}"
125 );
126 assert!(format!("{refused:?}").contains("ghost"), "{refused:?}");
127 }
128
129 #[test]
130 fn a_shown_list_leaves_the_answer_to_the_person() {
131 assert!(answer_if_shown(true, "kill", &[], None, || Ok(true)).unwrap());
132 assert!(!answer_if_shown(true, "kill", &[], None, || Ok(false)).unwrap());
133 }
134}