smarana 0.9.8

An extensible note taking system for typst.
use std::io::Write;
use std::process::{Command, Stdio};

use crate::config::{AppConfig, GlobalConfig};


/// Creates a new note, handles collisions by silently opening the existing file,
/// and automatically spawns the editor on creation.
pub fn create_note(name: Option<String>, template_override: Option<String>, note_type: Option<String>, tags: Option<Vec<String>>, body_content: Option<String>, no_edit: bool) {
    let global = GlobalConfig::load();
    let notebook_path = global.notebook_path().unwrap_or_else(|| {
        eprintln!("Notebook not initialized. Run `sma -I` first.");
        std::process::exit(1);
    });

    let config = AppConfig::load();
    
    let title = name.unwrap_or_else(|| config.smarana.default_title.clone());

    // Generate filename slug
    let filename_slug = generate_filename(&title, &config.smarana.filename_gen, &config.smarana.shell);
    let filename = format!("{}.typ", filename_slug.trim());

    let file_path = notebook_path.join(&filename);

    if !file_path.exists() {

        // Determine template: explicit override > type-based > default
        let resolved_type = note_type.as_deref().unwrap_or("fleeting");
        let template_name = if let Some(ref t) = template_override {
            t.clone()
        } else {
            match resolved_type {
                "fleeting" => "fleeting.typ".to_string(),
                "capture" => "capture.typ".to_string(),
                "atomic" => "atomic.typ".to_string(),
                _ => config.frontmatter.template.clone(),
            }
        };

        let template_path = notebook_path.join(".smarana").join("templates").join(&template_name);
        
        let mut matter = match std::fs::read_to_string(&template_path) {
            Ok(content) => content,
            Err(e) => {
                eprintln!("Failed to read template `{}`: {}", template_path.display(), e);
                std::process::exit(1);
            }
        };

        // Built-in {title} replacement
        matter = matter.replace("{title}", &title);

        // Dynamic shell replacements
        for (k, v) in &config.frontmatter.variables {
            let output = eval_shell_script(v, &config.smarana.shell);
            matter = matter.replace(&format!("{{{}}}", k), output.trim());
        }

        // Add explicit tags if provided
        if let Some(t) = tags {
            if !t.is_empty() {
                let mut tags_str = t.iter().map(|tag| format!("\"{}\"", tag)).collect::<Vec<_>>().join(", ");
                if t.len() == 1 {
                    tags_str.push(',');
                }
                // The template generally has `tags: (),`
                matter = matter.replace("tags: (),", &format!("tags: ({}),", tags_str));
            }
        }

        // Add stdin content if in interactive mode
        if let Some(content) = body_content {
            matter.push_str("\n");
            matter.push_str(&content);
            matter.push_str("\n");
        }

        // Write file
        if let Err(e) = std::fs::write(&file_path, matter) {
            eprintln!("Failed to create note file: {e}");
            std::process::exit(1);
        }

        crate::vprintln!("Created note: {}", file_path.display());

        // Immediately sync the newly created file so it appears in smarana.typ
        // before the editor opens. This enables immediate LSP completion for the new note!
        crate::db::sync(&notebook_path);
    }

    if !no_edit {
        // Launch Editor natively
        let status = Command::new(&config.smarana.editor)
            .arg(&file_path)
            .status();

        match status {
            Ok(s) if !s.success() => {
                eprintln!("Editor exited with non-zero status");
            }
            Err(e) => {
                eprintln!("Failed to open editor '{}': {}", config.smarana.editor, e);
            }
            _ => {}
        }
    }

    // Capture modifications inherently saved by the Editor back to the DB cleanly.
    crate::db::sync(&notebook_path);
}

fn generate_filename(name: &str, script: &str, shell: &str) -> String {
    let mut child = Command::new(shell)
        .arg("-c")
        .arg(script)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap_or_else(|e| {
            eprintln!("Failed to spawn filename generation script: {e}");
            std::process::exit(1);
        });

    if let Some(mut stdin) = child.stdin.take() {
        if let Err(e) = stdin.write_all(name.as_bytes()) {
            eprintln!("Failed to pipe name to filename_gen script: {e}");
        }
    }

    let output = child.wait_with_output().unwrap_or_else(|e| {
        eprintln!("Failed to wait on filename_gen script: {e}");
        std::process::exit(1);
    });

    if !output.status.success() {
        eprintln!("filename_gen script failed with status: {}", output.status);
        std::process::exit(1);
    }

    String::from_utf8_lossy(&output.stdout).to_string()
}

fn eval_shell_script(script: &str, shell: &str) -> String {
    let output = Command::new(shell)
        .arg("-c")
        .arg(script)
        .output()
        .unwrap_or_else(|e| {
            eprintln!("Failed to execute frontmatter script `{}`: {e}", script);
            std::process::exit(1);
        });

    if !output.status.success() {
        eprintln!("frontmatter script `{}` failed with status: {}", script, output.status);
    }

    String::from_utf8_lossy(&output.stdout).to_string()
}