1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
use std::io::{BufRead, Write};
use crate::confirm::{ConfirmCount, ConfirmPredicate};
/// A confirm for interactive confirmation.
///
/// This structure caches the confirmed state to avoid repeated prompts.
///
/// Typically, `ResConfirm` is registered via `ConfirmSetup`, and then injected into functions
/// through Mingling's resource injection system.
///
/// # Registration
///
/// Before use, the `ConfirmSetup` must be registered with the program:
///
/// ```
/// # use mingling::MockProgramCollect as ThisProgram;
/// use mingling::setup::ConfirmSetup;
/// use mingling::Program;
///
/// let mut program = Program::<ThisProgram>::new();
/// program.with_setup(ConfirmSetup);
/// ```
///
/// # Examples
///
/// ```
/// use mingling::res::ResConfirm;
/// use mingling::confirm::YesConfirm;
///
/// // In actual use, obtain the registered Confirm through the resource injection system
/// let confirm = ResConfirm::new_confirmed();
/// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] "));
/// ```
#[derive(Debug, Default, Clone, Copy)]
pub struct ResConfirm {
pub(crate) confirmed: bool,
}
impl ResConfirm {
/// Creates a new `ResConfirm` instance.
///
/// # Examples
///
/// ```
/// use mingling::res::ResConfirm;
///
/// let confirm = ResConfirm::new();
/// ```
#[must_use]
pub const fn new() -> Self {
Self { confirmed: false }
}
/// Creates a `Confirm` instance in the confirmed state.
///
/// The returned `Confirm` will directly return `true` when calling [`ask`](ResConfirm::ask) or
/// [`try_ask`](ResConfirm::try_ask), without prompting the user.
///
/// # Examples
///
/// ```
/// use mingling::res::ResConfirm;
/// use mingling::confirm::YesConfirm;
///
/// let confirm = ResConfirm::new_confirmed();
/// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] "));
/// ```
#[must_use]
pub const fn new_confirmed() -> Self {
Self { confirmed: true }
}
/// Marks the Confirm as confirmed.
///
/// After calling this method, subsequent calls to [`ask`](ResConfirm::ask) or
/// [`try_ask`](ResConfirm::try_ask) on this Confirm will directly return `true`
/// without prompting the user.
///
/// # Examples
///
/// ```
/// use mingling::res::ResConfirm;
/// use mingling::confirm::YesConfirm;
///
/// let mut confirm = ResConfirm::new();
/// confirm.set_confirmed();
/// assert!(confirm.ask::<YesConfirm>("Continue? [y/n] "));
/// ```
pub const fn set_confirmed(&mut self) {
self.confirmed = true;
}
/// Asks the user a confirmation question, with at most one attempt.
///
/// Returns `false` if the user provides an unrecognizable answer.
/// Returns `true` directly if already confirmed previously.
///
/// # Parameters
///
/// * `ask` - The prompt text to display to the user.
///
/// # Returns
///
/// Returns a boolean indicating whether the user confirmed. Returns `false` if the user's input
/// could not be parsed or the maximum number of attempts was reached.
///
/// # Examples
///
/// ```
/// use mingling::res::ResConfirm;
/// use mingling::confirm::YesConfirm;
///
/// let confirm = ResConfirm::new_confirmed();
/// let confirmed = confirm.ask::<YesConfirm>("Delete this file? [y/n] ");
/// ```
pub fn ask<P: ConfirmPredicate>(&self, ask: impl AsRef<str>) -> bool {
self.try_ask::<P>(ask, ConfirmCount::Max(1))
.unwrap_or(false)
}
/// Asks the user a confirmation question, allowing a specified maximum number of attempts.
///
/// # Parameters
///
/// * `ask` - The prompt text to display to the user.
/// * `count` - The maximum number of attempts. Passing `0` means unlimited attempts (loop
/// indefinitely), passing a positive integer means at most that many attempts.
///
/// # Returns
///
/// Returns `Some(true)` for confirmation, `Some(false)` for rejection.
/// Returns `None` if the maximum number of attempts is reached without being able to parse
/// the user's input.
///
/// # Panics
///
/// This function panics when the standard error output (`stderr`) cannot be flushed or when
/// reading from standard input fails.
///
/// # Examples
///
/// ```
/// use mingling::res::ResConfirm;
/// use mingling::confirm::YesConfirm;
///
/// let confirm = ResConfirm::new_confirmed();
/// let confirmed = confirm.try_ask::<YesConfirm>("Confirm execution? [y/n] ", 3);
/// ```
pub fn try_ask<P: ConfirmPredicate>(
&self,
ask: impl AsRef<str>,
count: impl Into<ConfirmCount>,
) -> Option<bool> {
if self.confirmed {
return Some(true);
}
let count = count.into();
let mut attempts = 0usize;
loop {
eprint!("{}", ask.as_ref());
std::io::stderr().flush().unwrap();
let stdin = std::io::stdin();
let mut input = String::new();
stdin.lock().read_line(&mut input).unwrap();
if let Some(result) = P::is_yes(&input) {
return Some(result);
}
attempts += 1;
match count {
ConfirmCount::Loop => {}
ConfirmCount::Max(max) => {
if attempts >= max {
return None;
}
}
}
}
}
}
|