use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use color_eyre::owo_colors::OwoColorize;
use color_eyre::Result;
use log::Level;
use punktf_lib::visit::deploy::deployment::{Deployment, DeploymentStatus, ItemStatus};
pub fn get_target_path() -> Option<PathBuf> {
std::env::var_os(super::PUNKTF_TARGET_ENVVAR).map(|val| val.into())
}
pub fn ask_user_merge(source_path: &Path, deploy_path: &Path) -> Result<bool> {
use std::io::Write;
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
let mut line = String::new();
loop {
stdout.write_all(
format!(
"Overwrite `{}` with `{}` [y/N]: ",
deploy_path.display(),
source_path.display()
)
.as_bytes(),
)?;
stdout.flush()?;
stdin.read_line(&mut line)?;
line.make_ascii_lowercase();
return match line.trim() {
"y" => Ok(true),
"n" => Ok(false),
_ => {
line.clear();
continue;
}
};
}
}
fn output_and_clear(print: bool, s: &mut String, level: Level) {
if !s.is_empty() {
if print {
println!("{s}");
} else {
log::log!(level, "{}", s);
}
s.clear();
}
}
#[derive(Debug, Clone, Copy)]
struct DeployCounts {
success: usize,
skipped: usize,
failed: usize,
}
fn log_success<T, F>(
out: &mut String,
print: bool,
item_name: &str,
items: &HashMap<PathBuf, T>,
fmt_fn: F,
) -> usize
where
T: AsRef<ItemStatus>,
F: Fn(&Path, &T) -> String,
{
let mut item_count = 0;
for (idx, (path, item)) in items
.iter()
.filter(|(_, item)| item.as_ref().is_success())
.enumerate()
{
if idx == 0 {
out.push_str(&format!("{} ({})", item_name, "SUCCESS".green()));
}
out.push_str(&fmt_fn(path, item));
item_count += 1;
}
output_and_clear(print, out, Level::Info);
item_count
}
fn log_skipped<T, F>(
out: &mut String,
print: bool,
item_name: &str,
items: &HashMap<PathBuf, T>,
fmt_fn: F,
) -> usize
where
T: AsRef<ItemStatus>,
F: Fn(&Path, &T, &str) -> String,
{
let mut item_count = 0;
for (idx, (path, item, reason)) in items
.iter()
.filter_map(|(idx, item)| {
if let ItemStatus::Skipped(reason) = item.as_ref() {
Some((idx, item, reason))
} else {
None
}
})
.enumerate()
{
if idx == 0 {
out.push_str(&format!("{} ({})", item_name, "SKIPPED".yellow()));
}
out.push_str(&fmt_fn(path, item, reason));
item_count += 1;
}
output_and_clear(print, out, Level::Info);
item_count
}
fn log_failed<T, F>(
out: &mut String,
print: bool,
item_name: &str,
items: &HashMap<PathBuf, T>,
fmt_fn: F,
) -> usize
where
T: AsRef<ItemStatus>,
F: Fn(&Path, &T, &str) -> String,
{
let mut item_count = 0;
for (idx, (path, item, reason)) in items
.iter()
.filter_map(|(idx, item)| {
if let ItemStatus::Failed(reason) = item.as_ref() {
Some((idx, item, reason))
} else {
None
}
})
.enumerate()
{
if idx == 0 {
out.push_str(&format!("{} ({})", item_name, "FAILED".red()));
}
out.push_str(&fmt_fn(path, item, reason));
item_count += 1;
}
output_and_clear(print, out, Level::Info);
item_count
}
fn log_dotfiles(out: &mut String, deployment: &Deployment, print: bool) -> DeployCounts {
const ITEM_NAME: &str = "Dotfiles";
let files_success = log_success(out, print, ITEM_NAME, deployment.dotfiles(), |path, _| {
format!("\n\t{}", path.display().bright_black())
});
let files_skipped = log_skipped(
out,
print,
ITEM_NAME,
deployment.dotfiles(),
|path, _, reason| format!("\n\t{}: {}", path.display(), reason.bright_black()),
);
let files_failed = log_failed(
out,
print,
ITEM_NAME,
deployment.dotfiles(),
|path, _, reason| format!("\n\t{}: {}", path.display(), reason.bright_black()),
);
DeployCounts {
success: files_success,
skipped: files_skipped,
failed: files_failed,
}
}
fn log_links(out: &mut String, deployment: &Deployment, print: bool) -> DeployCounts {
const ITEM_NAME: &str = "Links";
let files_success = log_success(
out,
print,
ITEM_NAME,
deployment.symlinks(),
|path, link| {
format!(
"\n\t{} => {}",
link.source.display().bright_black(),
path.display().bright_black()
)
},
);
let files_skipped = log_skipped(
out,
print,
ITEM_NAME,
deployment.symlinks(),
|path, link, reason| {
format!(
"\n\t{} => {}: {}",
link.source.display().bright_black(),
path.display().bright_black(),
reason.bright_black()
)
},
);
let files_failed = log_failed(
out,
print,
ITEM_NAME,
deployment.symlinks(),
|path, link, reason| {
format!(
"\n\t{} => {}: {}",
link.source.display().bright_black(),
path.display().bright_black(),
reason.bright_black()
)
},
);
DeployCounts {
success: files_success,
skipped: files_skipped,
failed: files_failed,
}
}
pub fn log_deployment(deployment: &Deployment, print: bool) {
let mut out = String::new();
let DeployCounts {
success: dotfiles_success,
skipped: dotfiles_skipped,
failed: dotfiles_failed,
} = log_dotfiles(&mut out, deployment, print);
let DeployCounts {
success: links_success,
skipped: links_skipped,
failed: links_failed,
} = log_links(&mut out, deployment, print);
match deployment.status() {
DeploymentStatus::Success => {
out.push_str(&format!("Status: {}", "SUCCESS".green()));
}
DeploymentStatus::Failed(reason) => {
out.push_str(&format!("Status: {}\n\t{}", "FAILED".red(), reason));
}
};
let dotfiles_total = dotfiles_success + dotfiles_skipped + dotfiles_failed;
let links_total = links_success + links_skipped + links_failed;
let elapsed = deployment
.duration()
.expect("Failed to get duration from deployment");
let report = format!(
"
Time : {:?}
{hruler}
Files (deployed): {}
Files (skipped) : {}
Files (failed) : {}
Files (total) : {}
{hruler}
Links (deployed): {}
Links (skipped) : {}
Links (failed) : {}
Links (total) : {}",
elapsed,
dotfiles_success,
dotfiles_skipped,
dotfiles_failed,
dotfiles_total,
links_success,
links_skipped,
links_failed,
links_total,
hruler = "-".repeat(80).dimmed(),
);
out.push_str(&report);
output_and_clear(print, &mut out, Level::Info)
}