#![cfg_attr(feature = "nightly", feature(specialization))]
use rustyline::completion::{Completer, FilenameCompleter};
use rustyline::{error::ReadlineError, Editor};
use std::env;
use std::path::PathBuf;
use std::str::FromStr;
#[cfg(feature = "nightly")]
use std::fmt::Display;
pub fn prompt<T, S>(msg: S) -> T
where
T: Promptable,
S: AsRef<str>,
{
T::prompt(msg)
}
pub fn prompt_opt<T, S>(msg: S) -> Option<T>
where
T: Promptable,
S: AsRef<str>,
{
T::prompt_opt(msg)
}
pub fn prompt_default<T, S>(msg: S, default: T) -> T
where
T: Promptable,
S: AsRef<str>,
{
T::prompt_default(msg, default)
}
pub trait Promptable: Sized {
fn prompt<S: AsRef<str>>(msg: S) -> Self;
fn prompt_opt<S: AsRef<str>>(msg: S) -> Option<Self>;
fn prompt_default<S: AsRef<str>>(msg: S, default: Self) -> Self;
}
#[cfg(feature = "nightly")]
default impl<T> Promptable for T
where
T: FromStr + Display,
<T as FromStr>::Err: ::std::error::Error,
{
fn prompt<S: AsRef<str>>(msg: S) -> Self {
prompt_parse(msg)
}
fn prompt_opt<S: AsRef<str>>(msg: S) -> Option<Self> {
prompt_parse_opt(msg)
}
fn prompt_default<S: AsRef<str>>(msg: S, default: Self) -> Self {
let msg = format!("{} (default={})", msg.as_ref(), default);
prompt_parse_opt(msg).unwrap_or(default)
}
}
impl Promptable for String {
fn prompt<S: AsRef<str>>(msg: S) -> Self {
Prompter::new().prompt_nonempty(msg)
}
fn prompt_opt<S: AsRef<str>>(msg: S) -> Option<Self> {
Prompter::new().prompt_opt(msg)
}
fn prompt_default<S: AsRef<str>>(msg: S, default: Self) -> Self {
let msg = format!("{} (default={})", msg.as_ref(), default);
Prompter::new().prompt_opt(msg).unwrap_or(default)
}
}
impl Promptable for PathBuf {
fn prompt<S: AsRef<str>>(msg: S) -> Self {
prompt_path(msg)
}
fn prompt_opt<S: AsRef<str>>(msg: S) -> Option<Self> {
prompt_path_opt(msg)
}
fn prompt_default<S: AsRef<str>>(msg: S, default: Self) -> Self {
let msg = format!("{} (default={})", msg.as_ref(), default.display());
prompt_path_opt(msg).unwrap_or(default)
}
}
impl Promptable for bool {
fn prompt<S: AsRef<str>>(msg: S) -> Self {
prompt_bool(msg)
}
fn prompt_opt<S: AsRef<str>>(msg: S) -> Option<Self> {
prompt_bool_opt(msg)
}
fn prompt_default<S: AsRef<str>>(msg: S, default: Self) -> Self {
let msg = if default {
format!("{} (Y/n)", msg.as_ref())
} else {
format!("{} (y/N)", msg.as_ref())
};
prompt_bool_opt(msg).unwrap_or(default)
}
}
macro_rules! impl_promptable_from_str {
($t:ty) => {
impl Promptable for $t {
fn prompt<S: AsRef<str>>(msg: S) -> Self {
prompt_parse(msg)
}
fn prompt_opt<S: AsRef<str>>(msg: S) -> Option<Self> {
prompt_parse_opt(msg)
}
fn prompt_default<S: AsRef<str>>(msg: S, default: Self) -> Self {
let msg = format!("{} (default={})", msg.as_ref(), default);
prompt_parse_opt(msg).unwrap_or(default)
}
}
};
}
impl_promptable_from_str!(char);
impl_promptable_from_str!(u8);
impl_promptable_from_str!(u16);
impl_promptable_from_str!(u32);
impl_promptable_from_str!(u64);
impl_promptable_from_str!(u128);
impl_promptable_from_str!(usize);
impl_promptable_from_str!(i8);
impl_promptable_from_str!(i16);
impl_promptable_from_str!(i32);
impl_promptable_from_str!(i64);
impl_promptable_from_str!(i128);
impl_promptable_from_str!(isize);
impl_promptable_from_str!(f32);
impl_promptable_from_str!(f64);
impl_promptable_from_str!(::std::net::IpAddr);
impl_promptable_from_str!(::std::net::Ipv4Addr);
impl_promptable_from_str!(::std::net::Ipv6Addr);
impl_promptable_from_str!(::std::net::SocketAddrV4);
impl_promptable_from_str!(::std::net::SocketAddrV6);
impl_promptable_from_str!(::std::num::NonZeroI128);
impl_promptable_from_str!(::std::num::NonZeroI64);
impl_promptable_from_str!(::std::num::NonZeroI32);
impl_promptable_from_str!(::std::num::NonZeroI16);
impl_promptable_from_str!(::std::num::NonZeroI8);
impl_promptable_from_str!(::std::num::NonZeroIsize);
impl_promptable_from_str!(::std::num::NonZeroU128);
impl_promptable_from_str!(::std::num::NonZeroU64);
impl_promptable_from_str!(::std::num::NonZeroU32);
impl_promptable_from_str!(::std::num::NonZeroU16);
impl_promptable_from_str!(::std::num::NonZeroU8);
impl_promptable_from_str!(::std::num::NonZeroUsize);
#[cfg(feature = "url")]
impl_promptable_from_str!(url::Url);
pub struct Prompter<C: Completer> {
editor: Editor<C>,
err_handler: Box<dyn Fn(ReadlineError)>, }
impl Prompter<()> {
pub fn new() -> Prompter<()> {
Prompter::default()
}
}
impl Default for Prompter<()> {
fn default() -> Self {
Prompter {
editor: Editor::new(),
err_handler: Box::new(default_err_handler),
}
}
}
fn default_err_handler(err: ReadlineError) {
match err {
ReadlineError::Interrupted => (),
_ => println!("Readline error: {}", err),
}
::std::process::exit(1);
}
impl<C> Prompter<C>
where
C: Completer,
{
pub fn with_completer(completer: C) -> Prompter<C> {
let mut editor = Editor::new();
editor.set_completer(Some(completer));
Prompter {
editor,
err_handler: Box::new(default_err_handler),
}
}
pub fn on_error<F: Fn(ReadlineError) + 'static>(mut self, handler: F) {
self.err_handler = Box::new(handler);
}
pub fn prompt_once<S: AsRef<str>>(&mut self, msg: S) -> String {
match self.editor.readline(&format!("{}: ", msg.as_ref())) {
Ok(line) => line.trim().to_owned(),
Err(err) => {
(self.err_handler)(err);
unreachable!("Prompter's on_error handler should never return")
}
}
}
pub fn prompt_opt<S: AsRef<str>>(&mut self, msg: S) -> Option<String> {
let val = self.prompt_once(msg);
if val.is_empty() {
return None;
}
Some(val)
}
pub fn prompt_nonempty<S: AsRef<str>>(&mut self, msg: S) -> String {
let mut val;
val = self.prompt_opt(&msg);
while val.is_none() {
eprintln!("Value is required.");
val = self.prompt_opt(&msg);
}
val.unwrap()
}
pub fn prompt_then<S, F, U>(&mut self, msg: S, handler: F) -> U
where
S: AsRef<str>,
F: Fn(String) -> ::std::result::Result<U, String>,
{
let mut val = handler(self.prompt_once(&msg));
while let Err(e) = val {
eprintln!("{}", e);
val = handler(self.prompt_once(&msg));
}
val.unwrap()
}
}
fn prompt_bool<S: AsRef<str>>(msg: S) -> bool {
Prompter::new().prompt_then(msg, |s| match &*s.to_lowercase() {
"true" | "yes" | "y" => Ok(true),
"false" | "no" | "n" => Ok(false),
s => Err(format!("Could not parse {} as bool.", s)),
})
}
fn prompt_bool_opt<S: AsRef<str>>(msg: S) -> Option<bool> {
Prompter::new().prompt_then(msg, |s| match &*s.to_lowercase().trim() {
"" => Ok(None),
"true" | "yes" | "y" => Ok(Some(true)),
"false" | "no" | "n" => Ok(Some(false)),
s => Err(format!("Could not parse {} as bool.", s)),
})
}
fn prompt_path<S: AsRef<str>>(msg: S) -> PathBuf {
let completer = FilenameCompleter::new();
let s = Prompter::with_completer(completer).prompt_nonempty(msg);
PathBuf::from(path_expand(s))
}
fn prompt_path_opt<S: AsRef<str>>(msg: S) -> Option<PathBuf> {
let completer = FilenameCompleter::new();
Prompter::with_completer(completer)
.prompt_opt(msg)
.map(path_expand)
.map(PathBuf::from)
}
fn prompt_parse<T, S>(msg: S) -> T
where
T: FromStr,
<T as FromStr>::Err: ::std::error::Error,
S: AsRef<str>,
{
Prompter::new().prompt_then(msg, |s| T::from_str(s.as_ref()).map_err(|e| e.to_string()))
}
fn prompt_parse_opt<T, S>(msg: S) -> Option<T>
where
T: FromStr,
<T as FromStr>::Err: ::std::error::Error,
S: AsRef<str>,
{
Prompter::new().prompt_then(msg, |s| match s.trim() {
"" => Ok(None),
_ => match T::from_str(s.as_ref()) {
Ok(n) => Ok(Some(n)),
Err(e) => Err(e.to_string()),
},
})
}
fn path_expand(s: String) -> String {
if s.starts_with('~') {
if let Ok(home) = env::var("HOME") {
return s.replacen("~", &home, 1);
}
}
s
}