1use crate::cli::output::Output;
23use scope::error::ScopeError;
24use std::io::{self, BufRead};
25use std::sync::atomic::{AtomicBool, Ordering};
26
27pub(crate) static CANCEL: AtomicBool = AtomicBool::new(false);
31
32#[cfg(unix)]
35const POLL_INTERVAL_MS: libc::c_int = 100;
36
37fn cancelled() -> io::Error {
39 io::Error::new(io::ErrorKind::Interrupted, "cancelled")
40}
41
42pub(crate) fn ask(reader: &mut impl BufRead, out: &Output, question: &str) -> io::Result<String> {
52 crate::cli::progress::paused(|| {
53 crate::err!(out, "{question}")?;
54 let mut answer = String::new();
55 reader.read_line(&mut answer)?;
56 Ok(answer)
57 })
58}
59
60pub fn ask_stdin(out: &Output, question: &str) -> io::Result<String> {
67 ask(&mut stdin(), out, question)
68}
69
70pub(crate) fn pick(
78 reader: &mut impl BufRead,
79 out: &Output,
80 list: &str,
81 what: &str,
82 count: usize,
83) -> scope::error::Result<usize> {
84 let question = format!("{list}\nSelect {what} (1-{count}): ");
85 let answer = ask(reader, out, &question)?;
86 let n: usize = answer
87 .trim()
88 .parse()
89 .map_err(|_| ScopeError::Api("Invalid selection".to_string()))?;
90 if !(1..=count).contains(&n) {
91 return Err(ScopeError::Api(format!(
92 "Selection must be between 1 and {count}"
93 )));
94 }
95 Ok(n - 1)
96}
97
98#[cfg(unix)]
100pub(crate) fn stdin() -> PromptInput<'static> {
101 PromptInput::new(libc::STDIN_FILENO, &CANCEL)
102}
103
104#[cfg(not(unix))]
106pub(crate) fn stdin() -> io::StdinLock<'static> {
107 io::stdin().lock()
108}
109
110#[cfg(unix)]
112pub(crate) struct PromptInput<'a> {
113 fd: std::os::fd::RawFd,
114 cancel: &'a AtomicBool,
115 byte: [u8; 1],
117 has_byte: bool,
118}
119
120#[cfg(unix)]
121impl<'a> PromptInput<'a> {
122 pub(crate) fn new(fd: std::os::fd::RawFd, cancel: &'a AtomicBool) -> Self {
123 Self {
124 fd,
125 cancel,
126 byte: [0],
127 has_byte: false,
128 }
129 }
130
131 fn read_byte(&mut self) -> io::Result<Option<u8>> {
135 if std::mem::take(&mut self.has_byte) {
136 return Ok(Some(self.byte[0]));
137 }
138 loop {
139 if self.cancel.load(Ordering::SeqCst) {
140 return Err(cancelled());
141 }
142 let mut pfd = libc::pollfd {
143 fd: self.fd,
144 events: libc::POLLIN,
145 revents: 0,
146 };
147 let ready = unsafe { libc::poll(&mut pfd, 1, POLL_INTERVAL_MS) };
149 if ready < 0 {
150 let e = io::Error::last_os_error();
151 if e.kind() == io::ErrorKind::Interrupted {
152 continue;
153 }
154 return Err(e);
155 }
156 if ready == 0 {
157 continue;
158 }
159 let mut b = 0u8;
161 let n = unsafe { libc::read(self.fd, (&mut b as *mut u8).cast(), 1) };
163 match n {
164 1 => return Ok(Some(b)),
165 0 => return Ok(None),
166 _ => {
167 let e = io::Error::last_os_error();
168 if matches!(
169 e.kind(),
170 io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
171 ) {
172 continue;
173 }
174 return Err(e);
175 }
176 }
177 }
178 }
179}
180
181#[cfg(unix)]
182impl io::Read for PromptInput<'_> {
183 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
184 let Some(first) = buf.first_mut() else {
185 return Ok(0);
186 };
187 Ok(match self.read_byte()? {
188 Some(b) => {
189 *first = b;
190 1
191 }
192 None => 0,
193 })
194 }
195}
196
197#[cfg(unix)]
198impl BufRead for PromptInput<'_> {
199 fn fill_buf(&mut self) -> io::Result<&[u8]> {
203 if !self.has_byte {
204 match self.read_byte() {
205 Ok(Some(b)) => {
206 self.byte[0] = b;
207 self.has_byte = true;
208 }
209 Ok(None) => {}
210 Err(e) if e.kind() == io::ErrorKind::Interrupted => {
211 return Err(io::Error::other(e));
212 }
213 Err(e) => return Err(e),
214 }
215 }
216 Ok(&self.byte[..usize::from(self.has_byte)])
217 }
218
219 fn consume(&mut self, amt: usize) {
220 if amt > 0 {
221 self.has_byte = false;
222 }
223 }
224
225 fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
234 let mut line = Vec::new();
235 while let Some(b) = self.read_byte()? {
236 line.push(b);
237 if b == b'\n' {
238 break;
239 }
240 }
241 let text = String::from_utf8(line).map_err(|_| {
242 io::Error::new(
243 io::ErrorKind::InvalidData,
244 "stream did not contain valid UTF-8",
245 )
246 })?;
247 buf.push_str(&text);
248 Ok(text.len())
249 }
250}
251
252#[cfg(test)]
253mod ask_tests {
254 use super::*;
255 use crate::cli::progress::testing::{REGISTRY_LOCK, Screen, drawing_spinner};
256 use std::io::Write;
257 use std::sync::Arc;
258 use std::sync::atomic::AtomicUsize;
259 use std::time::Duration;
260
261 struct SlowAnswer<'a> {
264 screen: &'a Screen,
265 draws_at_answer: Option<usize>,
266 }
267
268 impl io::Read for SlowAnswer<'_> {
269 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
270 if self.draws_at_answer.is_some() {
271 return Ok(0);
272 }
273 std::thread::sleep(Duration::from_millis(40));
274 self.draws_at_answer = Some(self.screen.ops().len());
275 buf[..2].copy_from_slice(b"y\n");
276 Ok(2)
277 }
278 }
279
280 struct Question {
283 screen: Screen,
284 draws_at_question: Arc<AtomicUsize>,
285 }
286
287 impl Write for Question {
288 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
289 self.draws_at_question
290 .store(self.screen.ops().len(), Ordering::SeqCst);
291 Ok(buf.len())
292 }
293 fn flush(&mut self) -> io::Result<()> {
294 Ok(())
295 }
296 }
297
298 #[test]
299 fn test_ask_keeps_a_ticking_spinner_off_the_question() {
300 let _lock = REGISTRY_LOCK.lock().unwrap_or_else(|e| e.into_inner());
303 let screen = Screen::default();
304 let sp = drawing_spinner("Searching...", &Output::capture().0, &screen);
305 std::thread::sleep(Duration::from_millis(30));
306 let mut answer = SlowAnswer {
307 screen: &screen,
308 draws_at_answer: None,
309 };
310 let mut reader = io::BufReader::new(&mut answer);
311 let draws_at_question = Arc::new(AtomicUsize::new(0));
312 let out = Output::from_writers(
313 Vec::new(),
314 Question {
315 screen: screen.clone(),
316 draws_at_question: draws_at_question.clone(),
317 },
318 );
319 let got = ask(&mut reader, &out, "Save? [y/N]: ").unwrap();
320 drop(reader);
321 assert_eq!(got, "y\n");
322 assert_eq!(
323 answer.draws_at_answer,
324 Some(draws_at_question.load(Ordering::SeqCst)),
325 "the spinner drew between the question and the answer"
326 );
327 sp.finish_and_clear();
328 }
329
330 #[test]
331 fn test_a_prompt_never_reaches_the_data_channel() {
332 let (out, cap) = Output::capture();
335 let got = ask(&mut io::Cursor::new(b"y\n".to_vec()), &out, "Save? [y/N]: ").unwrap();
336 assert_eq!(got, "y\n");
337 assert_eq!(cap.out(), "");
338 assert_eq!(cap.err(), "Save? [y/N]: ");
339 }
340
341 #[test]
342 fn test_ask_returns_the_write_error() {
343 let out = Output::from_writers(Vec::new(), crate::cli::output::tests::ClosedPipe);
344 let e = ask(&mut io::Cursor::new(b"y\n".to_vec()), &out, "q? ").unwrap_err();
345 assert_eq!(e.kind(), io::ErrorKind::BrokenPipe);
346 }
347
348 #[test]
349 fn test_pick_returns_the_index_and_rejects_bad_answers() {
350 let pick_with = |answer: &str| {
351 let (out, cap) = Output::capture();
352 let got = pick(
353 &mut io::Cursor::new(answer.as_bytes().to_vec()),
354 &out,
355 "1 a\n2 b\n",
356 "token",
357 2,
358 );
359 assert_eq!(cap.err(), "1 a\n2 b\n\nSelect token (1-2): ");
360 assert_eq!(cap.out(), "", "the list is not data");
361 got.map_err(|e| e.to_string())
362 };
363 assert_eq!(pick_with("2\n"), Ok(1));
364 assert!(pick_with("x\n").unwrap_err().contains("Invalid selection"));
365 assert!(
366 pick_with("3\n")
367 .unwrap_err()
368 .contains("Selection must be between 1 and 2")
369 );
370 assert!(
371 pick_with("0\n")
372 .unwrap_err()
373 .contains("Selection must be between 1 and 2")
374 );
375 }
376}
377
378#[cfg(all(test, unix))]
379mod tests {
380 use super::*;
381 use std::io::Read;
382 use std::os::fd::RawFd;
383 use std::time::{Duration, Instant};
384
385 struct Pipe(RawFd, RawFd);
387
388 impl Pipe {
389 fn new() -> Self {
390 let mut fds = [0; 2];
391 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
393 Pipe(fds[0], fds[1])
394 }
395
396 fn write(&self, bytes: &[u8]) {
397 let n = unsafe { libc::write(self.1, bytes.as_ptr().cast(), bytes.len()) };
399 assert_eq!(n, bytes.len() as isize);
400 }
401
402 fn close_write(&mut self) {
403 unsafe { libc::close(self.1) };
405 self.1 = -1;
406 }
407
408 fn rest(&mut self) -> Vec<u8> {
410 self.close_write();
411 let mut out = Vec::new();
412 let mut b = [0u8; 64];
413 loop {
414 let n = unsafe { libc::read(self.0, b.as_mut_ptr().cast(), b.len()) };
416 if n <= 0 {
417 return out;
418 }
419 out.extend_from_slice(&b[..n as usize]);
420 }
421 }
422 }
423
424 impl Drop for Pipe {
425 fn drop(&mut self) {
426 for fd in [self.0, self.1] {
427 if fd >= 0 {
428 unsafe { libc::close(fd) };
430 }
431 }
432 }
433 }
434
435 #[test]
436 fn test_reads_one_line_and_leaves_the_rest() {
437 let flag = AtomicBool::new(false);
440 let mut pipe = Pipe::new();
441 pipe.write(b"yes\nnext\n");
442 let mut input = PromptInput::new(pipe.0, &flag);
443 let mut line = String::from("old:");
444 assert_eq!(input.read_line(&mut line).unwrap(), 4);
445 assert_eq!(line, "old:yes\n", "appends, as read_line does");
446 assert_eq!(pipe.rest(), b"next\n");
447 }
448
449 #[test]
450 fn test_end_of_input_returns_the_partial_line_then_zero() {
451 let flag = AtomicBool::new(false);
453 let mut pipe = Pipe::new();
454 pipe.write(b"abc");
455 pipe.close_write();
456 let mut input = PromptInput::new(pipe.0, &flag);
457 let mut line = String::new();
458 assert_eq!(input.read_line(&mut line).unwrap(), 3);
459 assert_eq!(line, "abc");
460 assert_eq!(input.read_line(&mut line).unwrap(), 0);
461 }
462
463 #[test]
464 fn test_invalid_utf8_is_an_error_and_leaves_the_buffer() {
465 let flag = AtomicBool::new(false);
466 let pipe = Pipe::new();
467 pipe.write(b"\xff\n");
468 let mut input = PromptInput::new(pipe.0, &flag);
469 let mut line = String::from("kept");
470 let e = input.read_line(&mut line).unwrap_err();
471 assert_eq!(e.kind(), io::ErrorKind::InvalidData);
472 assert_eq!(line, "kept");
473 }
474
475 #[test]
476 fn test_cancel_stops_a_waiting_read_and_takes_no_later_input() {
477 let flag = AtomicBool::new(false);
480 let mut pipe = Pipe::new();
481 let started = Instant::now();
482 let e = std::thread::scope(|s| {
483 s.spawn(|| {
484 std::thread::sleep(Duration::from_millis(150));
485 flag.store(true, Ordering::SeqCst);
486 });
487 let mut line = String::new();
488 PromptInput::new(pipe.0, &flag)
489 .read_line(&mut line)
490 .unwrap_err()
491 });
492 assert_eq!(e.kind(), io::ErrorKind::Interrupted);
493 assert!(started.elapsed() < Duration::from_secs(1), "stopped late");
494 pipe.write(b"k");
495 std::thread::sleep(Duration::from_millis(2 * POLL_INTERVAL_MS as u64));
496 assert_eq!(pipe.rest(), b"k", "the key after the cancel is still there");
497 }
498
499 #[test]
500 fn test_a_set_flag_stops_the_read_before_it_waits() {
501 let flag = AtomicBool::new(true);
502 let mut pipe = Pipe::new();
503 pipe.write(b"y\n");
504 let mut line = String::new();
505 let e = PromptInput::new(pipe.0, &flag)
506 .read_line(&mut line)
507 .unwrap_err();
508 assert_eq!(e.kind(), io::ErrorKind::Interrupted);
509 assert_eq!(pipe.rest(), b"y\n");
510 }
511
512 #[test]
513 fn test_read_and_fill_buf_take_one_byte_at_a_time() {
514 let flag = AtomicBool::new(false);
515 let mut pipe = Pipe::new();
516 pipe.write(b"ab");
517 let mut input = PromptInput::new(pipe.0, &flag);
518 assert_eq!(input.read(&mut []).unwrap(), 0);
519 assert_eq!(input.fill_buf().unwrap(), b"a");
520 assert_eq!(input.fill_buf().unwrap(), b"a", "not consumed yet");
521 let mut buf = [0u8; 4];
522 assert_eq!(input.read(&mut buf).unwrap(), 1, "the buffered byte");
523 assert_eq!(buf[0], b'a');
524 input.consume(0);
525 assert_eq!(input.fill_buf().unwrap(), b"b");
526 input.consume(1);
527 assert_eq!(pipe.rest(), b"");
528 assert_eq!(input.fill_buf().unwrap(), b"", "end of input");
529 assert_eq!(input.read(&mut buf).unwrap(), 0);
530 }
531
532 #[test]
533 fn test_fill_buf_cancel_is_not_interrupted() {
534 let flag = AtomicBool::new(true);
537 let pipe = Pipe::new();
538 let mut input = PromptInput::new(pipe.0, &flag);
539 let e = input.fill_buf().unwrap_err();
540 assert_eq!(e.kind(), io::ErrorKind::Other);
541 let mut v = Vec::new();
542 assert!(input.read_until(b'\n', &mut v).is_err(), "no endless retry");
543 }
544
545 #[test]
546 fn test_a_read_error_is_returned() {
547 let flag = AtomicBool::new(false);
549 let fd = unsafe { libc::open(c"/".as_ptr(), libc::O_RDONLY) };
551 assert!(fd >= 0);
552 let mut line = String::new();
553 let r = PromptInput::new(fd, &flag).read_line(&mut line);
554 unsafe { libc::close(fd) };
556 assert!(r.is_err());
557 assert_ne!(r.unwrap_err().kind(), io::ErrorKind::Interrupted);
558 }
559
560 #[test]
561 fn test_stdin_reads_from_the_process_cancel_flag() {
562 let input = stdin();
563 assert_eq!(input.fd, libc::STDIN_FILENO);
564 assert!(std::ptr::eq(input.cancel, &CANCEL));
565 }
566}