| 213 | } |
| 214 | |
| 215 | fn render_dir( |
| 216 | dir_path: &str, |
| 217 | depth: usize, |
| 218 | dirs: &HashSet<String>, |
| 219 | files_by_dir: &HashMap<String, Vec<String>>, |
| 220 | ) -> String { |
| 221 | let indent = " ".repeat(depth); |
| 222 | let mut output = String::new(); |
| 223 | |
| 224 | if depth > 0 { |
| 225 | let name = Path::new(dir_path) |
| 226 | .file_name() |
| 227 | .map(|n| n.to_string_lossy().to_string()) |
| 228 | .unwrap_or_else(|| dir_path.to_string()); |
| 229 | output.push_str(&format!("{}{}/\n", indent, name)); |
| 230 | } |
| 231 | |
| 232 | let child_indent = " ".repeat(depth + 1); |
| 233 | |
| 234 | let parent_prefix = if dir_path == "." { |
| 235 | String::new() |
| 236 | } else { |
| 237 | format!("{}/", dir_path) |
| 238 | }; |
| 239 | |
| 240 | let mut children: Vec<String> = dirs |
| 241 | .iter() |
| 242 | .filter(|d| { |
| 243 | let d_str = d.as_str(); |
| 244 | if d_str == dir_path { |
| 245 | return false; |
| 246 | } |
| 247 | if dir_path == "." { |
| 248 | !d_str.contains('/') |
| 249 | } else { |
| 250 | d_str.starts_with(&parent_prefix) |
| 251 | && d_str[parent_prefix.len()..].matches('/').count() == 0 |
| 252 | } |
| 253 | }) |
| 254 | .cloned() |
| 255 | .collect(); |
| 256 | children.sort(); |
| 257 | |
| 258 | for child in &children { |
| 259 | output.push_str(&render_dir(child, depth + 1, dirs, files_by_dir)); |
| 260 | } |
| 261 | |
| 262 | let mut files = files_by_dir.get(dir_path).cloned().unwrap_or_default(); |
| 263 | files.sort(); |
| 264 | for file in files { |
| 265 | output.push_str(&format!("{}{}\n", child_indent, file)); |
| 266 | } |
| 267 | |
| 268 | output |
| 269 | } |
| 270 | |
| 271 | let output = format!( |
| 272 | "{}/\n{}", |