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
//! Prompts for the `manage` CLI.
//!
//! Every prompt returns `Ok(None)` when stdin is not a terminal, so
//! scripts and tests still get the usual "argument required" error
//! instead of blocking. In a real shell the prompt runs and returns
//! `Some(value)`. This keeps the `manage::run` signature unchanged
//! and makes the interactive path opt in by itself.
use std::io::{self, BufRead, IsTerminal as _, Write as _};
/// One line of input, from wherever the caller reads it.
///
/// The wizard and the menu prompt between commands, and a command
/// may prompt again through [`ask`]. Holding a `StdinLock` across
/// that boundary deadlocks, so the loops take a `LineSource` and
/// the stdin version locks for one read at a time.
///
/// Any `BufRead` is a `LineSource`, so tests can drive the loops
/// from a `Cursor` with no terminal.
pub trait LineSource {
/// Read one line, including its newline. `Ok(0)` means EOF.
///
/// # Errors
/// Whatever the underlying reader returns.
fn read_line_from(&mut self, buf: &mut String) -> io::Result<usize>;
}
impl<R: BufRead> LineSource for R {
fn read_line_from(&mut self, buf: &mut String) -> io::Result<usize> {
self.read_line(buf)
}
}
/// Reads stdin, locking it for one read only. The lock is free
/// again before the caller does anything else.
pub struct SharedStdin;
impl LineSource for SharedStdin {
fn read_line_from(&mut self, buf: &mut String) -> io::Result<usize> {
io::stdin().read_line(buf)
}
}
/// Read one trimmed, non-empty line from stdin. `Ok(None)` means
/// stdin is not a terminal, the user typed nothing, or we hit EOF;
/// the caller decides if that is an error.
///
/// The prompt goes to stderr, so it does not mix into stdout when
/// someone pipes the output elsewhere.
///
/// # Errors
/// [`io::Error`] if the terminal read or write fails.
pub fn ask(prompt: &str) -> io::Result<Option<String>> {
if !io::stdin().is_terminal() {
return Ok(None);
}
let mut stderr = io::stderr().lock();
write!(stderr, "{prompt}")?;
stderr.flush()?;
drop(stderr);
let mut buf = String::new();
let n = io::stdin().read_line(&mut buf)?;
if n == 0 {
// Ctrl+D before any input.
return Ok(None);
}
let trimmed = buf.trim().to_owned();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed))
}
}
/// Like [`ask`], but the typed characters are not shown on screen.
/// `rpassword` turns echo off for the read.
///
/// `Ok(None)` for a non-terminal stdin or empty input, the same as
/// [`ask`].
///
/// # Errors
/// [`io::Error`] if switching terminal mode or reading fails.
pub fn ask_password(prompt: &str) -> io::Result<Option<String>> {
if !io::stdin().is_terminal() {
return Ok(None);
}
let pw = rpassword::prompt_password(prompt)?;
let trimmed = pw.trim().to_owned();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed))
}
}