use f00_core::{Entry, IndicatorStyle};
use crate::color::Colorizer;
use crate::icons::icon_prefix;
use crate::perms::classify_suffix;
pub fn format_tree(
entries: &[Entry],
colorizer: &Colorizer,
icons: bool,
indicator: IndicatorStyle,
) -> String {
let items: Vec<&Entry> = entries.iter().filter(|e| !e.is_dir_header).collect();
if items.is_empty() {
return String::new();
}
let use_depth = items.iter().any(|e| e.depth > 0);
let mut out = String::with_capacity(items.len().saturating_mul(48));
if !use_depth {
for (i, entry) in items.iter().enumerate() {
let last = i + 1 == items.len();
out.push_str(if last { "└── " } else { "├── " });
push_name(&mut out, entry, colorizer, icons, indicator);
out.push('\n');
}
return out;
}
format_tree_by_depth(&items, colorizer, icons, indicator, &mut out);
out
}
fn push_name(
out: &mut String,
entry: &Entry,
colorizer: &Colorizer,
icons: bool,
indicator: IndicatorStyle,
) {
let icon = icon_prefix(entry, icons);
let suffix = classify_suffix(entry, indicator);
let plain = format!("{icon}{}{suffix}", entry.name);
out.push_str(&colorizer.paint_name(entry, &plain));
}
fn format_tree_by_depth(
items: &[&Entry],
colorizer: &Colorizer,
icons: bool,
indicator: IndicatorStyle,
out: &mut String,
) {
let n = items.len();
let depths: Vec<usize> = items.iter().map(|e| e.depth.max(1)).collect();
let is_last = precompute_is_last(&depths);
let mut stack: Vec<usize> = Vec::with_capacity(16);
for i in 0..n {
let d = depths[i];
while !stack.is_empty() && depths[*stack.last().unwrap()] >= d {
stack.pop();
}
for &anc in &stack {
if is_last[anc] {
out.push_str(" ");
} else {
out.push_str("│ ");
}
}
out.push_str(if is_last[i] {
"└── "
} else {
"├── "
});
push_name(out, items[i], colorizer, icons, indicator);
out.push('\n');
stack.push(i);
}
}
fn precompute_is_last(depths: &[usize]) -> Vec<bool> {
let n = depths.len();
let mut is_last = vec![true; n];
if n == 0 {
return is_last;
}
let max_d = depths.iter().copied().max().unwrap_or(1);
let mut last_at: Vec<Option<usize>> = vec![None; max_d + 2];
for (i, &d) in depths.iter().enumerate() {
for slot in last_at.iter_mut().skip(d + 1) {
*slot = None;
}
if let Some(prev) = last_at[d] {
is_last[prev] = false;
}
last_at[d] = Some(i);
}
is_last
}
#[cfg(test)]
mod tests {
use super::*;
use f00_core::{Entry, EntryKind, GitStatus};
use std::path::PathBuf;
fn e_path(path: &str, depth: usize) -> Entry {
let p = PathBuf::from(path);
let name = p.file_name().unwrap().to_string_lossy().into_owned();
Entry {
path: p,
name,
kind: EntryKind::File,
size: 0,
modified: None,
created: None,
accessed: None,
changed: None,
mode: 0o644,
readonly: false,
symlink_target: None,
depth,
git_status: GitStatus::Clean,
is_dir_header: false,
nlink: 1,
uid: 0,
gid: 0,
inode: 0,
blocks: 0,
owner: String::new(),
group: String::new(),
author: String::new(),
context: String::new(),
}
}
#[test]
fn precompute_siblings() {
let d = vec![1, 2, 3, 2];
let last = precompute_is_last(&d);
assert!(last[0]); assert!(!last[1]); assert!(last[2]); assert!(last[3]); }
#[test]
fn tree_connectors_correct() {
let entries = vec![
e_path("/r/a", 1),
e_path("/r/a/b", 2),
e_path("/r/a/b/g", 3),
e_path("/r/a/f", 2),
];
let mut entries = entries;
entries[0].kind = EntryKind::Directory;
entries[1].kind = EntryKind::Directory;
let colorizer = Colorizer::new(false);
let out = format_tree(&entries, &colorizer, false, IndicatorStyle::None);
assert!(out.contains("└── a\n"), "{out}");
assert!(out.contains("├── b\n"), "{out}");
assert!(out.contains("│ └── g\n"), "{out}");
assert!(out.contains("└── f\n"), "{out}");
}
}