Skip to main content

invoice_cli/
typst_assets.rs

1// ═══════════════════════════════════════════════════════════════════════════
2// Embedded Typst sources. Extracted to ~/.local/share/invoice/typst/ on first
3// use so templates can reference each other via relative #import paths.
4// ═══════════════════════════════════════════════════════════════════════════
5
6use rust_embed::RustEmbed;
7use std::path::Path;
8
9use crate::config;
10use crate::error::Result;
11
12#[derive(RustEmbed)]
13#[folder = "typst/"]
14#[prefix = ""]
15pub struct Assets;
16
17pub fn ensure_extracted() -> Result<()> {
18    let root = config::assets_path()?;
19    std::fs::create_dir_all(&root)?;
20    for path in Assets::iter() {
21        let file = Assets::get(&path).expect("embedded asset");
22        let dest = root.join(path.as_ref());
23        if let Some(parent) = dest.parent() {
24            std::fs::create_dir_all(parent)?;
25        }
26        // Only overwrite if content differs or file is missing
27        let needs_write = match std::fs::read(&dest) {
28            Ok(existing) => existing != file.data.as_ref(),
29            Err(_) => true,
30        };
31        if needs_write {
32            std::fs::write(&dest, file.data.as_ref())?;
33        }
34    }
35    Ok(())
36}
37
38pub fn template_dir() -> Result<std::path::PathBuf> {
39    Ok(config::assets_path()?.join("templates"))
40}
41
42pub fn shared_dir() -> Result<std::path::PathBuf> {
43    Ok(config::assets_path()?.join("shared"))
44}
45
46pub fn template_path(name: &str) -> Result<std::path::PathBuf> {
47    Ok(template_dir()?.join(format!("{name}.typ")))
48}
49
50pub fn list_templates() -> Result<Vec<String>> {
51    let dir = template_dir()?;
52    let mut names = Vec::new();
53    if dir.exists() {
54        for entry in std::fs::read_dir(&dir)? {
55            let entry = entry?;
56            let path = entry.path();
57            if path.extension().and_then(|s| s.to_str()) == Some("typ") {
58                if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
59                    // Skip stress variants
60                    if !name.ends_with("-stress") {
61                        names.push(name.to_string());
62                    }
63                }
64            }
65        }
66    }
67    names.sort();
68    Ok(names)
69}
70
71pub fn has_template(name: &str) -> Result<bool> {
72    Ok(template_path(name)?.exists())
73}
74
75pub fn project_root() -> Result<std::path::PathBuf> {
76    config::assets_path()
77}
78
79pub fn is_within_root(path: &Path) -> Result<bool> {
80    let root = project_root()?;
81    Ok(path.starts_with(&root))
82}