clin-rs 0.8.8

Encrypted terminal note-taking app inspired by Obsidian
use std::fs;
use std::path::PathBuf;

use anyhow::{Context, Result};

use super::model::{ContentConfig, Template, TitleConfig};
use super::store::sanitize_filename;

#[derive(Debug, Clone)]
pub struct TemplateSummary {
    pub filename: String,
    pub name: String,
}

#[derive(Debug)]
pub struct TemplateManager {
    templates_dir: PathBuf,
}

impl TemplateManager {
    pub fn new(templates_dir: PathBuf) -> Self {
        Self { templates_dir }
    }

    pub fn ensure_dir(&self) -> Result<()> {
        fs::create_dir_all(&self.templates_dir).context("failed to create templates directory")?;
        Ok(())
    }

    pub fn template_path(&self, name: &str) -> PathBuf {
        let filename = sanitize_filename(name);
        self.templates_dir.join(format!("{filename}.toml"))
    }

    pub fn list(&self) -> Result<Vec<TemplateSummary>> {
        let mut templates = Vec::new();

        if !self.templates_dir.exists() {
            return Ok(templates);
        }

        for entry in
            fs::read_dir(&self.templates_dir).context("failed to read templates directory")?
        {
            let entry = entry.context("failed to read template entry")?;
            let path = entry.path();

            if path.extension().and_then(|e| e.to_str()) != Some("toml") {
                continue;
            }

            let filename = path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown")
                .to_string();

            match Template::load(&path) {
                Ok(template) => {
                    templates.push(TemplateSummary {
                        filename,
                        name: template.name,
                    });
                }
                Err(_) => {
                    continue;
                }
            }
        }

        templates.sort_by_key(|a| a.name.to_lowercase());

        Ok(templates)
    }

    pub fn load(&self, filename: &str) -> Result<Template> {
        let path = self.template_path(filename);
        Template::load(&path)
    }

    pub fn save(&self, filename: &str, template: &Template) -> Result<()> {
        self.ensure_dir()?;
        let path = self.template_path(filename);
        template.save(&path)
    }

    pub fn delete(&self, filename: &str) -> Result<()> {
        let path = self.template_path(filename);
        fs::remove_file(&path).with_context(|| {
            format!(
                "failed to delete template file '{}'",
                path.file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("unknown")
            )
        })?;
        Ok(())
    }

    pub fn load_default(&self) -> Option<Template> {
        self.load("default").ok()
    }

    pub fn has_templates(&self) -> bool {
        self.list().map(|t| !t.is_empty()).unwrap_or(false)
    }

    pub fn create_examples(&self) -> Result<()> {
        if self.has_templates() {
            return Ok(());
        }

        self.ensure_dir()?;

        let meeting = Template {
            name: "Meeting Notes".to_string(),
            title: TitleConfig {
                template: Some("Meeting - {date}".to_string()),
            },
            content: ContentConfig {
                template: r"# Meeting Notes

**Date:** {date}
**Time:** {time}

## Attendees

- 

## Agenda

1. 

## Discussion

## Action Items

- [ ] 

## Next Meeting

"
                .to_string(),
            },
        };
        self.save("meeting", &meeting)?;

        let todo = Template {
            name: "Todo List".to_string(),
            title: TitleConfig {
                template: Some("Tasks - {date}".to_string()),
            },
            content: ContentConfig {
                template: r"# Tasks for {weekday}, {date}

## High Priority

- [ ] 

## Normal Priority

- [ ] 

## Low Priority

- [ ] 

## Notes

"
                .to_string(),
            },
        };
        self.save("todo", &todo)?;

        let journal = Template {
            name: "Journal Entry".to_string(),
            title: TitleConfig {
                template: Some("Journal - {date}".to_string()),
            },
            content: ContentConfig {
                template: r"# {weekday}, {date}

## How I'm feeling

## What happened today

## Grateful for

1. 
2. 
3. 

## Tomorrow's focus

"
                .to_string(),
            },
        };
        self.save("journal", &journal)?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn delete_removes_template_file() {
        let dir = tempfile::tempdir().unwrap();
        let manager = TemplateManager::new(dir.path().to_path_buf());
        let template = Template {
            name: "Temp".to_string(),
            title: TitleConfig {
                template: Some("Temp {date}".to_string()),
            },
            content: ContentConfig {
                template: "Body".to_string(),
            },
        };

        manager.save("to-delete", &template).unwrap();
        let path = manager.template_path("to-delete");
        assert!(path.exists());

        manager.delete("to-delete").unwrap();
        assert!(!path.exists());
    }
}