#![feature(specialization)]
extern crate rustyline;
use rustyline::completion::{Completer, FilenameCompleter};
use rustyline::{error::ReadlineError, Editor};
use std::env;
use std::fmt::Display;
use std::path::PathBuf;
use std::str::FromStr;
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;
}
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<T> Promptable for T
where
T: FromStr + Display,
<T as FromStr>::Err: ::std::error::Error,
{
default fn prompt<S: AsRef<str>>(msg: S) -> Self {
prompt_parse(msg)
}
default fn prompt_opt<S: AsRef<str>>(msg: S) -> Option<Self> {
prompt_parse_opt(msg)
}
default 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 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 = match default {
true => format!("{} (Y/n)", msg.as_ref()),
false => format!("{} (y/N)", msg.as_ref()),
};
prompt_bool_opt(msg).unwrap_or(default)
}
}
impl<P: Promptable> Promptable for Option<P> {
fn prompt<S: AsRef<str>>(msg: S) -> Self {
P::prompt_opt(msg)
}
fn prompt_opt<S: AsRef<str>>(_msg: S) -> Option<Self> {
unimplemented!(
"prompt_opt is not implemented for Option types as it would return Option<Option<T>>"
);
}
fn prompt_default<S: AsRef<str>>(msg: S, default: Self) -> Self {
P::prompt_opt(msg).or(default)
}
}
pub struct Prompter<C: Completer> {
editor: Editor<C>,
}
impl Prompter<()> {
pub fn new() -> Prompter<()> {
Prompter {
editor: Editor::new(),
}
}
}
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 }
}
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) => {
match err {
ReadlineError::Interrupted => (),
_ => println!("Readline error: {}", err),
}
::std::process::exit(1);
}
}
}
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
}