1use std::fs::{self, Metadata};
2use std::os::unix::fs::PermissionsExt;
3use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5
6use crate::filter::Filter;
7use crate::sort::{SortKey, Sorter};
8
9#[derive(Debug, Clone)]
11pub struct TreeEntry {
12 pub path: PathBuf,
13 pub name: String,
14 pub is_dir: bool,
15 pub is_symlink: bool,
16 pub symlink_target: Option<PathBuf>,
17 pub metadata: Option<Metadata>,
18 pub children: Vec<TreeEntry>,
19 pub error: Option<String>,
20}
21
22impl TreeEntry {
23 pub fn new(path: PathBuf) -> Self {
24 let name = path
25 .file_name()
26 .map(|s| s.to_string_lossy().to_string())
27 .unwrap_or_else(|| path.to_string_lossy().to_string());
28
29 let symlink_meta = fs::symlink_metadata(&path).ok();
30 let is_symlink = symlink_meta.as_ref().map(|m| m.is_symlink()).unwrap_or(false);
31
32 let metadata = fs::metadata(&path).ok();
33 let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
34
35 let symlink_target = if is_symlink {
36 fs::read_link(&path).ok()
37 } else {
38 None
39 };
40
41 Self {
42 path,
43 name,
44 is_dir,
45 is_symlink,
46 symlink_target,
47 metadata,
48 children: Vec::new(),
49 error: None,
50 }
51 }
52
53 pub fn size(&self) -> u64 {
55 self.metadata.as_ref().map(|m| m.len()).unwrap_or(0)
56 }
57
58 pub fn modified(&self) -> Option<SystemTime> {
60 self.metadata.as_ref().and_then(|m| m.modified().ok())
61 }
62
63 pub fn permissions_string(&self) -> String {
65 let meta = match &self.metadata {
66 Some(m) => m,
67 None => return "----------".to_string(),
68 };
69
70 let mode = meta.permissions().mode();
71 let file_type = if self.is_dir {
72 'd'
73 } else if self.is_symlink {
74 'l'
75 } else {
76 '-'
77 };
78
79 let user = triplet((mode >> 6) & 0o7, mode & 0o4000 != 0, 's');
80 let group = triplet((mode >> 3) & 0o7, mode & 0o2000 != 0, 's');
81 let other = triplet(mode & 0o7, mode & 0o1000 != 0, 't');
82
83 format!("{}{}{}{}", file_type, user, group, other)
84 }
85
86 pub fn is_executable(&self) -> bool {
88 if self.is_dir {
89 return false;
90 }
91 self.metadata
92 .as_ref()
93 .map(|m| m.permissions().mode() & 0o111 != 0)
94 .unwrap_or(false)
95 }
96
97 pub fn type_indicator(&self) -> &'static str {
99 if self.is_dir {
100 "/"
101 } else if self.is_symlink {
102 "@"
103 } else if self.is_executable() {
104 "*"
105 } else {
106 ""
107 }
108 }
109}
110
111fn triplet(mode: u32, special: bool, special_char: char) -> String {
112 let r = if mode & 0o4 != 0 { 'r' } else { '-' };
113 let w = if mode & 0o2 != 0 { 'w' } else { '-' };
114 let x = if mode & 0o1 != 0 {
115 if special {
116 special_char
117 } else {
118 'x'
119 }
120 } else if special {
121 special_char.to_ascii_uppercase()
122 } else {
123 '-'
124 };
125 format!("{}{}{}", r, w, x)
126}
127
128#[derive(Debug, Clone)]
130pub struct TreeConfig {
131 pub show_hidden: bool,
132 pub dirs_only: bool,
133 pub max_depth: Option<usize>,
134 pub follow_symlinks: bool,
135 pub full_path: bool,
136 pub filter: Filter,
137 pub sort_key: SortKey,
138 pub sort_reverse: bool,
139 pub dirs_first: bool,
140}
141
142impl Default for TreeConfig {
143 fn default() -> Self {
144 Self {
145 show_hidden: false,
146 dirs_only: false,
147 max_depth: None,
148 follow_symlinks: false,
149 full_path: false,
150 filter: Filter::default(),
151 sort_key: SortKey::Name,
152 sort_reverse: false,
153 dirs_first: false,
154 }
155 }
156}
157
158#[derive(Debug, Default)]
160pub struct TreeStats {
161 pub directories: usize,
162 pub files: usize,
163}
164
165pub fn walk_directory(
167 path: &Path,
168 config: &TreeConfig,
169 stats: &mut TreeStats,
170 current_depth: usize,
171) -> TreeEntry {
172 let mut entry = TreeEntry::new(path.to_path_buf());
173
174 if let Some(max_depth) = config.max_depth {
176 if current_depth >= max_depth {
177 return entry;
178 }
179 }
180
181 if !entry.is_dir {
182 return entry;
183 }
184
185 let read_dir = match fs::read_dir(path) {
187 Ok(rd) => rd,
188 Err(e) => {
189 entry.error = Some(format!("error opening dir: {}", e));
190 return entry;
191 }
192 };
193
194 let mut children: Vec<TreeEntry> = Vec::new();
195
196 for dir_entry in read_dir.flatten() {
197 let child_path = dir_entry.path();
198 let child_name = child_path
199 .file_name()
200 .map(|s| s.to_string_lossy().to_string())
201 .unwrap_or_default();
202
203 if !config.show_hidden && child_name.starts_with('.') {
205 continue;
206 }
207
208 let child_is_dir = child_path.is_dir();
209
210 if config.dirs_only && !child_is_dir {
212 continue;
213 }
214
215 if !config.filter.matches(&child_name, child_is_dir) {
217 continue;
218 }
219
220 let child = walk_directory(&child_path, config, stats, current_depth + 1);
222
223 if child.is_dir {
224 stats.directories += 1;
225 } else {
226 stats.files += 1;
227 }
228
229 children.push(child);
230 }
231
232 let sorter = Sorter::new(config.sort_key.clone(), config.sort_reverse, config.dirs_first);
234 sorter.sort(&mut children);
235
236 entry.children = children;
237 entry
238}