#![deny(missing_docs)]
extern crate clap;
extern crate clipboard;
#[macro_use]
extern crate cursive as _cursive;
extern crate dirs;
extern crate glob;
extern crate regex;
#[macro_use]
extern crate serde_json;
mod clap_conv;
pub mod cursive {
pub use _cursive::*;
}
pub use serde_json::value::Value;
pub mod feeders;
pub mod fields;
pub mod form;
pub mod utils;
pub mod validators;
pub mod views;
use clipboard::ClipboardContext;
use clipboard::ClipboardProvider;
use cursive::event::Event;
use cursive::traits::{Boxable, Identifiable};
use cursive::view::Scrollable;
use cursive::views::{Dialog, LayerPosition, OnEventView};
use cursive::Cursive;
use fields::autocomplete::AutocompleteManager;
use form::FormView;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::rc::Rc;
use validators::OneOf;
use views::Autocomplete;
const DEFAULT_THEME: &'static str = "
[colors]
highlight_inactive = \"light black\"
";
const COMMAND_PICKER_ID: &'static str = "fui-command-picker";
struct Action<'action> {
name: &'action str,
help: &'action str,
form: Option<FormView>,
handler: Rc<Fn(Value)>,
}
impl<'action> Action<'action> {
fn cmd_with_desc(&self) -> String {
format!("{}: {}", self.name, self.help)
}
}
fn value2array(value: &Value) -> Vec<String> {
let mut result: Vec<String> = Vec::new();
if let Value::Object(map) = value {
for (key, val) in map {
let key_is_digit = key.parse::<u32>().is_ok();
match val {
Value::Bool(true) => {
result.push(format!("--{}", key));
}
Value::Number(n) => {
if !key_is_digit {
result.push(format!("--{}", key));
}
result.push(format!("{}", n));
}
Value::String(s) => {
if val != "" {
if !key_is_digit {
result.push(format!("--{}", key));
}
result.push(format!("{}", s));
}
}
Value::Array(vals) => {
if !key_is_digit {
result.push(format!("--{}", key));
}
for v in vals {
if v.is_string() {
result.push(v.to_string().trim_matches('"').to_string());
}
}
}
Value::Object(_) => {
result.push(format!("{}", key));
let mut found = value2array(&val);
result.append(&mut found);
}
_ => (),
};
}
}
result
}
trait DumpAsCli {
fn dump_as_cli(&self) -> String;
}
impl DumpAsCli for Value {
fn dump_as_cli(&self) -> String {
return value2array(&self)
.iter()
.map(|a| {
if a.contains(" ") {
format!("\"{}\"", a)
} else {
format!("{}", a)
}
})
.collect::<Vec<String>>()
.join(" ");
}
}
pub struct Fui<'attrs, 'action> {
actions: BTreeMap<String, Action<'action>>,
form_fields_count: BTreeMap<&'action str, u8>,
name: &'attrs str,
version: &'attrs str,
about: &'attrs str,
author: &'attrs str,
theme: &'attrs str,
picked_action: Rc<RefCell<Option<String>>>,
form_data: Rc<RefCell<Option<Value>>>,
skip_single_action: bool,
skip_empty_form: bool,
active_step: Rc<RefCell<u8>>,
}
impl<'attrs, 'action> Fui<'attrs, 'action> {
pub fn new(program_name: &'attrs str) -> Self {
Fui {
actions: BTreeMap::new(),
form_fields_count: BTreeMap::new(),
name: program_name,
version: "",
about: "",
author: "",
theme: &DEFAULT_THEME,
picked_action: Rc::new(RefCell::new(None)),
form_data: Rc::new(RefCell::new(None)),
skip_single_action: false,
skip_empty_form: false,
active_step: Rc::new(RefCell::new(1)),
}
}
pub fn action<F>(
mut self,
name: &'action str,
help: &'action str,
form: FormView,
hdlr: F,
) -> Self
where
F: Fn(Value) + 'static,
{
let action_details = Action {
name: name,
help: help,
form: Some(form),
handler: Rc::new(hdlr),
};
if let Some(item) = self.action_by_name(&name) {
panic!(
"Action name must be unique, but it's already defined ({:?})",
item.cmd_with_desc()
);
}
let fields_count = action_details.form.as_ref().unwrap().get_fields().len();
self.form_fields_count
.insert(action_details.name, fields_count as u8);
self.actions
.insert(action_details.cmd_with_desc(), action_details);
self
}
fn action_by_name(&self, name: &str) -> Option<&Action> {
self.actions.values().find(|a| a.name == name)
}
pub fn run(mut self) {
let args = env::args_os();
let input_data = if args.len() > 1 {
self.input_from_cli(args)
} else {
self.input_from_tui()
};
if let Some((action_name, data)) = input_data {
if let Some(action) = self.action_by_name(&action_name) {
let hdlr = action.handler.clone();
hdlr(data);
}
}
}
fn dump_as_cli(&self) -> Vec<String> {
let mut arg_vec = vec![self.name.to_owned()];
if let Some(a) = self.picked_action.borrow().as_ref() {
if *a != arg_vec[0] {
arg_vec.push(a.to_owned())
}
}
if let Some(f) = self.form_data.borrow().as_ref() {
arg_vec.append(&mut value2array(&f));
}
arg_vec
}
#[cfg(test)]
fn actions(&self) -> Vec<&Action> {
self.actions.values().collect()
}
#[cfg(test)]
fn set_action(&mut self, name: &str) {
if let Some(a) = self.action_by_name(name) {
*self.picked_action.borrow_mut() = Some(a.name.to_string());
}
}
#[cfg(test)]
fn set_form_data(&mut self, form_data: Value) {
*self.form_data.borrow_mut() = Some(form_data);
}
pub fn get_cli_input(mut self) -> Vec<String> {
self.input_from_tui();
self.dump_as_cli()
}
pub fn build_cli_app(&self) -> clap::App {
let mut sub_cmds: Vec<clap::App> = Vec::new();
for action in self.actions.values() {
let args = action.form.as_ref().unwrap().fields2clap_args();
let sub_cmd = clap::SubCommand::with_name(action.name.as_ref())
.about(action.help.as_ref())
.args(args.as_slice());
sub_cmds.push(sub_cmd);
}
clap::App::new(self.name.as_ref())
.version(self.version.as_ref())
.about(self.about.as_ref())
.author(self.author.as_ref())
.subcommands(sub_cmds)
}
fn input_from_cli<I, T>(&self, user_args: I) -> Option<(String, Value)>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let user_args = user_args
.into_iter()
.map(|x| x.into())
.collect::<Vec<OsString>>();
let app = self.build_cli_app();
let matches = app.get_matches_from(user_args);
let cmd_name = matches.subcommand_name().unwrap();
let cmd_matches = matches.subcommand_matches(cmd_name).unwrap();
let action = self
.actions
.values()
.find(|action| action.name == cmd_name)
.unwrap();
let value = action
.form
.as_ref()
.unwrap()
.clap_arg_matches2value(cmd_matches);
Some((action.cmd_with_desc(), value))
}
fn header(&self) -> String {
let header = if (self.name.len() > 0) & (self.version.len() > 0) {
format!("{} ({})", self.name, self.version)
} else if self.name.len() > 0 {
format!("{}", self.name)
} else {
format!("")
};
return header;
}
fn set_form_events(&self, form: &mut FormView) {
let form_data = Rc::clone(&self.form_data);
let step_submit = Rc::clone(&self.active_step);
let step_cancel = Rc::clone(&self.active_step);
form.set_on_submit(move |c: &mut Cursive, data: Value| {
*form_data.borrow_mut() = Some(data);
*step_submit.borrow_mut() += 1;
c.quit();
});
form.set_on_cancel(move |c: &mut Cursive| {
*step_cancel.borrow_mut() -= 1;
c.quit();
});
}
fn add_form(&self, c: &mut Cursive, form: FormView, form_id: &str) {
let form = form.with_id(form_id).full_width().scrollable();
let prog_name = self.name.to_owned();
let form_id = form_id.to_owned();
let form = OnEventView::new(form).on_event(Event::CtrlChar('k'), move |c| {
let err = c.call_on_id(&form_id, |form: &mut FormView| match form.validate() {
Ok(s) => {
let msg = format!("{} {} {}", prog_name, form_id, s.dump_as_cli());
let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
ctx.set_contents(msg).unwrap();
Ok(())
}
Err(_) => {
let err = format!("Copying to clipboard - FAILED.\nFix form errors first.");
Err(err)
}
});
if let Err(e) = err.unwrap() {
c.add_layer(Dialog::info(e).title("Form invalid!"));
}
});
c.add_layer(form);
}
fn add_forms(&mut self, c: &mut Cursive) {
let action_form_list = self
.actions
.iter_mut()
.map(|(_, a)| (a.name, a.form.take().unwrap()))
.collect::<Vec<(&str, FormView)>>();
for (form_id, mut form) in action_form_list.into_iter() {
self.set_form_events(&mut form);
self.add_form(c, form, form_id);
}
}
fn add_cmd_picker(&mut self, c: &mut Cursive) {
let cmd_submit = Rc::clone(&self.picked_action);
let step_submit = Rc::clone(&self.active_step);
let step_cancel = Rc::clone(&self.active_step);
let actions = self
.actions
.keys()
.map(|x| x.to_owned())
.collect::<Vec<String>>();
let feeder = actions.clone();
let mngr = AutocompleteManager::with_factory_view(Rc::new(move || {
Autocomplete::new(feeder.clone()).shown_count(12)
}));
let form = FormView::new()
.title(&self.header())
.field(
fields::Field::new("action", mngr, "".to_string())
.help("Pick action")
.validator(OneOf(actions)),
)
.on_submit(move |c, data| {
let value = data.get("action").unwrap().clone();
*cmd_submit.borrow_mut() = Some(value.as_str().unwrap().to_string());
*step_submit.borrow_mut() += 1;
c.quit();
})
.on_cancel(move |c| {
*step_cancel.borrow_mut() -= 1;
c.quit();
})
.with_id(COMMAND_PICKER_ID)
.full_screen();
c.add_layer(form)
}
fn top_layer_by_name(&self, cursive: &mut Cursive, layer_name: &str) {
let stack = cursive.screen_mut();
let from = stack.find_layer_from_id(layer_name).unwrap();
stack.move_layer(from, LayerPosition::FromFront(0));
}
fn has_form_fields(&self, action_name: &str) -> bool {
if let Some(v) = self.form_fields_count.get(&action_name) {
if *v == 0 as u8 {
return false;
} else {
return true;
}
}
return false;
}
fn input_from_tui(&mut self) -> Option<(String, Value)> {
let mut c = Cursive::default();
self.add_forms(&mut c);
self.add_cmd_picker(&mut c);
loop {
let current_step = *self.active_step.borrow();
match current_step {
0 => ::std::process::exit(0),
1 => {
if self.skip_single_action && self.actions.len() < 2 {
let action_with_desc = self.actions.keys().nth(0).unwrap().clone();
*self.picked_action.borrow_mut() = Some(action_with_desc);
*self.active_step.borrow_mut() = 2;
continue;
}
self.top_layer_by_name(&mut c, COMMAND_PICKER_ID);
},
2 => {
let action_with_desc = match self.picked_action.borrow().clone() {
Some(v) => v,
None => {
*self.active_step.borrow_mut() = 1;
continue;
},
};
let action_name = self.actions.get(&action_with_desc).unwrap().name;
*self.picked_action.borrow_mut() = Some(action_name.to_string());
if !self.has_form_fields(&action_name) {
*self.form_data.borrow_mut() = Some(json!({}));
*self.active_step.borrow_mut() = 3;
continue;
}
self.top_layer_by_name(&mut c, action_name);
},
3 => break,
_ => unimplemented!(),
}
c.run();
if current_step == *self.active_step.borrow() {
*self.active_step.borrow_mut() = 0;
}
}
Some((
self.picked_action.borrow().clone().unwrap(),
self.form_data.borrow().clone().unwrap(),
))
}
pub fn name(mut self, name: &'attrs str) -> Self {
self.name = name;
self
}
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_about(&self) -> &str {
&self.about
}
pub fn get_author(&self) -> &str {
&self.author
}
pub fn get_version(&self) -> &str {
&self.version
}
pub fn version(mut self, version: &'attrs str) -> Self {
self.version = version;
self
}
pub fn about(mut self, about: &'attrs str) -> Self {
self.about = about;
self
}
pub fn author(mut self, author: &'attrs str) -> Self {
self.author = author;
self
}
pub fn theme(mut self, theme: &'attrs str) -> Self {
self.theme = theme;
self
}
pub fn skip_single_action(mut self, skip: bool) -> Self {
self.skip_single_action = skip;
self
}
pub fn skip_empty_form(mut self, skip: bool) -> Self {
self.skip_empty_form = skip;
self
}
}
#[cfg(test)]
mod test_date_getting_from_program_args {
use super::*;
#[test]
fn cli_checkbox_is_serialized_ok_when_value_preset() {
let value = Fui::new("app")
.action(
"action1",
"desc",
FormView::new().field(fields::Checkbox::new("ch1")),
|_| {},
)
.input_from_cli(vec!["my_app", "action1", "--ch1"]);
let exp: Value = serde_json::from_str(r#"{ "ch1": true }"#).unwrap();
assert_eq!(value, Some(("action1: desc".to_string(), exp)));
}
#[test]
fn cli_checkbox_is_serialized_ok_when_value_missing() {
let value = Fui::new("app")
.action(
"action1",
"desc",
FormView::new().field(fields::Checkbox::new("ch1")),
|_| {},
)
.input_from_cli(vec!["my_app", "action1"]);
let exp: Value = serde_json::from_str(r#"{ "ch1": false }"#).unwrap();
assert_eq!(value, Some(("action1: desc".to_string(), exp)));
}
#[test]
fn cli_text_is_serialized_ok_when_value_preset() {
let value = Fui::new("app")
.action(
"action1",
"desc",
FormView::new().field(fields::Text::new("t1")),
|_| {},
)
.input_from_cli(vec!["my_app", "action1", "--t1", "v1"]);
let exp: Value = serde_json::from_str(r#"{ "t1": "v1" }"#).unwrap();
assert_eq!(value, Some(("action1: desc".to_string(), exp)));
}
#[test]
fn cli_autocomplete_is_serialized_ok_when_value_preset() {
let value = Fui::new("app")
.action(
"action1",
"desc",
FormView::new().field(fields::Autocomplete::new("ac", vec!["v1", "v2", "v3"])),
|_| {},
)
.input_from_cli(vec!["my_app", "action1", "--ac", "v1"]);
let exp: Value = serde_json::from_str(r#"{ "ac": "v1" }"#).unwrap();
assert_eq!(value, Some(("action1: desc".to_string(), exp)));
}
#[test]
fn cli_multiselect_is_serialized_ok_when_value_preset() {
let value = Fui::new("app")
.action(
"action1",
"desc",
FormView::new().field(fields::Multiselect::new("mf", vec!["v1", "v2", "v3"])),
|_| {},
)
.input_from_cli(vec!["my_app", "action1", "--mf", "v1"]);
let exp: Value = serde_json::from_str(r#"{ "mf": ["v1"] }"#).unwrap();
assert_eq!(value, Some(("action1: desc".to_string(), exp)));
}
}
#[cfg(test)]
mod dump_as_cli {
use super::*;
#[test]
fn test_value_is_converted_to_cmd_ok_when_is_string() {
let v: Value = serde_json::from_str(r#"{ "arg": "abc" }"#).unwrap();
assert_eq!(v.dump_as_cli(), r#"--arg abc"#);
}
#[test]
fn test_value_string_includes_quotes_when_include_space() {
let v: Value = serde_json::from_str(r#"{ "arg": "a b" }"#).unwrap();
assert_eq!(v.dump_as_cli(), r#"--arg "a b""#);
}
#[test]
fn test_value_is_converted_to_cmd_ok_when_is_array() {
let v: Value = serde_json::from_str(r#"{ "arg": ["a", "b c"] }"#).unwrap();
assert_eq!(v.dump_as_cli(), r#"--arg a "b c""#);
}
#[test]
fn test_value_is_empty_when_arg_is_false() {
let v: Value = serde_json::from_str(r#"{ "arg": false }"#).unwrap();
assert_eq!(v.dump_as_cli(), r#""#);
}
}
#[cfg(test)]
mod value2array_tests {
use super::*;
#[test]
fn test_value_empty_object_is_converted_to_empty_array() {
let v: Value = serde_json::from_str(r#"{}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = Vec::new();
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_bool_false_is_converted_to_empty_array() {
let v: Value = serde_json::from_str(r#"{"arg": false}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = Vec::new();
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_bool_true_is_converted_to_arg() {
let v: Value = serde_json::from_str(r#"{"arg": true}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = vec!["--arg"].iter().map(|x| x.to_string()).collect();
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_numerical_is_converted_to_arg() {
let v: Value = serde_json::from_str(r#"{"arg": 5}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = vec!["--arg", "5"].iter().map(|x| x.to_string()).collect();
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_string_is_converted_to_arg() {
let v: Value = serde_json::from_str(r#"{"arg": "text"}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = vec!["--arg", "text"]
.iter()
.map(|x| x.to_string())
.collect();
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_empty_string_is_skipped() {
let v: Value = serde_json::from_str(r#"{"arg": ""}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = Vec::new();
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_array_is_converted_to_arg() {
let v: Value = serde_json::from_str(r#"{"arg": ["a", "b", "c"]}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = vec!["--arg", "a", "b", "c"]
.iter()
.map(|x| x.to_string())
.collect();
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_empty_object_is_converted_to_arg() {
let v: Value = serde_json::from_str(r#"{"subcmd": {}}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = vec!["subcmd".into()];
assert_eq!(found, expected);
}
#[test]
fn test_value_object_with_text_in_object_is_converted_to_arg() {
let v: Value = serde_json::from_str(r#"{"subcmd": {"arg": "text"}}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = vec!["subcmd", "--arg", "text"]
.iter()
.map(|x| x.to_string())
.collect();
assert_eq!(found, expected);
}
#[test]
fn test_order_is_respected_for_positional_values() {
let v: Value = serde_json::from_str(r#"{"2": "b", "1": "a", "3": "c"}"#).unwrap();
let found: Vec<String> = value2array(&v);
let expected: Vec<String> = vec!["a", "b", "c"].iter().map(|x| x.to_string()).collect();
assert_eq!(found, expected);
}
}