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
12pub type QueryResult<T> = io::Result<T>;
14
15fn query_input() -> QueryResult<String> {
17 let stdin = io::stdin();
18
19 let mut buffer = String::new();
21 stdin.read_line(&mut buffer)?;
22 Ok(buffer)
23}
24
25fn 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!(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
54pub fn string(string: &str) -> io::Result<String> {
60 string_with_default(string, Option::<String>::None)
61}
62
63pub fn string_with_default(
73 text: &str,
74 def: Option<impl Into<String> + Display>,
75) -> QueryResult<String> {
76
77 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
91pub 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
119pub 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
146pub 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
191pub 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
204pub 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}