lx_cli/formatter/
recursive.rs

1use crate::config::Config;
2use crate::file_entry::FileEntry;
3use crate::formatter::long::{calculate_column_widths, print_long_entries_with_widths};
4use crate::sort::sort_default;
5use colored::Colorize;
6use std::fs;
7use std::os::unix::fs::PermissionsExt;
8use std::path::Path;
9
10pub fn format_recursive(path: &Path, config: &Config, show_hidden: bool, use_long_format: bool) {
11    let recursive_long_style = &config.display.tree.recursive_long_format;
12    // Print the root directory as the parent
13    if let Some(dir_name) = path.file_name() {
14        let dir_name_str = dir_name.to_string_lossy();
15
16        // Create a temporary FileEntry just for getting the directory icon
17        let temp_entry = FileEntry {
18            path: dir_name.to_os_string(),
19            is_dir: true,
20            is_executable: false,
21            mode: 0o755,
22            size: 0,
23            modified: std::time::SystemTime::UNIX_EPOCH,
24            owner: String::new(),
25            group: String::new(),
26            nlink: 0,
27        };
28
29        let icon = temp_entry.get_icon_custom(&config.icons);
30        let icon_colored = icon.color(temp_entry.get_icon_color(&config.icons.colors));
31        let dir_name_colored = dir_name_str
32            .color(temp_entry.get_color(&config.colors))
33            .bold();
34
35        // Only add space if icon is not empty
36        if icon.is_empty() {
37            println!("{}", dir_name_colored);
38        } else {
39            println!("{} {}", icon_colored, dir_name_colored);
40        }
41    }
42
43    // Print the tree contents
44    print_directory_tree(
45        path,
46        config,
47        show_hidden,
48        "",
49        &config.display.tree.style,
50        use_long_format,
51        recursive_long_style.as_str(),
52    );
53}
54
55fn print_directory_tree(
56    path: &Path,
57    config: &Config,
58    show_hidden: bool,
59    prefix: &str,
60    tree_style: &str,
61    use_long_format: bool,
62    recursive_long_style: &str,
63) {
64    match fs::read_dir(path) {
65        Ok(entries_iter) => {
66            let mut entries: Vec<_> = entries_iter.filter_map(|e| e.ok()).collect();
67
68            // Sort entries by filename
69            entries.sort_by(|a, b| {
70                let a_name = a.file_name();
71                let b_name = b.file_name();
72                a_name.cmp(&b_name)
73            });
74
75            // Filter out hidden files if needed
76            let entries: Vec<_> = entries
77                .into_iter()
78                .filter(|e| {
79                    if show_hidden {
80                        true
81                    } else {
82                        let file_name = e.file_name();
83                        let name_str = file_name.to_string_lossy();
84                        !name_str.starts_with('.')
85                    }
86                })
87                .collect();
88
89            if use_long_format {
90                if recursive_long_style == "header" {
91                    // Header-style output (list format with directory paths as headers)
92                    print_long_format_with_headers(
93                        &entries,
94                        path,
95                        config,
96                        prefix,
97                        show_hidden,
98                        tree_style,
99                        recursive_long_style,
100                    );
101                } else {
102                    // Nested-style output (inline indentation for subdirectories)
103                    // Collect FileEntry objects for long format, separating files and dirs
104                    let mut file_entries: Vec<(FileEntry, std::path::PathBuf)> = Vec::new();
105
106                    for entry in entries.iter() {
107                        let entry_path = entry.path();
108                        let file_name = entry.file_name();
109                        if let Ok(metadata) = entry.metadata() {
110                            let is_dir = metadata.is_dir();
111                            let is_executable =
112                                !is_dir && (metadata.permissions().mode() & 0o111) != 0;
113
114                            let file_entry = FileEntry {
115                                path: file_name.clone(),
116                                is_dir,
117                                is_executable,
118                                mode: metadata.permissions().mode(),
119                                size: metadata.len(),
120                                modified: metadata
121                                    .modified()
122                                    .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
123                                owner: String::new(),
124                                group: String::new(),
125                                nlink: 0,
126                            };
127
128                            file_entries.push((file_entry, entry_path));
129                        }
130                    }
131
132                    // Calculate widths once for all entries at this level
133                    let mut entries_only: Vec<FileEntry> =
134                        file_entries.iter().map(|(e, _)| e.clone()).collect();
135                    // Apply default sorting: by type, then alphabetically (case-insensitive)
136                    sort_default(&mut entries_only);
137                    let fields = &config.display.long_format_fields;
138                    let widths = calculate_column_widths(&entries_only, fields);
139
140                    // Print each entry and recurse into directories immediately after
141                    for entry in &entries_only {
142                        // Find the corresponding path from the original file_entries
143                        let entry_path = file_entries
144                            .iter()
145                            .find(|(e, _)| e.path.to_string_lossy() == entry.path.to_string_lossy())
146                            .map(|(_, p)| p.clone())
147                            .unwrap_or_else(|| std::path::PathBuf::new());
148                        // Print this entry using pre-calculated widths
149                        let single_entry = vec![entry.clone()];
150                        print_long_entries_with_widths(
151                            &single_entry,
152                            config,
153                            prefix,
154                            fields,
155                            &widths,
156                        );
157
158                        // If it's a directory, recurse into it immediately
159                        if entry.is_dir {
160                            let new_prefix = format!("{}    ", prefix);
161                            print_directory_tree(
162                                &entry_path,
163                                config,
164                                show_hidden,
165                                &new_prefix,
166                                tree_style,
167                                use_long_format,
168                                recursive_long_style,
169                            );
170                        }
171                    }
172                }
173            } else {
174                // Tree-style output (original behavior)
175                for (idx, entry) in entries.iter().enumerate() {
176                    let is_last = idx == entries.len() - 1;
177                    let entry_path = entry.path();
178                    let file_name = entry.file_name();
179                    let file_name_str = file_name.to_string_lossy();
180
181                    // Get metadata
182                    if let Ok(metadata) = entry.metadata() {
183                        let is_dir = metadata.is_dir();
184                        let is_executable = !is_dir && (metadata.permissions().mode() & 0o111) != 0;
185
186                        // Create FileEntry for icon/color handling
187                        let file_entry = FileEntry {
188                            path: file_name.clone(),
189                            is_dir,
190                            is_executable,
191                            mode: metadata.permissions().mode(),
192                            size: metadata.len(),
193                            modified: metadata
194                                .modified()
195                                .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
196                            owner: String::new(),
197                            group: String::new(),
198                            nlink: 0,
199                        };
200
201                        // Determine tree connectors
202                        let (connector, extension_prefix) = if tree_style == "ascii" {
203                            if is_last {
204                                ("└──", "    ")
205                            } else {
206                                ("├──", "│   ")
207                            }
208                        } else {
209                            // indent style
210                            ("", "  ")
211                        };
212
213                        // Get icon and color
214                        let icon = file_entry.get_icon_custom(&config.icons);
215                        let icon_colored =
216                            icon.color(file_entry.get_icon_color(&config.icons.colors));
217
218                        // Get filename color
219                        let filename_colored = match file_entry.get_file_type() {
220                            crate::file_entry::FileType::Directory
221                            | crate::file_entry::FileType::Executable => file_name_str
222                                .color(file_entry.get_color(&config.colors))
223                                .bold(),
224                            crate::file_entry::FileType::RegularFile => {
225                                file_name_str.color(file_entry.get_color(&config.colors))
226                            }
227                        };
228
229                        // Print the entry
230                        println!(
231                            "{}{}{} {}",
232                            prefix, connector, icon_colored, filename_colored
233                        );
234
235                        // If it's a directory, recurse
236                        if is_dir {
237                            let new_prefix = if tree_style == "ascii" {
238                                format!("{}{}", prefix, extension_prefix)
239                            } else {
240                                format!("{}  ", prefix)
241                            };
242
243                            print_directory_tree(
244                                &entry_path,
245                                config,
246                                show_hidden,
247                                &new_prefix,
248                                tree_style,
249                                use_long_format,
250                                recursive_long_style,
251                            );
252                        }
253                    }
254                }
255            }
256        }
257        Err(_) => {}
258    }
259}
260
261fn print_long_format_with_headers(
262    entries: &[std::fs::DirEntry],
263    path: &std::path::Path,
264    config: &crate::config::Config,
265    prefix: &str,
266    show_hidden: bool,
267    tree_style: &str,
268    recursive_long_style: &str,
269) {
270    // Print directory header with path
271    if !prefix.is_empty() {
272        println!("{}:", path.display());
273    }
274
275    // Collect FileEntry objects for long format
276    let mut file_entries: Vec<(FileEntry, std::path::PathBuf)> = Vec::new();
277
278    for entry in entries.iter() {
279        let entry_path = entry.path();
280        let file_name = entry.file_name();
281        if let Ok(metadata) = entry.metadata() {
282            let is_dir = metadata.is_dir();
283            let is_executable = !is_dir && (metadata.permissions().mode() & 0o111) != 0;
284
285            let file_entry = FileEntry {
286                path: file_name.clone(),
287                is_dir,
288                is_executable,
289                mode: metadata.permissions().mode(),
290                size: metadata.len(),
291                modified: metadata
292                    .modified()
293                    .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
294                owner: String::new(),
295                group: String::new(),
296                nlink: 0,
297            };
298
299            file_entries.push((file_entry, entry_path));
300        }
301    }
302
303    // Use the new print_long_entries function for configurable field ordering
304    let mut file_entries_only: Vec<FileEntry> =
305        file_entries.iter().map(|(e, _)| e.clone()).collect();
306    if !file_entries_only.is_empty() {
307        // Apply default sorting: by type, then alphabetically (case-insensitive)
308        sort_default(&mut file_entries_only);
309        let fields = &config.display.long_format_fields;
310        let widths = calculate_column_widths(&file_entries_only, fields);
311        print_long_entries_with_widths(&file_entries_only, config, "", fields, &widths);
312    }
313
314    // Recurse into directories
315    for (entry, entry_path) in &file_entries {
316        if entry.is_dir {
317            let new_prefix = format!("{}    ", prefix);
318            print_directory_tree(
319                &entry_path,
320                config,
321                show_hidden,
322                &new_prefix,
323                tree_style,
324                true,
325                recursive_long_style,
326            );
327        }
328    }
329}