use std::fs;
use std::path::Path;
use super::workspace::{Workspace, io_problem};
pub(super) const MAX_ENTRIES: usize = 500;
pub(super) const MAX_DEPTH: i64 = 3;
const SKIPPED: [&str; 3] = [".git", "target", "node_modules"];
struct Entry {
level: usize,
name: String,
size: Option<u64>,
}
impl Entry {
fn column(&self) -> usize {
self.level * 2 + self.name.chars().count()
}
}
pub(super) fn list(workspace: &Workspace, path: &str, depth: Option<i64>) -> Result<String, String> {
let depth = match depth {
None => 1,
Some(depth) if (1..=MAX_DEPTH).contains(&depth) => depth as usize,
Some(depth) => {
return Err(format!("A depth of {depth} is not one; `list_dir` takes a depth from 1 to {MAX_DEPTH}."));
}
};
let real = workspace.resolve(path)?;
let shown = workspace.relative(&real);
if real.is_file() {
return Err(format!("`{shown}` is a file; use read_file to read it."));
}
let mut tree = Vec::new();
let (mut more, mut seen) = (0, 0);
walk(workspace.root(), &real, 0, depth, &mut tree, &mut more, &mut seen)
.map_err(|error| io_problem(&shown, &error))?;
if seen == 0 {
return Ok(format!("`{shown}` is empty."));
}
let sized = depth > 1;
let width =
if sized { tree.iter().filter(|entry| entry.size.is_some()).map(Entry::column).max().unwrap_or(0) } else { 0 };
let mut out: Vec<String> = tree
.iter()
.map(|entry| {
let indent = " ".repeat(entry.level);
match (sized, entry.size) {
(true, Some(size)) => {
let pad = " ".repeat(width - entry.column());
format!("{indent}{}{pad} {}", entry.name, size_of(size))
}
_ => format!("{indent}{}", entry.name),
}
})
.collect();
if more > 0 {
out.push(format!("(… and {more} more entries not shown.)"));
}
Ok(out.join("\n"))
}
fn walk(
root: &Path,
folder: &Path,
level: usize,
left: usize,
tree: &mut Vec<Entry>,
more: &mut usize,
seen: &mut usize,
) -> std::io::Result<()> {
let entries = fs::read_dir(folder)?;
let mut items: Vec<(bool, String, bool, Option<u64>)> = entries
.filter_map(Result::ok)
.map(|entry| {
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
let link = fs::symlink_metadata(&path).is_ok_and(|meta| meta.file_type().is_symlink());
if link && !fs::canonicalize(&path).is_ok_and(|real| real.starts_with(root)) {
return (false, format!("{name} (a link out of the working folder)"), false, None);
}
let meta = fs::metadata(&path).ok();
let is_dir = meta.as_ref().is_some_and(std::fs::Metadata::is_dir);
let enterable = !link && is_dir;
(is_dir, name, enterable, meta.map(|meta| meta.len()))
})
.collect();
items.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
for (is_dir, name, enterable, size) in items {
*seen += 1;
if tree.len() == MAX_ENTRIES {
*more += 1;
continue;
}
let size = if is_dir { None } else { size };
let below = (enterable && left > 1 && !SKIPPED.contains(&name.as_str())).then(|| folder.join(&name));
let shown = if is_dir { format!("{name}/") } else { name };
tree.push(Entry { level, name: shown, size });
if let Some(child) = below {
let _ = walk(root, &child, level + 1, left - 1, tree, more, seen);
}
}
Ok(())
}
fn size_of(bytes: u64) -> String {
const KB: f64 = 1024.0;
let size = bytes as f64;
match size {
s if s < KB => format!("{bytes} B"),
s if s < KB * KB => format!("{:.1} KB", s / KB),
s if s < KB * KB * KB => format!("{:.1} MB", s / KB / KB),
s => format!("{:.1} GB", s / KB / KB / KB),
}
}