use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
pub const SHIPPED: &[(&str, &str)] = &[
("editor.conf", include_str!("../../shoin/editor.conf")),
("glyphs.conf", include_str!("../../shoin/glyphs.conf")),
("input.conf", include_str!("../../shoin/input.conf")),
("keys.conf", include_str!("../../shoin/keys.conf")),
("layout.conf", include_str!("../../shoin/layout.conf")),
("theme.conf", include_str!("../../shoin/theme.conf")),
("tree.conf", include_str!("../../shoin/tree.conf")),
];
pub fn target_dir() -> Option<PathBuf> {
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
if !xdg.is_empty() {
return Some(PathBuf::from(xdg).join("shoin"));
}
}
let home = std::env::var_os("HOME").filter(|h| !h.is_empty())?;
Some(PathBuf::from(home).join(".config").join("shoin"))
}
#[derive(Debug)]
pub struct Seeded {
pub dir: PathBuf,
pub written: Vec<&'static str>,
pub replaced: Vec<&'static str>,
}
pub fn seed(dir: &Path, force: bool) -> Result<Seeded> {
let existing: Vec<&'static str> = SHIPPED
.iter()
.map(|(name, _)| *name)
.filter(|name| dir.join(name).exists())
.collect();
let occupied = std::fs::read_dir(dir)
.map(|rd| {
rd.flatten()
.any(|e| e.path().extension().and_then(|x| x.to_str()) == Some("conf"))
})
.unwrap_or(false);
if occupied && !force {
bail!(
"{} already has a config — nothing written (use --force to replace it)",
dir.display()
);
}
std::fs::create_dir_all(dir)
.with_context(|| format!("creating {}", dir.display()))?;
let mut written = Vec::new();
for (name, body) in SHIPPED {
let path = dir.join(name);
std::fs::write(&path, body)
.with_context(|| format!("writing {}", path.display()))?;
written.push(*name);
}
Ok(Seeded {
dir: dir.to_path_buf(),
written,
replaced: if force { existing } else { Vec::new() },
})
}
pub fn run(force: bool) -> Result<()> {
let dir = target_dir()
.context("no HOME or XDG_CONFIG_HOME to put a config in — use --config <path>")?;
let done = seed(&dir, force)?;
println!("wrote {} files to {}", done.written.len(), done.dir.display());
for name in &done.written {
println!(" {name}");
}
if !done.replaced.is_empty() {
println!("replaced: {}", done.replaced.join(", "));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn dir(tag: &str) -> PathBuf {
use std::sync::atomic::{AtomicUsize, Ordering};
static N: AtomicUsize = AtomicUsize::new(0);
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let n = N.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("shoin-init-{tag}-{t}-{n}"))
}
#[test]
fn the_embedded_set_is_the_whole_shipped_directory() {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("shoin");
let mut on_disk: Vec<String> = std::fs::read_dir(&src)
.expect("shoin/ ships with the repo")
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".conf"))
.collect();
on_disk.sort();
let mut embedded: Vec<String> =
SHIPPED.iter().map(|(n, _)| (*n).to_string()).collect();
embedded.sort();
assert_eq!(embedded, on_disk, "SHIPPED has drifted from shoin/");
for (name, body) in SHIPPED {
let disk = std::fs::read_to_string(src.join(name)).unwrap();
assert_eq!(*body, disk, "{name} differs from the shipped file");
}
}
#[test]
fn a_seeded_directory_loads_cleanly() {
let d = dir("load");
let done = seed(&d, false).unwrap();
assert_eq!(done.written.len(), SHIPPED.len());
assert!(done.replaced.is_empty());
let cfg = super::super::load(Some(&d)).expect("the seeded config loads");
let warnings = super::super::validate(&cfg);
assert!(warnings.is_empty(), "seeded config warns: {warnings:?}");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn an_existing_config_is_refused_without_force() {
let d = dir("refuse");
std::fs::create_dir_all(&d).unwrap();
std::fs::write(d.join("mine.conf"), "[layout]\nmeasure = 55\n").unwrap();
let err = seed(&d, false).unwrap_err().to_string();
assert!(err.contains("already has a config"), "{err}");
assert!(err.contains("--force"), "the message says how to proceed: {err}");
assert_eq!(
std::fs::read_to_string(d.join("mine.conf")).unwrap(),
"[layout]\nmeasure = 55\n"
);
assert!(!d.join("editor.conf").exists(), "nothing was written");
let done = seed(&d, true).unwrap();
assert!(done.replaced.is_empty(), "none of OURS were there before");
assert!(d.join("editor.conf").exists());
assert!(d.join("mine.conf").exists(), "a file we do not ship is left alone");
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn force_reports_what_it_replaced() {
let d = dir("force");
seed(&d, false).unwrap();
let again = seed(&d, true).unwrap();
assert_eq!(again.replaced.len(), SHIPPED.len());
std::fs::remove_dir_all(&d).ok();
}
#[test]
fn the_target_is_where_discovery_will_look() {
let dir = target_dir().expect("a home on this machine");
assert!(dir.ends_with("shoin"), "{}", dir.display());
assert!(dir.is_absolute(), "{}", dir.display());
}
}