use super::Formatter;
use crate::core::{MemoryFile, MemoryType};
use anyhow::Result;
use colored::Colorize;
use std::path::Path;
use termtree::Tree;
pub struct TreeFormatter;
impl Default for TreeFormatter {
fn default() -> Self {
Self::new()
}
}
impl TreeFormatter {
pub fn new() -> Self {
Self
}
}
impl Formatter for TreeFormatter {
fn format(&self, files: &[MemoryFile]) -> Result<()> {
let mut root = Tree::new("CLAUDE.md Files".bold().to_string());
let mut user_files = Vec::new();
let mut project_files = Vec::new();
let mut subdir_files = Vec::new();
let mut local_files = Vec::new();
for file in files {
match file.file_type {
MemoryType::UserMemory => user_files.push(file),
MemoryType::ProjectMemory => project_files.push(file),
MemoryType::SubdirMemory => subdir_files.push(file),
MemoryType::LocalMemory => local_files.push(file),
}
}
if !user_files.is_empty() {
let mut user_node = Tree::new("User Memory".blue().to_string());
for file in user_files {
user_node.push(format_file_node(file));
}
root.push(user_node);
}
if !project_files.is_empty() {
let mut project_node = Tree::new("Project Memory".green().to_string());
for file in project_files {
project_node.push(format_file_node(file));
}
root.push(project_node);
}
if !subdir_files.is_empty() {
let mut subdir_node = Tree::new("Subdirectory Memories".cyan().to_string());
for file in subdir_files {
let relative_path = file.path.strip_prefix(".").unwrap_or(&file.path);
add_to_tree(&mut subdir_node, relative_path, file);
}
root.push(subdir_node);
}
if !local_files.is_empty() {
let mut local_node = Tree::new("Local Memory (Deprecated)".yellow().to_string());
for file in local_files {
local_node.push(format_file_node(file));
}
root.push(local_node);
}
println!("{}", root);
Ok(())
}
}
fn format_file_node(file: &MemoryFile) -> Tree<String> {
let meta = &file.metadata;
let imports_info = if file.imports.is_empty() {
String::new()
} else {
format!(" [{} imports]", file.imports.len())
.dimmed()
.to_string()
};
let info = format!(
"{} ({} lines, {}{})",
file.path.file_name().unwrap_or_default().to_string_lossy(),
meta.line_count,
format_size(meta.size),
imports_info
);
Tree::new(info)
}
fn add_to_tree(tree: &mut Tree<String>, path: &Path, file: &MemoryFile) {
let components: Vec<_> = path
.parent()
.unwrap_or(Path::new(""))
.components()
.collect();
if components.is_empty() {
tree.push(format_file_node(file));
return;
}
let mut path_components = Vec::new();
for component in components {
path_components.push(component.as_os_str().to_string_lossy().to_string());
}
fn ensure_path<'a>(
tree: &'a mut Tree<String>,
path: &[String],
depth: usize,
) -> &'a mut Tree<String> {
if depth >= path.len() {
return tree;
}
let name = &path[depth];
let child_idx = tree.leaves.iter().position(|child| &child.root == name);
if child_idx.is_none() {
tree.push(Tree::new(name.clone()));
}
let child_idx = tree
.leaves
.iter()
.position(|child| &child.root == name)
.unwrap();
ensure_path(&mut tree.leaves[child_idx], path, depth + 1)
}
let target = ensure_path(tree, &path_components, 0);
target.push(format_file_node(file));
}
fn format_size(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", size as u64, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}