mod tree;
use crate::cfg::TransformsValues;
use crate::cfg::VariableCfg;
use crate::cli_opt::*;
use crate::error::*;
use crate::variable_def::LabelValue;
use crate::variable_def::VariableDef;
use crate::FileOperation;
use crate::{Action, Ctx, Variables};
use console::Style;
use console::Term;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Confirm;
use dialoguer::Input;
use dialoguer::Select;
use handlebars_misc_helpers::new_hbs;
use lazy_static::lazy_static;
use std::borrow::Cow;
use tracing::{debug, instrument, span, warn, Level};
lazy_static! {
static ref TERM: Term = Term::stdout();
static ref TITLE_STYLE: Style = Style::new().bold();
static ref PROMPT_THEME: ColorfulTheme = ColorfulTheme::default();
}
fn write_title(s: &str) -> Result<()> {
TERM.write_line(&format!("\n\n{}\n", TITLE_STYLE.apply_to(s)))?;
Ok(())
}
#[derive(Debug)]
pub struct VariableResponse {
value: String,
idx: Option<usize>,
}
#[derive(Debug)]
pub struct VariableRequest {
prompt: String,
default_value: Option<VariableResponse>,
values: Vec<String>,
}
#[instrument]
fn to_variabledef(v: &VariableCfg) -> Result<VariableDef> {
let hidden: bool = match v.hidden {
None => false,
Some(ref v) => serde_yaml::from_str(v)?,
};
let select_in_values: Vec<LabelValue> = match &v.select_in_values {
None => vec![],
Some(v) => v.into(),
};
Ok(VariableDef {
name: v.name.clone(),
default_value: v.default_value.as_ref().map(|v| v.0.clone()),
ask: v.ask.clone(),
hidden,
select_in_values,
})
}
pub(crate) fn ask_variables(
ctx: &Ctx,
list_variables: &[VariableCfg],
mut init: Variables,
) -> Result<Variables> {
let mut variables = Variables::default();
variables.append(&mut init);
let handlebars = new_hbs();
write_title("Configure variables")?;
for variable_cfg in list_variables.iter().cloned() {
let _span_ = span!(Level::DEBUG, "ask_variables", ?variable_cfg).entered();
if variables.contains_key(&variable_cfg.name) {
continue;
}
let render = |v: &str| {
let r = handlebars.render_template(v, &variables);
match r {
Ok(s) => s,
Err(e) => {
warn!(input = ?v, error = ?e, "failed to convert");
v.into()
}
}
};
let variable_cfg = variable_cfg.transforms_values(&render)?;
let variable = to_variabledef(&variable_cfg)?;
let name = variable.name;
let request = {
let prompt = if variable.ask.is_some() {
let ask = variable.ask.expect("variable ask should defined");
handlebars
.render_template(&ask, &variables)
.map_err(|source| Error::Handlebars {
when: format!("define prompt for '{}'", &name),
template: ask.clone(),
source,
})?
} else {
name.clone()
};
let values: Vec<String> = variable
.select_in_values
.iter()
.map(|v| v.label.to_string())
.collect::<Vec<String>>();
let default_value = variable
.default_value
.and_then(|default_value| Variables::value_as_str(&default_value).ok())
.map(|value| {
let idx = variable
.select_in_values
.iter()
.enumerate()
.filter_map(|(i, v)| {
if v.label == value
|| Variables::value_as_str(&v.value)
.map(|v| v == value)
.unwrap_or(false)
{
Some(i)
} else {
None
}
})
.next();
VariableResponse { value, idx }
});
VariableRequest {
prompt,
default_value,
values,
}
};
let resp = if variable.hidden || ctx.cmd_opt.no_interaction {
request.default_value.unwrap_or(VariableResponse {
value: "".to_owned(),
idx: None,
})
} else {
ask_variable_value(request)?
};
if let Some(idx) = resp.idx {
variables.insert(format!("{}__idx", name), idx)?;
variables.insert(format!("{}__label", name), resp.value)?;
variables.insert(
name.clone(),
variable
.select_in_values
.get(idx)
.expect("selected should be in the list")
.value
.clone(),
)?;
} else {
variables.insert(name.clone(), Variables::value_from_str(&resp.value)?)?;
}
}
Ok(variables)
}
pub fn ask_variable_value(req: VariableRequest) -> Result<VariableResponse> {
if req.values.is_empty() {
let value = match req.default_value {
Some(v) if v.value == "true" || v.value == "false" => {
Confirm::with_theme(&(*PROMPT_THEME))
.default(v.value == "true")
.with_prompt(&req.prompt)
.interact()
.map(|r| r.to_string())?
}
_ => {
let mut input = Input::with_theme(&(*PROMPT_THEME));
if let Some(default_value) = req.default_value {
input.default(default_value.value);
}
input.with_prompt(&req.prompt).interact()?
}
};
Ok(VariableResponse { value, idx: None })
} else {
let mut input = Select::with_theme(&(*PROMPT_THEME));
input.with_prompt(&req.prompt).items(&req.values);
if let Some(default_value) = req.default_value.and_then(|v| v.idx) {
input.default(default_value);
}
let idx = input.interact()?;
Ok(VariableResponse {
value: req.values[idx].clone(),
idx: Some(idx),
})
}
}
fn format_operation(op: &FileOperation) -> Cow<'static, str> {
let s = match op {
FileOperation::Nothing => "do nothing",
FileOperation::Ignore => "ignore",
FileOperation::MkDir => "make dir",
FileOperation::AddFile => "add file",
FileOperation::UpdateFile => "update file",
};
console::pad_str(s, 15, console::Alignment::Left, Some("..."))
}
pub fn confirm_plan(ctx: &Ctx, actions: &[Action]) -> Result<bool> {
write_title("Plan to execute")?;
debug!(?actions, "plan");
let prefixes = tree::provide_prefix(actions, |parent, item| {
Some(parent.dst_path.relative.as_path()) == item.dst_path.relative.parent()
});
for (a, prefix) in actions.iter().zip(prefixes.iter()) {
let p = a.dst_path.base.join(&a.dst_path.relative);
let s = format!(
" - {} \x1B[38;2;{};{};{}m{}\x1B[0m{}",
format_operation(&a.operation),
80,
80,
80,
prefix,
p.file_name().and_then(|v| v.to_str()).unwrap_or("???"),
);
TERM.write_line(&s)?;
}
let r = if ctx.cmd_opt.confirm == AskConfirmation::Always {
Confirm::with_theme(&(*PROMPT_THEME))
.with_prompt("Do you want to apply plan ?")
.interact()?
} else {
true
};
Ok(r)
}
pub fn show_difference<P>(local: P, remote: P) -> Result<()>
where
P: AsRef<std::path::Path>,
{
use difference::Changeset;
use std::fs;
let local_str = fs::read_to_string(&local)?;
let remote_str = fs::read_to_string(&remote)?;
let changeset = Changeset::new(&local_str, &remote_str, "\n");
println!("{}", changeset);
Ok(())
}
pub fn ask_update_mode<P>(local: P) -> Result<UpdateMode>
where
P: AsRef<std::path::Path>,
{
let values = vec![
("show diff then ask", UpdateMode::ShowDiff),
("keep existing local file (ignore template)", UpdateMode::Keep),
("override local file with file from template", UpdateMode::Override),
("keep existing local file, add template with extension .REMOTE", UpdateMode::UpdateAsRemote),
("rename existing local file with extension .LOCAL, add template file", UpdateMode::CurrentAsLocal),
("try to merge existing local with remote template via merge tool (defined in the git's configuration)", UpdateMode::Merge),
];
let mut input = Select::with_theme(&(*PROMPT_THEME));
input
.with_prompt(&format!(
"Modification of {:?} (use arrow + return to select option)",
local.as_ref()
))
.items(
&values
.iter()
.map(|v| format!("{} - {}", v.1, v.0))
.collect::<Vec<_>>(),
)
.default(0);
let idx = input.interact()?;
Ok(values[idx].1.clone())
}
pub fn show_message(
_ctx: &Ctx,
template_name: impl std::fmt::Display,
message: impl std::fmt::Display,
) -> Result<()> {
println!("\n message from template: {}\n\t{}", template_name, message);
Ok(())
}
pub fn confirm_run_script(
ctx: &Ctx,
template_name: impl std::fmt::Display,
script: impl std::fmt::Display,
) -> Result<bool> {
println!(
"\n command to run:\n\t from template: {}\n\t commands:\n{}",
template_name, script
);
if ctx.cmd_opt.no_interaction {
Ok(true)
} else {
Confirm::with_theme(&(*PROMPT_THEME))
.with_prompt("Do you want to run the commands ?")
.interact()
.map_err(Error::from)
}
}