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 walker = WalkBuilder::new(path)
50 .hidden(!cli.hidden && !cli.all)
51 .git_ignore(!cli.ignored && !cli.all)
52 .git_exclude(!cli.ignored && !cli.all)
53 .parents(true)
54 .max_depth(Some(1))
55 .build();
56
57 match &cli.depth {
58 Some(d) if d < &depth => return Ok(()),
59 _ => (),
60 }
61
62 for result in walker {
63 let entry = match result {
64 Ok(entry) => entry,
65 Err(e) => {
66 eprintln!("Error walking directory: {}", e);
67 continue;
68 }
69 };
70 if entry.path() == path {
71 continue;
72 }
73 let path = entry.path();
74 let name = path.file_name()
75 .map(|s| s.to_string_lossy().into_owned())
76 .unwrap_or_default();
77
78 if !cli.hidden && is_hidden(&path) {
79 continue;
80 }
81
82 let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
83
84 if cli.dirs_only && !is_dir {
85 continue;
86 }
87 if cli.files_only && is_dir {
88 continue;
89 }
90 let mut child: TreeEntry = if is_dir {
91 TreeEntry::new_dir(name)
92 }
93 else {
94 TreeEntry::new_file(name)
95 };
96
97 if is_dir {
98 let new_depth = depth + 1;
99 Self::build_recursive(cli, &path, &mut child, new_depth)?;
100 }
101
102 if let Some(children) = &mut parent.children {
103 children.push(child);
104 }
105 }
106
107 Ok(())
108 }
109
110 pub fn export<P: AsRef<Path>>(&self, path: P, format: Format) -> Result<()> {
111 match format {
112 Format::Json => {
113 let export_str = serde_json::to_string_pretty(self)?;
114 std::fs::write(path, export_str)?;
115 },
116 }
117 Ok(())
118 }
119}
120
121pub fn is_hidden(path: &Path) -> bool {
122 path.file_name()
123 .map(|s| s.to_string_lossy().starts_with('.'))
124 .unwrap_or(false)
125}
126
127
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use tempfile::tempdir;
133 use std::fs::{create_dir, File};
134 use std::io::Write;
135
136 #[test]
137 fn test_new_file() {
138 let file = TreeEntry::new_file("test.txt".to_string());
139 assert_eq!(file.name, "test.txt");
140 assert!(!file.is_dir);
141 assert!(file.children.is_none());
142 }
143
144 #[test]
145 fn test_new_dir() {
146 let dir = TreeEntry::new_dir("src".to_string());
147 assert_eq!(dir.name, "src");
148 assert!(dir.is_dir);
149 assert!(dir.children.is_some());
150 }
151
152 #[test]
153 fn test_is_hidden() {
154 assert!(is_hidden(&PathBuf::from(".hidden")));
155 assert!(!is_hidden(&PathBuf::from("visible")));
156 }
157
158 #[test]
159 fn test_build_simple_tree() -> Result<()> {
160 let dir = tempdir()?;
161 let root_path = dir.path();
162
163 File::create(root_path.join("file.txt"))?.write_all(b"Hello")?;
169 create_dir(root_path.join("subdir"))?;
170 File::create(root_path.join("subdir/nested.txt"))?;
171
172 let cli = Cli {
173 hidden: false,
174 all: false,
175 ignored: false,
176 depth: None,
177 dirs_only: false,
178 files_only: false,
179 ..Default::default()
180 };
181
182 let tree = TreeEntry::build(&cli, root_path.to_str().unwrap())?;
183 assert_eq!(tree.is_dir, true);
184 assert!(tree.children.as_ref().unwrap().iter().any(|e| e.name == "file.txt"));
185 assert!(tree.children.as_ref().unwrap().iter().any(|e| e.name == "subdir"));
186
187 Ok(())
188 }
189
190 #[test]
191 fn test_export_json() -> Result<()> {
192 let tree = TreeEntry::new_dir("project".to_string());
193 let tmp_file = tempfile::NamedTempFile::new()?;
194
195 tree.export(tmp_file.path(), Format::Json)?;
196 let contents = std::fs::read_to_string(tmp_file.path())?;
197 assert!(contents.contains("\"name\": \"project\""));
198 Ok(())
199 }
200}
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219