abbaye/utils.rs
1use std::io::IsTerminal;
2use std::path::{Path, PathBuf};
3
4use flate2::Compression;
5use flate2::write::GzEncoder;
6use miette::{IntoDiagnostic, Result};
7
8/// Expands shell-style variables in a path string.
9///
10/// This function takes a path containing variables in `$VAR` or `${VAR}` syntax
11/// and substitutes them with values from the provided iterator of key-value pairs.
12/// Any variables not found in the provided mappings are left unchanged.
13///
14/// # Generic Parameters
15///
16/// * `P` - A type that can be converted to a `Path`
17/// * `I` - An iterator-like type that produces `(S, S)` tuples
18/// * `S` - A type that can be read as `&str`
19///
20/// # Arguments
21///
22/// * `path` - The path string or `Path` object containing variables
23/// * `vars` - An iterator of `(key, value)` pairs for variable substitution
24///
25/// # Returns
26///
27/// A `PathBuf` with all variables substituted with their corresponding values.
28///
29/// # Examples
30///
31/// ```
32/// use std::collections::HashMap;
33///
34/// let mut vars = HashMap::new();
35/// vars.insert("USER", "john");
36/// vars.insert("PROJECT", "rust");
37///
38/// let expanded = expand_variables("/home/$USER/$PROJECT", vars);
39/// assert_eq!(expanded.to_string_lossy(), "/home/john/rust");
40/// ```
41///
42/// With a vector:
43///
44/// ```
45/// let vars = vec![("USER", "john"), ("PROJECT", "rust")];
46/// let expanded = expand_variables("/home/$USER/${PROJECT}", vars);
47/// assert_eq!(expanded.to_string_lossy(), "/home/john/rust");
48/// ```
49pub fn expand_variables<P, I, S>(path: P, vars: I) -> PathBuf
50where
51 P: AsRef<Path>,
52 I: IntoIterator<Item = (S, S)>,
53 S: AsRef<str>,
54{
55 let mut path_str = path.as_ref().to_string_lossy().to_string();
56
57 for (key, value) in vars {
58 let key = key.as_ref();
59 let value = value.as_ref();
60 path_str = path_str.replace(&format!("${{{key}}}"), value);
61 path_str = path_str.replace(&format!("${key}"), value);
62 }
63
64 PathBuf::from(path_str)
65}
66
67/// Pack `src` directory into a `.tar.gz` archive at `dest`.
68///
69/// The top-level entry inside the archive is named after the source directory.
70pub(crate) fn archive_dir(src: &Path, dest: &Path) -> Result<()> {
71 let dir_name = src
72 .file_name()
73 .map(|n| n.to_string_lossy().into_owned())
74 .unwrap_or_else(|| "docs".to_owned());
75
76 let file = std::fs::File::create(dest).into_diagnostic()?;
77 let enc = GzEncoder::new(file, Compression::default());
78 let mut archive = tar::Builder::new(enc);
79 archive.append_dir_all(&dir_name, src).into_diagnostic()?;
80 archive
81 .into_inner()
82 .into_diagnostic()?
83 .finish()
84 .into_diagnostic()?;
85 Ok(())
86}
87
88/// Check whether the program is running in an interactive terminal suitable
89/// for progress bars and spinners.
90///
91/// Returns `false` when stdout is not a terminal (piped to a file or another
92/// process) or when `TERM` is set to `dumb`. This lets callers gracefully
93/// fall back to plain log output instead of using ANSI-spinner-based UI.
94pub fn is_interactive() -> bool {
95 std::io::stdout().is_terminal() && std::env::var("TERM").ok().as_deref() != Some("dumb")
96}