use std::collections::BTreeMap;
use crate::check::{CheckReport, DiskMap};
pub struct TreeOpts {
pub ascii: bool,
pub depth: Option<usize>,
}
pub fn render(report: &CheckReport, opts: &TreeOpts) -> String {
let maps = &report.maps;
if maps.is_empty() {
return "no maps - run `radar map` first\n".to_string();
}
let mut kids: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for scope in maps.keys() {
if scope.is_empty() {
continue;
}
let parent = nearest_ancestor(maps, scope).unwrap_or("");
kids.entry(parent).or_default().push(scope);
}
let mut out = String::new();
let name_width = maps
.keys()
.map(|s| display_name(s, maps).len() + s.matches('/').count() * 4)
.max()
.unwrap_or(8)
.max(8);
render_node(
report, &kids, "", "", true, true, opts, name_width, 0, &mut out,
);
out
}
fn display_name<'a>(scope: &'a str, maps: &BTreeMap<String, DiskMap>) -> &'a str {
if scope.is_empty() {
return ".";
}
match nearest_ancestor(maps, scope) {
Some(p) if !p.is_empty() => scope
.strip_prefix(p)
.map_or(scope, |s| s.trim_start_matches('/')),
_ => scope,
}
}
fn nearest_ancestor<'m>(maps: &'m BTreeMap<String, DiskMap>, scope: &str) -> Option<&'m str> {
let mut cur = scope;
loop {
let parent = match cur.rfind('/') {
Some(i) => &cur[..i],
None if !cur.is_empty() => "",
None => return None,
};
if let Some((k, _)) = maps.get_key_value(parent) {
return Some(k.as_str());
}
if parent.is_empty() {
return None;
}
cur = parent;
}
}
#[allow(clippy::too_many_arguments)]
fn render_node(
report: &CheckReport,
kids: &BTreeMap<&str, Vec<&str>>,
scope: &str,
prefix: &str,
is_last: bool,
is_root: bool,
opts: &TreeOpts,
name_width: usize,
depth: usize,
out: &mut String,
) {
let Some(map) = report.maps.get(scope) else {
return;
};
let (tee, ell, pipe) = if opts.ascii {
("|-- ", "`-- ", "| ")
} else {
("├── ", "└── ", "│ ")
};
let connector = if is_root {
""
} else if is_last {
ell
} else {
tee
};
let name = display_name(scope, &report.maps);
let stale = report.stale.get(scope).copied().unwrap_or(false);
let marker = match (stale, opts.ascii) {
(true, false) => "✗ stale",
(true, true) => "x stale",
(false, false) => "✓",
(false, true) => "ok",
};
let tokens = map.tokens.as_deref().unwrap_or("?");
let label = format!("{prefix}{connector}{name}");
let pad = name_width.saturating_sub(label.chars().count()) + 2;
out.push_str(&format!(
"{label}{:pad$}{:<8} {:>7} {marker}\n",
"", map.fidelity, tokens,
));
if opts.depth.is_some_and(|d| depth + 1 > d) {
return;
}
let children = kids.get(scope).cloned().unwrap_or_default();
let child_prefix = if is_root {
String::new()
} else if is_last {
format!("{prefix} ")
} else {
format!("{prefix}{pipe}")
};
for (i, child) in children.iter().enumerate() {
let last = i + 1 == children.len();
render_node(
report,
kids,
child,
&child_prefix,
last,
false,
opts,
name_width,
depth + 1,
out,
);
}
}