quest/
lib.rs

1#![deny(missing_docs)]
2
3//! Command-line input utilities.
4
5#[macro_use] extern crate cfg_if;
6extern crate rpassword;
7extern crate tempfile;
8#[cfg(unix)] extern crate termios;
9#[cfg(windows)] extern crate winapi;
10
11use interactive::Interactive;
12use std::ffi::OsStr;
13use std::fs::File;
14use std::io::{Read, Write};
15use std::process::Command;
16use std::{env, io};
17use tempfile::TempDir;
18
19mod interactive;
20mod util;
21
22/// The ASCII escape character.
23const ESC: u8 = 0x1B;
24
25/// Print a question, in bold, without creating a new line.
26pub fn ask(q: &str) {
27    print!("\u{1B}[1m{}\u{1B}[0m", q);
28    io::stdout().flush().unwrap();
29}
30
31/// Print a message of success, with a newline.
32pub fn success(s: &str) {
33    println!("\u{1B}[1;92m{}\u{1B}[0m", s);
34}
35
36/// Print an error message, with a newline.
37pub fn error(s: &str) {
38    println!("\u{1B}[1;91m{}\u{1B}[0m", s);
39}
40
41/// Ask for a password (the password will not be visible).
42pub fn password() -> io::Result<String> {
43    rpassword::read_password()
44}
45
46/// Ask for a line of text.
47pub fn text() -> io::Result<String> {
48    // Read up to the first newline or EOF.
49
50    let mut out = String::new();
51    io::stdin().read_line(&mut out)?;
52
53    // Only capture up to the first newline.
54
55    if let Some(mut newline) = out.find('\n') {
56        if newline > 0 && out.as_bytes()[newline - 1] == b'\r' { newline -= 1; }
57        out.truncate(newline);
58    }
59
60    Ok(out)
61}
62
63/// Ask a yes-or-no question.
64///
65/// `None` indicates an invalid response.
66pub fn yesno(default: bool) -> io::Result<Option<bool>> {
67    let s = text()?.to_lowercase();
68    Ok(if s.is_empty() {
69        Some(default)
70    } else if "yes".starts_with(&s) {
71        Some(true)
72    } else if "no".starts_with(&s) {
73        Some(false)
74    } else {
75        None
76    })
77}
78
79/// Ask the user to enter some text through their editor.
80///
81/// We'll check the `VISUAL` environment variable, then `EDITOR`, and then
82/// finally default to `vi`. The message will be the initial contents of the
83/// file, and the result will be the final contents of the file, after the user
84/// has quit their editor.
85///
86/// On Windows, the editor defaults to `notepad`.
87pub fn editor(name: &str, message: &[u8]) -> io::Result<String> {
88    // Create a temporary file with the message.
89
90    let dir = TempDir::new()?;
91    let path = dir.path().join(name);
92    File::create(&path)?.write_all(message)?;
93
94    // Get the editor command from the environment.
95
96    let editor = env::var_os("VISUAL").or_else(|| env::var_os("EDITOR"));
97
98    let editor = match editor {
99        Some(ref editor) => editor,
100        None => OsStr::new(
101            #[cfg(windows)] "notepad",
102            #[cfg(unix)] "vi"),
103    };
104
105    // Call the editor.
106
107    Command::new(editor).arg(&path).spawn()?.wait()?;
108
109    // Read the file back.
110
111    let mut out = String::new();
112    File::open(&path)?.read_to_string(&mut out)?;
113    Ok(out)
114}
115
116/// The text to use when indicating whether an item is selected.
117#[derive(Clone, Copy, Debug)]
118pub struct Boxes<'a> {
119    /// The text to use when an item is selected.
120    pub on: &'a str,
121    /// The text to use when an item is not selected.
122    pub off: &'a str,
123}
124
125impl<'a> Default for Boxes<'a> {
126    fn default() -> Self {
127        Self {
128            on: ">",
129            off: " ",
130        }
131    }
132}
133
134/// Ask the user to choose exactly one option from a list.
135pub fn choose<S: AsRef<str>>(boxes: Boxes, items: &[S]) -> io::Result<usize> {
136    assert!(items.len() > 0);
137
138    let stdin = io::stdin();
139    let mut stdin = stdin.bytes();
140    let mut selected = 0;
141
142    let interactive = Interactive::start()?;
143
144    loop {
145        for (i, item) in items.iter().enumerate() {
146            println!(
147                "{} {}",
148                if i == selected { boxes.on } else { boxes.off },
149                item.as_ref(),
150            );
151        }
152
153        match util::or2ro(stdin.next())? {
154            Some(ESC) => match util::or2ro(stdin.next())? {
155                Some(b'[') => match util::or2ro(stdin.next())? {
156                    Some(b'A') => {
157                        selected = selected.saturating_sub(1);
158                    }
159                    Some(b'B') => {
160                        selected = selected.saturating_add(1).min(items.len() - 1);
161                    }
162                    None => break,
163                    Some(_) => (),
164                },
165                None => break,
166                Some(_) => (),
167            },
168            Some(b'\r') | Some(b'\n') => break,
169            Some(b'k') => {
170                selected = selected.saturating_sub(1);
171            }
172            Some(b'j') => {
173                selected = selected.saturating_add(1).min(items.len() - 1);
174            }
175            None => break,
176            Some(_) => (),
177        }
178
179        interactive.up(items.len());
180        interactive.clear_right();
181    }
182
183    Ok(selected)
184}