use std::path::{Path, PathBuf};
use crate::art::Canvas;
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Origin {
Builtin,
Local(PathBuf),
}
impl std::fmt::Display for Origin {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Builtin => write!(out, "built in"),
Self::Local(path) => write!(out, "{}", path.display()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Template {
pub name: String,
pub canvas: Canvas,
pub origin: Origin,
}
impl Template {
#[must_use]
pub fn title(&self) -> &str {
self.canvas
.meta()
.name
.as_deref()
.unwrap_or(self.name.as_str())
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.canvas.meta().description.as_deref()
}
#[must_use]
pub fn author(&self) -> Option<&str> {
self.canvas.meta().author.as_deref()
}
}
#[must_use]
pub fn local_dirs() -> Vec<PathBuf> {
let mut dirs = vec![PathBuf::from("templates")];
if let Some(config) = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
{
dirs.push(config.join("mossaic").join("templates"));
}
dirs
}
#[must_use]
pub fn catalogue() -> Vec<Template> {
let mut found: Vec<Template> = Vec::new();
let mut seen: Vec<String> = Vec::new();
for dir in local_dirs() {
for (name, canvas) in read_dir(&dir).0 {
if !seen.contains(&name) {
seen.push(name.clone());
found.push(Template {
name,
canvas,
origin: Origin::Local(dir.clone()),
});
}
}
}
for (name, source) in BUILTIN {
if seen.iter().any(|taken| taken == name) {
continue;
}
if let Ok(canvas) = Canvas::parse(source) {
found.push(Template {
name: (*name).to_string(),
canvas,
origin: Origin::Builtin,
});
}
}
found.sort_by(|a, b| a.name.cmp(&b.name));
found
}
#[derive(Debug, Clone)]
pub struct Skipped {
pub file: String,
pub stem: String,
pub why: String,
}
fn read_dir(dir: &Path) -> (Vec<(String, Canvas)>, Vec<Skipped>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return (Vec::new(), Vec::new());
};
let mut found = Vec::new();
let mut skipped = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("art") {
continue;
}
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
let file = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(stem)
.to_string();
let body = match std::fs::read_to_string(&path) {
Ok(body) => body,
Err(error) => {
skipped.push(Skipped {
file,
stem: stem.to_string(),
why: error.to_string(),
});
continue;
}
};
match Canvas::parse(&body) {
Ok(canvas) => found.push((stem.to_string(), canvas)),
Err(why) => skipped.push(Skipped {
file,
stem: stem.to_string(),
why,
}),
}
}
found.sort_by(|a, b| a.0.cmp(&b.0));
skipped.sort_by(|a, b| a.file.cmp(&b.file));
(found, skipped)
}
#[must_use]
pub fn skipped() -> Vec<Skipped> {
local_dirs()
.iter()
.flat_map(|dir| read_dir(dir).1)
.collect()
}
pub fn find(name: &str) -> Result<Template, String> {
let catalogue = catalogue();
let found = catalogue.iter().find(|template| template.name == name);
if found.is_none_or(|template| template.origin == Origin::Builtin) {
if let Some(broken) = skipped().into_iter().find(|s| s.stem == name) {
return Err(format!("{}: {}", broken.file, broken.why));
}
}
if let Some(found) = found {
return Ok(found.clone());
}
if catalogue.is_empty() {
return Err(format!(
"no template named {name:?}, and none are installed"
));
}
let names: Vec<&str> = catalogue
.iter()
.map(|template| template.name.as_str())
.collect();
Err(format!(
"no template named {name:?} — there is {}",
names.join(", ")
))
}
#[must_use]
pub fn builtin_sources() -> &'static [(&'static str, &'static str)] {
BUILTIN
}