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 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