use super::{hex::HexFormatter, Formatter, PathDisplayMode, StreamingFormatter};
use crate::context::detect_project_context;
use crate::scanner::{FileNode, TreeStats};
use anyhow::Result;
use sha2::{Digest, Sha256};
use std::io::Write;
use std::path::Path;
pub struct AiFormatter {
hex_formatter: HexFormatter,
}
impl AiFormatter {
pub fn new(no_emoji: bool, _path_mode: PathDisplayMode) -> Self {
Self {
hex_formatter: HexFormatter::new(false, no_emoji, true, PathDisplayMode::Off, false),
}
}
fn calculate_tree_hash(&self, nodes: &[FileNode]) -> String {
let mut hasher = Sha256::new();
for node in nodes {
hasher.update(node.depth.to_le_bytes());
hasher.update(
node.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.as_bytes(),
);
hasher.update([if node.is_dir { 1 } else { 0 }]);
hasher.update(node.size.to_le_bytes());
hasher.update(node.permissions.to_le_bytes());
}
let result = hasher.finalize();
hex::encode(&result[..8])
}
}
impl Formatter for AiFormatter {
fn format(
&self,
writer: &mut dyn Write,
nodes: &[FileNode],
stats: &TreeStats,
root_path: &Path,
) -> Result<()> {
writeln!(writer, "TREE_HEX_V1:")?;
if let Some(context) = detect_project_context(root_path) {
writeln!(writer, "CONTEXT: {}", context)?;
}
let tree_hash = self.calculate_tree_hash(nodes);
writeln!(writer, "HASH: {}", tree_hash)?;
self.hex_formatter.format(writer, nodes, stats, root_path)?;
writeln!(writer, "\nSTATS:")?;
writeln!(
writer,
"F:{:x} D:{:x} S:{:x} ({:.1}MB)",
stats.total_files,
stats.total_dirs,
stats.total_size,
stats.total_size as f64 / (1024.0 * 1024.0)
)?;
if !stats.file_types.is_empty() {
let mut types: Vec<_> = stats.file_types.iter().collect();
types.sort_by(|a, b| b.1.cmp(a.1));
let types_str: Vec<String> = types
.iter()
.take(10)
.map(|(ext, count)| format!("{}:{:x}", ext, count))
.collect();
writeln!(writer, "TYPES: {}", types_str.join(" "))?;
}
if !stats.largest_files.is_empty() {
let large_str: Vec<String> = stats
.largest_files
.iter()
.take(5)
.map(|(size, path)| {
let name = path
.file_name()
.unwrap_or(path.as_os_str())
.to_string_lossy();
format!("{}:{:x}", name, size)
})
.collect();
writeln!(writer, "LARGE: {}", large_str.join(" "))?;
}
if !stats.oldest_files.is_empty() && !stats.newest_files.is_empty() {
let oldest = stats.oldest_files[0]
.0
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let newest = stats.newest_files[0]
.0
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
writeln!(writer, "DATES: {:x}-{:x}", oldest, newest)?;
}
writeln!(writer, "END_AI")?;
Ok(())
}
}
impl StreamingFormatter for AiFormatter {
fn start_stream(&self, writer: &mut dyn Write, root_path: &Path) -> Result<()> {
writeln!(writer, "TREE_HEX_V1:")?;
if let Some(context) = detect_project_context(root_path) {
writeln!(writer, "CONTEXT: {}", context)?;
}
writeln!(writer, "HASH: STREAMING")?;
writer.flush()?;
Ok(())
}
fn format_node(&self, writer: &mut dyn Write, node: &FileNode, root_path: &Path) -> Result<()> {
self.hex_formatter.format_node(writer, node, root_path)
}
fn end_stream(
&self,
writer: &mut dyn Write,
stats: &TreeStats,
_root_path: &Path,
) -> Result<()> {
writeln!(writer, "\nSTATS:")?;
writeln!(
writer,
"F:{:x} D:{:x} S:{:x} ({:.1}MB)",
stats.total_files,
stats.total_dirs,
stats.total_size,
stats.total_size as f64 / (1024.0 * 1024.0)
)?;
if !stats.file_types.is_empty() {
let mut types: Vec<_> = stats.file_types.iter().collect();
types.sort_by(|a, b| b.1.cmp(a.1));
let types_str: Vec<String> = types
.iter()
.take(10)
.map(|(ext, count)| format!("{}:{:x}", ext, count))
.collect();
writeln!(writer, "TYPES: {}", types_str.join(" "))?;
}
if !stats.largest_files.is_empty() {
let large_str: Vec<String> = stats
.largest_files
.iter()
.take(5)
.map(|(size, path)| {
let name = path
.file_name()
.unwrap_or(path.as_os_str())
.to_string_lossy();
format!("{}:{:x}", name, size)
})
.collect();
writeln!(writer, "LARGE: {}", large_str.join(" "))?;
}
if !stats.oldest_files.is_empty() && !stats.newest_files.is_empty() {
let oldest = stats.oldest_files[0]
.0
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let newest = stats.newest_files[0]
.0
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
writeln!(writer, "DATES: {:x}-{:x}", oldest, newest)?;
}
writeln!(writer, "END_AI")?;
writer.flush()?;
Ok(())
}
}