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