1use console::{Key, Term, measure_text_width, style};
2use std::{cmp::Ordering, iter};
3
4pub fn confirm(message: &str, term: &Term, default: bool) -> std::io::Result<bool> {
9 confirm_inner(message, None, term, default)
10}
11
12pub fn confirm_with_hint(
14 message: &str,
15 hint: &str,
16 term: &Term,
17 default: bool,
18) -> std::io::Result<bool> {
19 confirm_inner(message, Some(hint), term, default)
20}
21
22fn confirm_inner(
23 message: &str,
24 hint: Option<&str>,
25 term: &Term,
26 default: bool,
27) -> std::io::Result<bool> {
28 let prompt = format!(
29 "{} {} {} {} {}",
30 style("?".to_string()).for_stderr().yellow(),
31 style(message).for_stderr().bold(),
32 style("[y/n]").for_stderr().black().bright(),
33 style("›").for_stderr().black().bright(),
34 style(if default { "yes" } else { "no" })
35 .for_stderr()
36 .cyan(),
37 );
38
39 term.write_str(&prompt)?;
40 if let Some(hint) = hint {
41 term.write_str(&format!(
42 "\n\n{}{} {hint}",
43 style("hint").for_stderr().bold().cyan(),
44 style(":").for_stderr().bold()
45 ))?;
46 }
47 term.hide_cursor()?;
48 term.flush()?;
49
50 let response = loop {
53 let input = term.read_key_raw()?;
54 match input {
55 Key::Char('y' | 'Y') => break true,
56 Key::Char('n' | 'N') => break false,
57 Key::Enter => break default,
58 Key::CtrlC => {
59 let term = Term::stderr();
60 term.show_cursor()?;
61 term.write_str("\n")?;
62 term.flush()?;
63
64 #[expect(clippy::exit, clippy::cast_possible_wrap)]
65 std::process::exit(if cfg!(windows) {
66 0xC000_013A_u32 as i32
67 } else {
68 130
69 });
70 }
71 _ => {}
72 }
73 };
74
75 let report = format!(
76 "{} {} {} {}",
77 style("✔".to_string()).for_stderr().green(),
78 style(message).for_stderr().bold(),
79 style("·").for_stderr().black().bright(),
80 style(if response { "yes" } else { "no" })
81 .for_stderr()
82 .cyan(),
83 );
84
85 if hint.is_some() {
86 term.clear_last_lines(2)?;
87 term.clear_to_end_of_screen()?;
90 } else {
91 term.clear_line()?;
92 }
93 term.write_line(&report)?;
94 term.show_cursor()?;
95 term.flush()?;
96
97 Ok(response)
98}
99
100pub fn password(prompt: &str, term: &Term) -> std::io::Result<String> {
104 term.write_str(prompt)?;
105 term.show_cursor()?;
106 term.flush()?;
107
108 let input = term.read_secure_line()?;
109
110 term.clear_line()?;
111
112 Ok(input)
113}
114
115pub fn username(prompt: &str, term: &Term) -> std::io::Result<String> {
117 term.write_str(prompt)?;
118 term.show_cursor()?;
119 term.flush()?;
120
121 let input = term.read_line()?;
122
123 term.clear_line()?;
124
125 Ok(input)
126}
127
128#[allow(
132 clippy::cast_possible_truncation,
134 clippy::cast_possible_wrap,
135 clippy::cast_sign_loss
136)]
137pub fn input(prompt: &str, term: &Term) -> std::io::Result<String> {
138 term.write_str(prompt)?;
139 term.show_cursor()?;
140 term.flush()?;
141
142 let prompt_len = measure_text_width(prompt);
143
144 let mut chars: Vec<char> = Vec::new();
145 let mut position = 0;
146 loop {
147 match term.read_key()? {
148 Key::Backspace if position > 0 => {
149 position -= 1;
150 chars.remove(position);
151 let line_size = term.size().1 as usize;
152 if (position + prompt_len).is_multiple_of(line_size - 1) {
154 term.clear_line()?;
155 term.move_cursor_up(1)?;
156 term.move_cursor_right(line_size + 1)?;
157 } else {
158 term.clear_chars(1)?;
159 }
160
161 let tail: String = chars[position..].iter().collect();
162
163 if !tail.is_empty() {
164 term.write_str(&tail)?;
165
166 let total = position + prompt_len + tail.chars().count();
167 let total_line = total / line_size;
168 let line_cursor = (position + prompt_len) / line_size;
169 term.move_cursor_up(total_line - line_cursor)?;
170
171 term.move_cursor_left(line_size)?;
172 term.move_cursor_right((position + prompt_len) % line_size)?;
173 }
174
175 term.flush()?;
176 }
177 Key::Char(chr) if !chr.is_ascii_control() => {
178 chars.insert(position, chr);
179 position += 1;
180 let tail: String = iter::once(&chr).chain(chars[position..].iter()).collect();
181 term.write_str(&tail)?;
182 term.move_cursor_left(tail.chars().count() - 1)?;
183 term.flush()?;
184 }
185 Key::ArrowLeft if position > 0 => {
186 if (position + prompt_len).is_multiple_of(term.size().1 as usize) {
187 term.move_cursor_up(1)?;
188 term.move_cursor_right(term.size().1 as usize)?;
189 } else {
190 term.move_cursor_left(1)?;
191 }
192 position -= 1;
193 term.flush()?;
194 }
195 Key::ArrowRight if position < chars.len() => {
196 if (position + prompt_len).is_multiple_of(term.size().1 as usize - 1) {
197 term.move_cursor_down(1)?;
198 term.move_cursor_left(term.size().1 as usize)?;
199 } else {
200 term.move_cursor_right(1)?;
201 }
202 position += 1;
203 term.flush()?;
204 }
205 Key::UnknownEscSeq(seq) if seq == vec!['b'] => {
206 let line_size = term.size().1 as usize;
207 let nb_space = chars[..position]
208 .iter()
209 .rev()
210 .take_while(|c| c.is_whitespace())
211 .count();
212 let find_last_space = chars[..position - nb_space]
213 .iter()
214 .rposition(|c| c.is_whitespace());
215
216 if let Some(mut last_space) = find_last_space {
218 if last_space < position {
219 last_space += 1;
220 let new_line = (prompt_len + last_space) / line_size;
221 let old_line = (prompt_len + position) / line_size;
222 let diff_line = old_line - new_line;
223 if diff_line != 0 {
224 term.move_cursor_up(old_line - new_line)?;
225 }
226
227 let new_pos_x = (prompt_len + last_space) % line_size;
228 let old_pos_x = (prompt_len + position) % line_size;
229 let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
230 if diff_pos_x < 0 {
231 term.move_cursor_left(-diff_pos_x as usize)?;
232 } else {
233 term.move_cursor_right((diff_pos_x) as usize)?;
234 }
235 position = last_space;
236 }
237 } else {
238 term.move_cursor_left(position)?;
239 position = 0;
240 }
241
242 term.flush()?;
243 }
244 Key::UnknownEscSeq(seq) if seq == vec!['f'] => {
245 let line_size = term.size().1 as usize;
246 let find_next_space = chars[position..].iter().position(|c| c.is_whitespace());
247
248 if let Some(mut next_space) = find_next_space {
250 let nb_space = chars[position + next_space..]
251 .iter()
252 .take_while(|c| c.is_whitespace())
253 .count();
254 next_space += nb_space;
255 let new_line = (prompt_len + position + next_space) / line_size;
256 let old_line = (prompt_len + position) / line_size;
257 term.move_cursor_down(new_line - old_line)?;
258
259 let new_pos_x = (prompt_len + position + next_space) % line_size;
260 let old_pos_x = (prompt_len + position) % line_size;
261 let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
262 if diff_pos_x < 0 {
263 term.move_cursor_left(-diff_pos_x as usize)?;
264 } else {
265 term.move_cursor_right((diff_pos_x) as usize)?;
266 }
267 position += next_space;
268 } else {
269 let new_line = (prompt_len + chars.len()) / line_size;
270 let old_line = (prompt_len + position) / line_size;
271 term.move_cursor_down(new_line - old_line)?;
272
273 let new_pos_x = (prompt_len + chars.len()) % line_size;
274 let old_pos_x = (prompt_len + position) % line_size;
275 let diff_pos_x = new_pos_x as i64 - old_pos_x as i64;
276 match diff_pos_x.cmp(&0) {
277 Ordering::Less => {
278 term.move_cursor_left((-diff_pos_x - 1) as usize)?;
279 }
280 Ordering::Equal => {}
281 Ordering::Greater => {
282 term.move_cursor_right((diff_pos_x) as usize)?;
283 }
284 }
285 position = chars.len();
286 }
287
288 term.flush()?;
289 }
290 Key::Enter => break,
291 _ => (),
292 }
293 }
294 let input = chars.iter().collect::<String>();
295 term.write_line("")?;
296
297 Ok(input)
298}
299
300#[allow(
304 clippy::cast_possible_truncation,
305 clippy::cast_possible_wrap,
306 clippy::cast_precision_loss,
307 clippy::cast_sign_loss
308)]
309pub fn human_readable_bytes(bytes: u64) -> (f32, &'static str) {
310 const UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
311 let bytes_f32 = bytes as f32;
312 let i = ((bytes_f32.log2() / 10.0) as usize).min(UNITS.len() - 1);
313 (bytes_f32 / 1024_f32.powi(i as i32), UNITS[i])
314}