ent_tree/
fs.rs

1use std::path::{Path, PathBuf};
2use ignore::WalkBuilder;
3use anyhow::{bail, Result};
4use serde::Serialize;
5use crate::cli::{Cli, Format};
6
7#[derive(Debug, Serialize, Clone)]
8pub struct TreeEntry {
9    pub name: String,
10    pub is_dir: bool,
11    pub children: Option<Vec<TreeEntry>>,
12}
13
14impl TreeEntry {
15    pub fn new(name: String, is_dir: bool) -> Self {
16        Self {
17            name,
18            is_dir,
19            children: None,
20        }
21    }
22    pub fn new_file(name: String) -> Self {
23        Self::new(name, false)
24    }
25    pub fn new_dir(name: String) -> Self {
26        let mut new = Self::new(name, true);
27        new.children = Some(Vec::new());
28        new
29    }
30
31    pub fn build(cli: &Cli, path: &str) -> Result<TreeEntry> {
32        let path = PathBuf::from(path);
33        if !path.exists() {
34            bail!("Path does not exist: {}", path.display());
35        }
36
37        let root_name = path.file_name()
38            .map(|s| s.to_string_lossy().into_owned())
39            .unwrap_or_else(|| ".".to_string());
40
41        let mut root = TreeEntry::new_dir(root_name);
42        let depth = 1;
43        Self::build_recursive(cli, &path, &mut root, depth)?;
44        Ok(root)
45
46    }
47
48    fn build_recursive(cli: &Cli, path: &Path, parent: &mut TreeEntry, depth: usize) -> Result<()> {
49        let show_hidden = cli.hidden || cli.all;
50        let show_ignored = cli.ignored || cli.all;
51        let show_dirs_only = cli.dirs_only;
52        let show_files_only = cli.files_only;
53
54        let walker = WalkBuilder::new(path)
55            .hidden(!show_hidden)
56            .git_ignore(!show_ignored)
57            .git_exclude(!show_ignored)
58            .parents(true)
59            .max_depth(Some(1))
60            .build();
61
62        match &cli.depth {
63            Some(d) if d < &depth => return Ok(()),
64            _ => (),
65        }
66
67        for result in walker {
68            let entry = match result {
69                Ok(entry) => entry,
70                Err(e) => {
71                    eprintln!("Error walking directory: {}", e);
72                    continue;
73                }
74            };
75            if entry.path() == path {
76                continue;
77            }
78            let path = entry.path();
79            let name = path.file_name()
80                .map(|s| s.to_string_lossy().into_owned())
81                .unwrap_or_default();
82
83
84            let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
85
86            if show_dirs_only && !is_dir {
87                continue;
88            }
89            else if show_files_only && is_dir {
90                continue;
91            }
92
93            let mut child: TreeEntry = if is_dir {
94                TreeEntry::new_dir(name)
95            }
96            else {
97                TreeEntry::new_file(name)
98            };
99
100            if is_dir {
101                let new_depth = depth + 1;
102                Self::build_recursive(cli, &path, &mut child, new_depth)?;
103            }
104
105            if let Some(children) = &mut parent.children {
106                children.push(child);
107            }
108        }
109
110        Ok(())
111    }
112
113    pub fn export<P: AsRef<Path>>(&self, path: P, format: Format) -> Result<()> {
114        match format {
115            Format::Json => {
116                let export_str = serde_json::to_string_pretty(self)?;
117                std::fs::write(path, export_str)?;
118            },
119        }
120        Ok(())
121    }
122}
123
124pub fn is_hidden(path: &Path) -> bool {
125    path.file_name()
126        .map(|s| s.to_string_lossy().starts_with('.'))
127        .unwrap_or(false)
128}
129
130
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use tempfile::tempdir;
136    use std::fs::{create_dir, File};
137    use std::io::Write;
138
139    #[test]
140    fn test_new_file() {
141        let file = TreeEntry::new_file("test.txt".to_string());
142        assert_eq!(file.name, "test.txt");
143        assert!(!file.is_dir);
144        assert!(file.children.is_none());
145    }
146
147    #[test]
148    fn test_new_dir() {
149        let dir = TreeEntry::new_dir("src".to_string());
150        assert_eq!(dir.name, "src");
151        assert!(dir.is_dir);
152        assert!(dir.children.is_some());
153    }
154
155    #[test]
156    fn test_is_hidden() {
157        assert!(is_hidden(&PathBuf::from(".hidden")));
158        assert!(!is_hidden(&PathBuf::from("visible")));
159    }
160
161    #[test]
162    fn test_build_simple_tree() -> Result<()> {
163        let dir = tempdir()?;
164        let root_path = dir.path();
165
166        // Create structure:
167        // root/
168        // ├── file.txt
169        // └── subdir/
170        //     └── nested.txt
171        File::create(root_path.join("file.txt"))?.write_all(b"Hello")?;
172        create_dir(root_path.join("subdir"))?;
173        File::create(root_path.join("subdir/nested.txt"))?;
174
175        let cli = Cli {
176            hidden: false,
177            all: false,
178            ignored: false,
179            depth: None,
180            dirs_only: false,
181            files_only: false,
182            ..Default::default()
183        };
184
185        let tree = TreeEntry::build(&cli, root_path.to_str().unwrap())?;
186        assert_eq!(tree.is_dir, true);
187        assert!(tree.children.as_ref().unwrap().iter().any(|e| e.name == "file.txt"));
188        assert!(tree.children.as_ref().unwrap().iter().any(|e| e.name == "subdir"));
189
190        Ok(())
191    }
192
193    #[test]
194    fn test_export_json() -> Result<()> {
195        let tree = TreeEntry::new_dir("project".to_string());
196        let tmp_file = tempfile::NamedTempFile::new()?;
197
198        tree.export(tmp_file.path(), Format::Json)?;
199        let contents = std::fs::read_to_string(tmp_file.path())?;
200        assert!(contents.contains("\"name\": \"project\""));
201        Ok(())
202    }
203}
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222