rust_filesearch/output/
templates.rs

1#[cfg(feature = "templates")]
2use crate::errors::Result;
3#[cfg(feature = "templates")]
4use crate::models::Entry;
5#[cfg(feature = "templates")]
6use std::io::Write;
7
8#[cfg(feature = "templates")]
9/// Template format types
10pub enum TemplateFormat {
11    Markdown,
12    Html,
13}
14
15#[cfg(feature = "templates")]
16impl std::str::FromStr for TemplateFormat {
17    type Err = String;
18
19    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
20        match s.to_lowercase().as_str() {
21            "markdown" | "md" => Ok(TemplateFormat::Markdown),
22            "html" => Ok(TemplateFormat::Html),
23            _ => Err(format!("Unknown template format: {}", s)),
24        }
25    }
26}
27
28#[cfg(feature = "templates")]
29/// Export entries using a template format
30pub fn export_with_template<W: Write>(
31    writer: &mut W,
32    entries: &[Entry],
33    format: &TemplateFormat,
34    title: Option<&str>,
35) -> Result<()> {
36    match format {
37        TemplateFormat::Markdown => export_markdown(writer, entries, title),
38        TemplateFormat::Html => export_html(writer, entries, title),
39    }
40}
41
42#[cfg(feature = "templates")]
43/// Export as Markdown table
44fn export_markdown<W: Write>(writer: &mut W, entries: &[Entry], title: Option<&str>) -> Result<()> {
45    // Write title if provided
46    if let Some(title) = title {
47        writeln!(writer, "# {}\n", title)?;
48    }
49
50    // Calculate totals
51    let total_files = entries
52        .iter()
53        .filter(|e| e.kind == crate::models::EntryKind::File)
54        .count();
55    let total_size: u64 = entries
56        .iter()
57        .filter(|e| e.kind == crate::models::EntryKind::File)
58        .map(|e| e.size)
59        .sum();
60
61    writeln!(writer, "**Total Files:** {}  ", total_files)?;
62    writeln!(
63        writer,
64        "**Total Size:** {}  \n",
65        humansize::format_size(total_size, humansize::BINARY)
66    )?;
67
68    // Write table header
69    writeln!(writer, "| Path | Size | Modified | Type |")?;
70    writeln!(writer, "|------|------|----------|------|")?;
71
72    // Write entries
73    for entry in entries {
74        let size_str = if entry.kind == crate::models::EntryKind::File {
75            humansize::format_size(entry.size, humansize::BINARY)
76        } else {
77            "-".to_string()
78        };
79
80        let kind_str = format!("{:?}", entry.kind);
81        let mtime_str = entry.mtime.format("%Y-%m-%d %H:%M").to_string();
82
83        writeln!(
84            writer,
85            "| {} | {} | {} | {} |",
86            entry.path.display(),
87            size_str,
88            mtime_str,
89            kind_str
90        )?;
91    }
92
93    Ok(())
94}
95
96#[cfg(feature = "templates")]
97/// Export as HTML table
98fn export_html<W: Write>(writer: &mut W, entries: &[Entry], title: Option<&str>) -> Result<()> {
99    // Calculate totals
100    let total_files = entries
101        .iter()
102        .filter(|e| e.kind == crate::models::EntryKind::File)
103        .count();
104    let total_size: u64 = entries
105        .iter()
106        .filter(|e| e.kind == crate::models::EntryKind::File)
107        .map(|e| e.size)
108        .sum();
109
110    let title_text = title.unwrap_or("File Explorer Results");
111
112    // Write HTML header
113    writeln!(writer, "<!DOCTYPE html>")?;
114    writeln!(writer, "<html lang=\"en\">")?;
115    writeln!(writer, "<head>")?;
116    writeln!(writer, "    <meta charset=\"UTF-8\">")?;
117    writeln!(
118        writer,
119        "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
120    )?;
121    writeln!(writer, "    <title>{}</title>", title_text)?;
122    writeln!(writer, "    <style>")?;
123    writeln!(writer, "        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; background: #f5f5f5; }}")?;
124    writeln!(writer, "        .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}")?;
125    writeln!(writer, "        h1 {{ color: #333; margin-top: 0; }}")?;
126    writeln!(writer, "        .summary {{ background: #e8f4f8; padding: 15px; border-radius: 4px; margin-bottom: 20px; }}")?;
127    writeln!(writer, "        .summary strong {{ color: #0066cc; }}")?;
128    writeln!(
129        writer,
130        "        table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}"
131    )?;
132    writeln!(writer, "        th {{ background: #0066cc; color: white; padding: 12px; text-align: left; font-weight: 600; }}")?;
133    writeln!(
134        writer,
135        "        td {{ padding: 10px 12px; border-bottom: 1px solid #e0e0e0; }}"
136    )?;
137    writeln!(writer, "        tr:hover {{ background: #f8f8f8; }}")?;
138    writeln!(
139        writer,
140        "        .file-path {{ font-family: 'Monaco', 'Menlo', monospace; color: #0066cc; }}"
141    )?;
142    writeln!(writer, "        .dir {{ color: #666; font-weight: 600; }}")?;
143    writeln!(writer, "        .file {{ color: #333; }}")?;
144    writeln!(
145        writer,
146        "        .symlink {{ color: #8b4513; font-style: italic; }}"
147    )?;
148    writeln!(writer, "    </style>")?;
149    writeln!(writer, "</head>")?;
150    writeln!(writer, "<body>")?;
151    writeln!(writer, "    <div class=\"container\">")?;
152    writeln!(writer, "        <h1>{}</h1>", title_text)?;
153
154    // Write summary
155    writeln!(writer, "        <div class=\"summary\">")?;
156    writeln!(
157        writer,
158        "            <strong>Total Files:</strong> {} &nbsp;&nbsp;",
159        total_files
160    )?;
161    writeln!(
162        writer,
163        "            <strong>Total Size:</strong> {}",
164        humansize::format_size(total_size, humansize::BINARY)
165    )?;
166    writeln!(writer, "        </div>")?;
167
168    // Write table
169    writeln!(writer, "        <table>")?;
170    writeln!(writer, "            <thead>")?;
171    writeln!(writer, "                <tr>")?;
172    writeln!(writer, "                    <th>Path</th>")?;
173    writeln!(writer, "                    <th>Size</th>")?;
174    writeln!(writer, "                    <th>Modified</th>")?;
175    writeln!(writer, "                    <th>Type</th>")?;
176    writeln!(writer, "                </tr>")?;
177    writeln!(writer, "            </thead>")?;
178    writeln!(writer, "            <tbody>")?;
179
180    for entry in entries {
181        let size_str = if entry.kind == crate::models::EntryKind::File {
182            humansize::format_size(entry.size, humansize::BINARY)
183        } else {
184            "-".to_string()
185        };
186
187        let kind_class = match entry.kind {
188            crate::models::EntryKind::Dir => "dir",
189            crate::models::EntryKind::File => "file",
190            crate::models::EntryKind::Symlink => "symlink",
191        };
192
193        let kind_str = format!("{:?}", entry.kind);
194        let mtime_str = entry.mtime.format("%Y-%m-%d %H:%M").to_string();
195
196        writeln!(writer, "                <tr>")?;
197        writeln!(
198            writer,
199            "                    <td class=\"file-path {}\">{}</td>",
200            kind_class,
201            entry.path.display()
202        )?;
203        writeln!(writer, "                    <td>{}</td>", size_str)?;
204        writeln!(writer, "                    <td>{}</td>", mtime_str)?;
205        writeln!(writer, "                    <td>{}</td>", kind_str)?;
206        writeln!(writer, "                </tr>")?;
207    }
208
209    writeln!(writer, "            </tbody>")?;
210    writeln!(writer, "        </table>")?;
211    writeln!(writer, "    </div>")?;
212    writeln!(writer, "</body>")?;
213    writeln!(writer, "</html>")?;
214
215    Ok(())
216}
217
218#[cfg(test)]
219#[cfg(feature = "templates")]
220mod tests {
221    use super::*;
222    use crate::models::EntryKind;
223    use chrono::Utc;
224    use std::path::PathBuf;
225
226    fn make_test_entry(name: &str, size: u64, kind: EntryKind) -> Entry {
227        Entry {
228            path: PathBuf::from(name),
229            name: name.to_string(),
230            size,
231            kind,
232            mtime: Utc::now(),
233            perms: None,
234            owner: None,
235            depth: 0,
236        }
237    }
238
239    #[test]
240    fn test_markdown_export() {
241        let entries = vec![
242            make_test_entry("file1.txt", 100, EntryKind::File),
243            make_test_entry("file2.txt", 200, EntryKind::File),
244        ];
245
246        let mut output = Vec::new();
247        export_markdown(&mut output, &entries, Some("Test Report")).unwrap();
248        let output_str = String::from_utf8(output).unwrap();
249
250        assert!(output_str.contains("# Test Report"));
251        assert!(output_str.contains("| Path | Size | Modified | Type |"));
252        assert!(output_str.contains("file1.txt"));
253        assert!(output_str.contains("file2.txt"));
254    }
255
256    #[test]
257    fn test_html_export() {
258        let entries = vec![make_test_entry("file1.txt", 100, EntryKind::File)];
259
260        let mut output = Vec::new();
261        export_html(&mut output, &entries, Some("Test Report")).unwrap();
262        let output_str = String::from_utf8(output).unwrap();
263
264        assert!(output_str.contains("<!DOCTYPE html>"));
265        assert!(output_str.contains("<title>Test Report</title>"));
266        assert!(output_str.contains("file1.txt"));
267    }
268}