Skip to main content

snapper_fmt/
init.rs

1use std::fs;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5
6/// Detect which prose formats exist in the current directory tree.
7fn detect_formats(dir: &Path) -> Vec<&'static str> {
8    let mut formats = Vec::new();
9    let check = |ext: &str| -> bool {
10        walkdir(dir)
11            .into_iter()
12            .any(|e| e.path().extension().and_then(|e| e.to_str()) == Some(ext))
13    };
14    // Simple recursive check using std::fs
15    fn walkdir(dir: &Path) -> Vec<fs::DirEntry> {
16        let mut entries = Vec::new();
17        if let Ok(rd) = fs::read_dir(dir) {
18            for entry in rd.flatten() {
19                let path = entry.path();
20                if path.is_dir()
21                    && !path
22                        .file_name()
23                        .is_some_and(|n| n.to_string_lossy().starts_with('.'))
24                {
25                    entries.extend(walkdir(&path));
26                } else {
27                    entries.push(entry);
28                }
29            }
30        }
31        entries
32    }
33
34    if check("org") {
35        formats.push("org");
36    }
37    if check("tex") || check("latex") {
38        formats.push("latex");
39    }
40    if check("md") || check("markdown") {
41        formats.push("markdown");
42    }
43    formats
44}
45
46/// Generate .snapperrc.toml content.
47fn generate_config(formats: &[&str]) -> String {
48    let default_format = formats.first().copied().unwrap_or("plaintext");
49    format!(
50        r#"# snapper project configuration
51# https://snapper.turtletech.us/docs/reference/config/
52
53# Extra abbreviations (merged with built-in list)
54# extra_abbreviations = ["GROMACS", "LAMMPS", "DFT"]
55
56# File patterns to ignore
57# ignore = ["*.bib", "*.cls", "*.sty"]
58
59# Default format (auto-detected from extension if omitted)
60format = "{default_format}"
61
62# Maximum line width (0 = unlimited)
63max_width = 0
64"#
65    )
66}
67
68/// Generate .gitattributes entries.
69fn generate_gitattributes(formats: &[&str]) -> String {
70    let mut lines = String::from("# snapper semantic line break filter\n");
71    for fmt in formats {
72        let ext = match *fmt {
73            "org" => "*.org",
74            "latex" => "*.tex",
75            "markdown" => "*.md",
76            _ => continue,
77        };
78        lines.push_str(&format!("{ext} filter=snapper\n"));
79    }
80    lines
81}
82
83/// Generate pre-commit config snippet.
84fn generate_precommit() -> &'static str {
85    r#"# Add to .pre-commit-config.yaml:
86- repo: https://github.com/TurtleTech-ehf/snapper
87  rev: v0.1.0
88  hooks:
89    - id: snapper
90"#
91}
92
93/// Generate Apheleia elisp snippet.
94fn generate_apheleia(formats: &[&str]) -> String {
95    let mut s = String::from(";; Add to your Emacs config:\n(with-eval-after-load 'apheleia\n");
96    s.push_str("  (push '(snapper . (\"snapper\")) apheleia-formatters)\n");
97    for fmt in formats {
98        let mode = match *fmt {
99            "org" => "org-mode",
100            "latex" => "latex-mode",
101            "markdown" => "markdown-mode",
102            _ => continue,
103        };
104        s.push_str(&format!(
105            "  (push '({mode} . snapper) apheleia-mode-alist)\n"
106        ));
107    }
108    s.push_str(")\n");
109    s
110}
111
112/// Run the init command.
113pub fn run_init(dry_run: bool) -> Result<()> {
114    let cwd = std::env::current_dir()?;
115    let formats = detect_formats(&cwd);
116
117    eprintln!(
118        "Detected formats: {}",
119        if formats.is_empty() {
120            "none (will use plaintext defaults)".to_string()
121        } else {
122            formats.join(", ")
123        }
124    );
125
126    // .snapperrc.toml
127    let config_content = generate_config(&formats);
128    let config_path = cwd.join(".snapperrc.toml");
129    if config_path.exists() {
130        eprintln!("  .snapperrc.toml already exists, skipping");
131    } else if dry_run {
132        eprintln!("\n--- .snapperrc.toml ---");
133        eprint!("{config_content}");
134    } else {
135        fs::write(&config_path, &config_content).context("failed to write .snapperrc.toml")?;
136        eprintln!("  Created .snapperrc.toml");
137    }
138
139    // .gitattributes
140    if !formats.is_empty() {
141        let ga_content = generate_gitattributes(&formats);
142        let ga_path = cwd.join(".gitattributes");
143        if dry_run {
144            eprintln!("\n--- .gitattributes (append) ---");
145            eprint!("{ga_content}");
146        } else if ga_path.exists() {
147            let existing = fs::read_to_string(&ga_path)?;
148            if !existing.contains("filter=snapper") {
149                fs::write(&ga_path, format!("{existing}\n{ga_content}"))
150                    .context("failed to append .gitattributes")?;
151                eprintln!("  Appended to .gitattributes");
152            } else {
153                eprintln!("  .gitattributes already has snapper filter, skipping");
154            }
155        } else {
156            fs::write(&ga_path, &ga_content).context("failed to write .gitattributes")?;
157            eprintln!("  Created .gitattributes");
158        }
159    }
160
161    // Print pre-commit and Apheleia snippets
162    eprintln!("\n{}", generate_precommit());
163    eprintln!("{}", generate_apheleia(&formats));
164
165    // Git filter setup reminder
166    eprintln!("To enable the git smudge/clean filter, run:");
167    eprintln!("  git config filter.snapper.clean \"snapper\"");
168    eprintln!("  git config filter.snapper.smudge cat");
169
170    Ok(())
171}