Skip to main content

invoice_cli/
typst_assets.rs

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