tree_rust/
tree.rs

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/// Represents a single entry in the directory tree
10#[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    /// Get file size in bytes
54    pub fn size(&self) -> u64 {
55        self.metadata.as_ref().map(|m| m.len()).unwrap_or(0)
56    }
57
58    /// Get modification time
59    pub fn modified(&self) -> Option<SystemTime> {
60        self.metadata.as_ref().and_then(|m| m.modified().ok())
61    }
62
63    /// Get file permissions as a string (e.g., "drwxr-xr-x")
64    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    /// Check if this is an executable file
87    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    /// Get the type indicator character (like ls -F)
98    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/// Configuration for tree traversal
129#[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/// Statistics collected during tree traversal
159#[derive(Debug, Default)]
160pub struct TreeStats {
161    pub directories: usize,
162    pub files: usize,
163}
164
165/// Walk a directory and build a tree structure
166pub 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    // Check depth limit
175    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    // Read directory contents
186    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        // Skip hidden files unless -a is specified
204        if !config.show_hidden && child_name.starts_with('.') {
205            continue;
206        }
207
208        let child_is_dir = child_path.is_dir();
209
210        // Skip files if dirs_only
211        if config.dirs_only && !child_is_dir {
212            continue;
213        }
214
215        // Apply filters
216        if !config.filter.matches(&child_name, child_is_dir) {
217            continue;
218        }
219
220        // Recursively walk subdirectories
221        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    // Sort children
233    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}