use crate::error::{Result, WaxError};
use crate::sudo;
use console::style;
use indicatif::{ProgressBar, ProgressStyle};
use inquire::Confirm;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tracing::debug;
static SHOW_TIMING: AtomicBool = AtomicBool::new(false);
pub fn set_timing_enabled(enabled: bool) {
SHOW_TIMING.store(enabled, Ordering::Relaxed);
}
pub fn timing_enabled() -> bool {
SHOW_TIMING.load(Ordering::Relaxed)
}
pub fn elapsed_suffix(elapsed: Duration) -> String {
if timing_enabled() {
format!(" [{}ms]", elapsed.as_millis())
} else {
String::new()
}
}
pub const PROGRESS_BAR_CHARS: &str = "█▓▒░ ";
pub const PROGRESS_BAR_TEMPLATE: &str =
"{msg} {wide_bar:.cyan/blue} {bytes}/{total_bytes} {bytes_per_sec} eta {eta}";
pub const PROGRESS_BAR_PREFIX_TEMPLATE: &str =
"{prefix:.bold} {wide_bar:.cyan/blue} {bytes}/{total_bytes} {bytes_per_sec} eta {eta}";
pub const SPINNER_TICK_CHARS: &str = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
match copy_dir_all_inner(src, dst) {
Ok(()) => Ok(()),
Err(ref e) if sudo::is_permission_error(e) || sudo::is_file_exists_error(e) => {
debug!(
"copy_dir_all failed ({:?}), retrying with sudo: {} -> {}",
e,
src.display(),
dst.display()
);
sudo::sudo_copy(src, dst)
}
Err(e) => Err(e),
}
}
fn copy_dir_all_inner(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if ty.is_dir() {
if let Ok(dst_meta) = dst_path.symlink_metadata() {
if dst_meta.is_symlink() || dst_meta.is_file() {
std::fs::remove_file(&dst_path).or_else(|_| sudo::sudo_remove(&dst_path))?;
}
}
copy_dir_all_inner(&src_path, &dst_path)?;
} else if ty.is_symlink() {
#[cfg(unix)]
{
let target = std::fs::read_link(&src_path)?;
if let Ok(dst_meta) = dst_path.symlink_metadata() {
if dst_meta.is_dir() && !dst_meta.is_symlink() {
std::fs::remove_dir_all(&dst_path)
.or_else(|_| sudo::sudo_remove(&dst_path).map(|_| ()))?;
} else {
std::fs::remove_file(&dst_path)
.or_else(|_| sudo::sudo_remove(&dst_path).map(|_| ()))?;
}
}
std::os::unix::fs::symlink(&target, &dst_path)
.or_else(|_| sudo::sudo_symlink(target.as_ref(), &dst_path).map(|_| ()))?;
}
#[cfg(not(unix))]
{
std::fs::copy(&src_path, &dst_path)?;
}
} else {
if let Ok(dst_meta) = dst_path.symlink_metadata() {
if dst_meta.is_dir() && !dst_meta.is_symlink() {
std::fs::remove_dir_all(&dst_path)
.or_else(|_| sudo::sudo_remove(&dst_path).map(|_| ()))?;
} else if dst_meta.is_symlink() {
std::fs::remove_file(&dst_path)
.or_else(|_| sudo::sudo_remove(&dst_path).map(|_| ()))?;
}
}
copy_regular_file(&src_path, &dst_path)?;
}
}
Ok(())
}
fn copy_regular_file(src: &Path, dst: &Path) -> Result<()> {
std::fs::copy(src, dst)?;
Ok(())
}
pub fn find_in_path(program: &str) -> Option<PathBuf> {
if program.contains(std::path::MAIN_SEPARATOR) {
let path = PathBuf::from(program);
return path.is_file().then_some(path);
}
std::env::var_os("PATH")
.into_iter()
.flat_map(|paths| std::env::split_paths(&paths).collect::<Vec<_>>())
.map(|dir| dir.join(program))
.find(|path| path.is_file())
}
pub fn confirm_prompt(message: &str) -> Result<bool> {
if io::stdin().is_terminal() {
return Confirm::new(message)
.with_default(false)
.prompt_skippable()
.map(|answer| answer.unwrap_or(false))
.map_err(|e| WaxError::InstallError(format!("prompt failed: {}", e)));
}
print!(
"{} {} {} ",
style("?").cyan().bold(),
message,
style("[y/N]").dim()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(matches!(
input.trim().to_ascii_lowercase().as_str(),
"y" | "yes"
))
}
pub fn create_spinner(message: &str) -> ProgressBar {
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap()
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
);
spinner.enable_steady_tick(Duration::from_millis(80));
spinner.set_message(message.to_string());
spinner
}
pub mod dirs {
use crate::error::{Result, WaxError};
use std::path::PathBuf;
pub fn home_dir() -> Result<PathBuf> {
if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
return Ok(home);
}
#[cfg(windows)]
if let Some(home) = std::env::var_os("USERPROFILE").map(PathBuf::from) {
return Ok(home);
}
Err(WaxError::InstallError(
"Home directory is not set ($HOME or USERPROFILE). Cannot determine home directory."
.to_string(),
))
}
pub fn wax_dir() -> Result<PathBuf> {
Ok(home_dir()?.join(".wax"))
}
pub fn wax_cache_dir() -> Result<PathBuf> {
if let Ok(dir) = std::env::var("WAX_CACHE_DIR") {
if !dir.is_empty() {
return Ok(PathBuf::from(dir));
}
}
Ok(wax_dir()?.join("cache"))
}
pub fn wax_logs_dir() -> Result<PathBuf> {
Ok(wax_dir()?.join("logs"))
}
}
pub struct ProgressGuard {
pb: ProgressBar,
active: bool,
}
impl ProgressGuard {
pub fn new(pb: ProgressBar) -> Self {
Self { pb, active: true }
}
pub fn disarm(&mut self) {
self.active = false;
}
}
impl Drop for ProgressGuard {
fn drop(&mut self) {
if self.active {
self.pb.finish_and_clear();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
use std::sync::Mutex;
use tempfile::tempdir;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn test_create_spinner() {
let message = "Loading...";
let spinner = create_spinner(message);
assert_eq!(spinner.message(), message);
}
#[test]
fn test_dirs_resolution() {
let _guard = ENV_LOCK.lock().unwrap();
let original_home = env::var_os("HOME");
#[cfg(windows)]
let original_userprofile = env::var_os("USERPROFILE");
let dummy_home = tempdir().unwrap().path().to_path_buf();
env::set_var("HOME", &dummy_home);
#[cfg(windows)]
env::remove_var("USERPROFILE");
assert_eq!(dirs::home_dir().unwrap(), dummy_home);
assert_eq!(dirs::wax_dir().unwrap(), dummy_home.join(".wax"));
assert_eq!(
dirs::wax_cache_dir().unwrap(),
dummy_home.join(".wax/cache")
);
let override_cache = dummy_home.join("override-cache");
env::set_var("WAX_CACHE_DIR", &override_cache);
assert_eq!(dirs::wax_cache_dir().unwrap(), override_cache);
env::remove_var("WAX_CACHE_DIR");
assert_eq!(dirs::wax_logs_dir().unwrap(), dummy_home.join(".wax/logs"));
env::remove_var("HOME");
#[cfg(windows)]
env::remove_var("USERPROFILE");
assert!(dirs::home_dir().is_err());
if let Some(h) = original_home {
env::set_var("HOME", h);
} else {
env::remove_var("HOME");
}
#[cfg(windows)]
if let Some(p) = original_userprofile {
env::set_var("USERPROFILE", p);
} else {
env::remove_var("USERPROFILE");
}
}
#[test]
fn test_copy_dir_all_basic() {
let temp = tempdir().unwrap();
let src = temp.path().join("src");
let dst = temp.path().join("dst");
fs::create_dir(&src).unwrap();
fs::write(src.join("file1.txt"), "hello").unwrap();
let src_sub = src.join("subdir");
fs::create_dir(&src_sub).unwrap();
fs::write(src_sub.join("file2.txt"), "world").unwrap();
copy_dir_all(&src, &dst).unwrap();
assert!(dst.exists());
assert!(dst.join("file1.txt").exists());
assert_eq!(fs::read_to_string(dst.join("file1.txt")).unwrap(), "hello");
let dst_sub = dst.join("subdir");
assert!(dst_sub.exists());
assert!(dst_sub.join("file2.txt").exists());
assert_eq!(
fs::read_to_string(dst_sub.join("file2.txt")).unwrap(),
"world"
);
}
#[cfg(unix)]
#[test]
fn test_copy_dir_all_with_symlink() {
use std::os::unix::fs::symlink;
let temp = tempdir().unwrap();
let src = temp.path().join("src");
let dst = temp.path().join("dst");
fs::create_dir(&src).unwrap();
fs::write(src.join("target.txt"), "target").unwrap();
symlink("target.txt", src.join("link.txt")).unwrap();
copy_dir_all(&src, &dst).unwrap();
assert!(dst.join("link.txt").exists());
let meta = dst.join("link.txt").symlink_metadata().unwrap();
assert!(meta.file_type().is_symlink());
assert_eq!(
fs::read_link(dst.join("link.txt"))
.unwrap()
.to_str()
.unwrap(),
"target.txt"
);
assert_eq!(fs::read_to_string(dst.join("link.txt")).unwrap(), "target");
}
#[test]
fn test_copy_dir_all_overwrite() {
let temp = tempdir().unwrap();
let src = temp.path().join("src");
let dst = temp.path().join("dst");
fs::create_dir(&src).unwrap();
fs::write(src.join("file1.txt"), "new content").unwrap();
fs::create_dir(&dst).unwrap();
fs::write(dst.join("file1.txt"), "old content").unwrap();
copy_dir_all(&src, &dst).unwrap();
assert_eq!(
fs::read_to_string(dst.join("file1.txt")).unwrap(),
"new content"
);
}
}