1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use walkdir::WalkDir;
5
6#[derive(Debug, Clone)]
7pub struct FileEntry {
8 pub path: PathBuf,
9 pub depth: usize,
10 pub is_dir: bool,
11}
12
13impl FileEntry {
14 pub fn name(&self) -> &str {
15 self.path.file_name().and_then(|n| n.to_str()).unwrap_or("")
16 }
17}
18
19#[derive(Debug, Clone)]
20pub struct FileTree {
21 pub root: PathBuf,
22 pub entries: Vec<FileEntry>,
23}
24
25impl FileTree {
26 pub fn from_dir(root: &Path) -> Result<Self> {
27 let root = root.canonicalize()?;
28 let mut entries = Vec::new();
29
30 for entry in WalkDir::new(&root).sort_by_file_name().min_depth(1) {
31 let entry = entry?;
32 let depth = entry.depth() - 1;
33 entries.push(FileEntry {
34 path: entry.path().to_path_buf(),
35 depth,
36 is_dir: entry.file_type().is_dir(),
37 });
38 }
39
40 Ok(Self { root, entries })
41 }
42
43 pub fn files_only(&self) -> impl Iterator<Item = &FileEntry> {
44 self.entries.iter().filter(|e| !e.is_dir)
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51 use std::fs;
52 use tempfile::TempDir;
53
54 #[test]
55 fn scan_directory() {
56 let tmp = TempDir::new().unwrap();
57 fs::create_dir(tmp.path().join("sub")).unwrap();
58 fs::write(tmp.path().join("file.txt"), "content").unwrap();
59 fs::write(tmp.path().join("sub/nested.rs"), "fn main() {}").unwrap();
60 fs::write(tmp.path().join(".hidden"), "secret").unwrap();
61
62 let tree = FileTree::from_dir(tmp.path()).unwrap();
63 let names: Vec<&str> = tree.entries.iter().map(|e| e.name()).collect();
64 assert!(names.contains(&"file.txt"));
65 assert!(names.contains(&"sub"));
66 assert!(names.contains(&"nested.rs"));
67 assert!(names.contains(&".hidden"));
68 }
69}