use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::utils;
const REGISTRY_REPO: &str = "JheisonMB/texforge-templates";
const GENERAL_TEMPLATE_TOML: &str = include_str!("general/template.toml");
const GENERAL_MAIN_TEX: &str = include_str!("general/main.tex");
const GENERAL_BODY_TEX: &str = include_str!("general/sections/body.tex");
const GENERAL_REFERENCES_BIB: &str = include_str!("general/bib/references.bib");
pub struct ResolvedTemplate {
pub files: HashMap<String, Vec<u8>>,
}
pub fn resolve(name: &str) -> Result<ResolvedTemplate> {
if let Ok(t) = load_from_cache(name) {
return Ok(t);
}
if let Ok(t) = download(name) {
return Ok(t);
}
if name == "general" {
return Ok(embedded_general());
}
anyhow::bail!(
"Template '{}' not found. Run 'texforge template add {}' first.",
name,
name
);
}
fn embedded_general() -> ResolvedTemplate {
let mut files = HashMap::new();
files.insert(
"template.toml".into(),
GENERAL_TEMPLATE_TOML.as_bytes().to_vec(),
);
files.insert("main.tex".into(), GENERAL_MAIN_TEX.as_bytes().to_vec());
files.insert(
"sections/body.tex".into(),
GENERAL_BODY_TEX.as_bytes().to_vec(),
);
files.insert(
"bib/references.bib".into(),
GENERAL_REFERENCES_BIB.as_bytes().to_vec(),
);
ResolvedTemplate { files }
}
fn load_from_cache(name: &str) -> Result<ResolvedTemplate> {
let dir = utils::templates_dir()?.join(name);
if !dir.is_dir() {
anyhow::bail!("not cached");
}
load_dir_recursive(&dir)
}
fn load_dir_recursive(base: &Path) -> Result<ResolvedTemplate> {
let mut files = HashMap::new();
for entry in walkdir::WalkDir::new(base)
.into_iter()
.filter_map(|e| e.ok())
{
if entry.file_type().is_file() {
let rel = entry
.path()
.strip_prefix(base)?
.to_string_lossy()
.to_string();
let content = std::fs::read(entry.path())?;
files.insert(rel, content);
}
}
Ok(ResolvedTemplate { files })
}
pub fn download(name: &str) -> Result<ResolvedTemplate> {
let url = format!(
"https://api.github.com/repos/{}/tarball/main",
REGISTRY_REPO
);
let response = reqwest::blocking::Client::new()
.get(&url)
.header("User-Agent", "texforge")
.send()
.context("Failed to connect to template registry")?;
if !response.status().is_success() {
anyhow::bail!("Registry returned HTTP {}", response.status());
}
let bytes = response.bytes()?;
let decoder = flate2::read::GzDecoder::new(&bytes[..]);
let mut archive = tar::Archive::new(decoder);
let cache_dir = utils::templates_dir()?.join(name);
let mut files = HashMap::new();
let prefix = format!("{}/", name);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?.to_string_lossy().to_string();
let Some(after_root) = path.split_once('/').map(|x| x.1) else {
continue;
};
let Some(rel) = after_root.strip_prefix(&prefix) else {
continue;
};
if rel.is_empty() || entry.header().entry_type().is_dir() {
continue;
}
let mut content = Vec::new();
std::io::Read::read_to_end(&mut entry, &mut content)?;
let dest = cache_dir.join(rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&dest, &content)?;
files.insert(rel.to_string(), content);
}
if files.is_empty() {
let _ = std::fs::remove_dir_all(&cache_dir);
anyhow::bail!("Template '{}' not found in registry", name);
}
Ok(ResolvedTemplate { files })
}
pub fn list_remote() -> Result<Vec<String>> {
let url = format!("https://api.github.com/repos/{}/contents", REGISTRY_REPO);
let response = reqwest::blocking::Client::new()
.get(&url)
.header("User-Agent", "texforge")
.send()
.context("Failed to connect to template registry")?;
if !response.status().is_success() {
anyhow::bail!("Registry returned HTTP {}", response.status());
}
#[derive(serde::Deserialize)]
struct Entry {
name: String,
#[serde(rename = "type")]
kind: String,
}
let entries: Vec<Entry> = response.json()?;
let mut names: Vec<String> = entries
.into_iter()
.filter(|e| e.kind == "dir")
.map(|e| e.name)
.collect();
names.sort();
Ok(names)
}
pub fn list_cached() -> Result<Vec<String>> {
let dir = utils::templates_dir()?;
let mut names = Vec::new();
if dir.is_dir() {
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
if let Some(name) = entry.file_name().to_str() {
names.push(name.to_string());
}
}
}
}
names.sort();
Ok(names)
}
pub fn remove_cached(name: &str) -> Result<PathBuf> {
let dir = utils::templates_dir()?.join(name);
if !dir.is_dir() {
anyhow::bail!("Template '{}' is not installed", name);
}
std::fs::remove_dir_all(&dir)?;
Ok(dir)
}