use std::{
env, fmt, fs,
io::{IsTerminal, stderr},
path::{Path, PathBuf},
};
use crate::{printer::Printer, shell};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Welcome<'a> {
pub bin: &'a str,
pub intro: &'a str,
pub path: &'a Path,
pub config_paths: &'a [PathBuf],
pub sample: Option<&'a Path>,
pub sample_url: &'a str,
pub next: &'a str,
pub command: &'a str,
}
impl fmt::Display for Welcome<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path = self.path.display();
let file = shell::quote(&self.path.to_string_lossy());
writeln!(f, "{}", self.intro)?;
writeln!(f)?;
writeln!(
f,
"No configuration found at {path}. Create it from the sample configuration:"
)?;
writeln!(f)?;
if let Some(sample) = self.sample {
let parent = self.path.parent().filter(|dir| !dir.as_os_str().is_empty());
if let Some(dir) = parent.filter(|dir| !dir.is_dir()) {
let mkdir = if cfg!(windows) { "mkdir" } else { "mkdir -p" };
let dir = shell::quote(&dir.to_string_lossy());
writeln!(f, " {mkdir} {dir}")?;
}
let copy = if cfg!(windows) {
"copy"
} else {
"install -m 644"
};
let sample = shell::quote(&sample.to_string_lossy());
writeln!(f, " {copy} {sample} {file}")?;
} else {
let curl = if cfg!(windows) { "curl.exe" } else { "curl" };
let url = self.sample_url;
writeln!(f, " {curl} -sSL --create-dirs -o {file} {url}")?;
}
writeln!(f)?;
writeln!(f, "{}", self.next)?;
writeln!(f)?;
let invocation = shell::invocation(self.bin, self.config_paths, None);
match self.command {
"" => writeln!(f, " {invocation}"),
command => writeln!(f, " {invocation} {command}"),
}
}
}
pub fn shipped_sample(bin: &str) -> Option<PathBuf> {
let exe = env::current_exe().ok()?;
let exe = if cfg!(windows) {
exe
} else {
fs::canonicalize(exe).ok()?
};
find_sample(exe.parent()?, bin)
}
pub fn is_wanted(printer: &impl Printer) -> bool {
!printer.is_json() && stderr().is_terminal()
}
fn find_sample(dir: &Path, bin: &str) -> Option<PathBuf> {
let sample: PathBuf = ["share", "doc", bin, "config.sample.toml"].iter().collect();
[dir.parent(), Some(dir)]
.into_iter()
.flatten()
.map(|prefix| prefix.join(&sample))
.find(|path| path.is_file())
}
#[cfg(test)]
mod tests {
use std::{
env, fs,
path::{Path, PathBuf},
process,
};
use crate::welcome::*;
const URL: &str = "https://example.org/config.sample.toml";
fn welcome<'a>(paths: &'a [PathBuf], sample: Option<&'a Path>) -> Welcome<'a> {
Welcome {
bin: "app",
intro: "Welcome to App.",
path: &paths[0],
config_paths: paths,
sample,
sample_url: URL,
next: "Edit it, then run a first command:",
command: "run",
}
}
#[test]
#[cfg(not(windows))]
fn the_welcome_reads_in_paragraphs() {
let paths = [PathBuf::from("/nonexistent/app/config.toml")];
let expected = "\
Welcome to App.
No configuration found at /nonexistent/app/config.toml. Create it from the sample configuration:
curl -sSL --create-dirs -o '/nonexistent/app/config.toml' https://example.org/config.sample.toml
Edit it, then run a first command:
app -c '/nonexistent/app/config.toml' run
";
assert_eq!(welcome(&paths, None).to_string(), expected);
}
#[test]
#[cfg(not(windows))]
fn the_welcome_commands_run_as_printed() {
let paths = [PathBuf::from("/a b/it's.toml")];
let file = r"'/a b/it'\''s.toml'";
let downloaded = welcome(&paths, None).to_string();
let curl = format!("\n curl -sSL --create-dirs -o {file} https://");
assert!(downloaded.contains(&curl));
assert!(downloaded.contains(&format!("\n app -c {file} run\n")));
let sample = Path::new("/usr/share/doc/app/config.sample.toml");
let copied = welcome(&paths, Some(sample)).to_string();
let copy = format!(
"\n mkdir -p '/a b'\n install -m 644 '{}' {file}\n",
sample.display()
);
assert!(copied.contains(©));
assert!(!copied.contains("curl"));
}
#[test]
#[cfg(windows)]
fn the_welcome_commands_run_in_cmd_and_powershell() {
let paths = [PathBuf::from(r"C:\a b\c.toml")];
let welcome = welcome(&paths, None).to_string();
let curl = r#" curl.exe -sSL --create-dirs -o "C:\a b\c.toml" https://"#;
assert!(welcome.contains(curl));
assert!(welcome.contains(r#" app -c "C:\a b\c.toml" run"#));
}
#[test]
fn the_shipped_sample_is_found_installed_or_extracted() {
let name = format!("pimalaya-cli-welcome-{}", process::id());
let prefix = env::temp_dir().join(name);
let bin = prefix.join("bin");
let sample = prefix.join("share/doc/app/config.sample.toml");
fs::create_dir_all(&bin).unwrap();
fs::create_dir_all(sample.parent().unwrap()).unwrap();
assert_eq!(find_sample(&bin, "app"), None);
fs::write(&sample, "").unwrap();
assert_eq!(find_sample(&bin, "app"), Some(sample.clone()));
assert_eq!(find_sample(&prefix, "app"), Some(sample));
fs::remove_dir_all(&prefix).unwrap();
}
}