uquery/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(clippy::pedantic)]
3#![warn(missing_docs)]
4
5use core::str::FromStr;
6use std::fmt::Display;
7use std::io;
8use std::io::Write;
9
10use yansi::Paint;
11
12/// A type representing the error for a query.
13pub type QueryResult<T> = io::Result<T>;
14
15/// Asks the users input
16fn query_input() -> QueryResult<String> {
17    let stdin = io::stdin();
18
19    // Read in the input
20    let mut buffer = String::new();
21    stdin.read_line(&mut buffer)?;
22    Ok(buffer)
23}
24
25/// Writes a prompt out in the form of:
26///
27/// ```txt
28/// {prefix} (Defaulting to: {def}) {text}
29/// ```
30fn write_prompt(
31    text: &str,
32    def: Option<&impl Display>,
33    prefix: &impl Display,
34) -> QueryResult<String> {
35    let mut stdout = io::stdout();
36
37    // Write out the prompt
38    write!(stdout, "{} ", prefix)?;
39    if let Some(ref def) = def {
40        write!(
41            stdout,
42            "{} {}{} ",
43            Paint::green("(Defaulting to:"),
44            Paint::green(def),
45            Paint::green(")"),
46        )?;
47    }
48    write!(stdout, "{} ", text)?;
49    stdout.flush()?;
50
51    query_input()
52}
53
54/// Queries the user for string input.
55///
56/// # Errors
57///
58/// Fails on io errors with the stdin and stdout.
59pub fn string(string: &str) -> io::Result<String> {
60    string_with_default(string, Option::<String>::None)
61}
62
63/// Queries the user for string input with a default.
64///
65///  The user can unset the value by typing a space,
66/// otherwise, if no string is specified and a deafult is given the
67/// default will be used.
68///
69/// # Errors
70///
71/// Fails on io errors with the stdin and stdout.
72pub fn string_with_default(
73    text: &str,
74    def: Option<impl Into<String> + Display>,
75) -> QueryResult<String> {
76
77    // Write out the prompt
78    let buffer = write_prompt(text, def.as_ref(), &Paint::yellow("::").bold())?;
79
80    match def {
81        Some(def)
82            if buffer.trim().is_empty() &&
83                (buffer.len() < 2 || &buffer[..buffer.len() - 1] != " ") =>
84        {
85            Ok(def.into())
86        }
87        _ => Ok(buffer.trim().to_owned()),
88    }
89}
90
91/// Queries the user for input of any time that implements `FromStr`
92/// and `Display`
93///
94/// # Errors
95///
96/// Fails on io errors with the stdin and stdout.
97pub fn parsable_with_default<T: FromStr + Display>(
98    text: &str,
99    def: T,
100) -> QueryResult<T> {
101    let buffer = write_prompt(text, Some(&def), &Paint::blue("::").bold())?;
102
103    if buffer.trim().is_empty() {
104        Ok(def)
105    } else {
106        match buffer.trim().parse() {
107            Ok(o) => Ok(o),
108            Err(_e) => {
109                println!(
110                    "{}",
111                    Paint::red("Failed to be parse input! Retrying...")
112                );
113                parsable_with_default(text, def)
114            }
115        }
116    }
117}
118
119/// Queries the user for input of any time that implements `FromStr`.
120///
121/// # Errors
122///
123/// Fails on io errors with the stdin and stdout.
124pub fn parsable<T>(text: &str) -> QueryResult<T>
125where
126    T: FromStr,
127    <T as FromStr>::Err: ToString,
128{
129    let buffer =
130        write_prompt(text, Option::<&u8>::None, &Paint::blue("::").bold())?;
131
132    match buffer.trim().parse() {
133        Ok(o) => Ok(o),
134        Err(e) => {
135            println!();
136            error(
137                "Failed to be parse input! Retrying...",
138                Some(&e.to_string()),
139            );
140            println!();
141            parsable(text)
142        }
143    }
144}
145
146/// Queries the user for boolean input, with an optional default.
147///
148/// # Errors
149///
150/// Fails with io errors with the stdin or stdout.
151pub fn boolean(text: &str, def: Option<bool>) -> QueryResult<bool> {
152    let mut stdout = io::stdout();
153    write!(stdout, "{} ", Paint::magenta("::").bold())?;
154    write!(stdout, "{} ", text)?;
155    write!(
156        stdout,
157        "{} ",
158        Paint::green(match def {
159            Some(true) => "[Y/n]",
160            Some(false) => "[y/N]",
161            None => "[y/n]",
162        }),
163    )?;
164    stdout.flush()?;
165
166    let buffer = query_input()?;
167
168    match (
169        def,
170        buffer
171            .trim()
172            .chars()
173            .next()
174            .and_then(|x| x.to_lowercase().next())
175    ) {
176        (_, Some('y')) => Ok(true),
177        (_, Some('n')) => Ok(false),
178        (Some(def), None) => Ok(def),
179        _ => {
180            println!();
181            error(
182                "Failed to be parse input! Retrying...",
183                Some("Use either `y` or `n` as your response"),
184            );
185            println!();
186            boolean(text, def)
187        }
188    }
189}
190
191/// Displays an error to the user.
192pub fn error(string: &str, suggestion: Option<&str>) {
193    match suggestion {
194        Some(suggestion) => {
195            eprintln!("{} {}", Paint::red("Error:").bold(), string);
196            eprintln!("  {} {}", Paint::cyan("Suggestion:"), suggestion);
197        }
198        None => {
199            eprintln!("{} {}", Paint::red("Error:").bold(), string);
200        }
201    }
202}
203
204/// Displays a warning to the user.
205pub fn warning(string: &str, suggestion: Option<&str>) {
206    match suggestion {
207        Some(suggestion) => {
208            eprintln!("{} {}", Paint::yellow("Warning:").bold(), string);
209            eprintln!("  {} {}", Paint::cyan("Suggestion:"), suggestion);
210        }
211        None => {
212            eprintln!("{} {}", Paint::yellow("Warning:").bold(), string);
213        }
214    }
215}