Skip to main content
Glama
by 8b-is
summary.rs26.7 kB
//! Summary formatter - "Intelligent defaults for humans!" - Omni //! Provides an intelligent summary based on directory content use super::Formatter; use crate::content_detector::{ContentDetector, DirectoryType, Language}; use crate::scanner::{FileNode, TreeStats}; use anyhow::Result; use colored::Colorize; use std::collections::HashMap; use std::io::Write; use std::path::Path; pub struct SummaryFormatter { use_color: bool, max_examples: usize, } impl SummaryFormatter { pub fn new(use_color: bool) -> Self { Self { use_color, max_examples: 5, } } fn colorize(&self, text: &str, color: &str) -> String { if self.use_color { match color { "blue" => text.blue().to_string(), "green" => text.green().to_string(), "yellow" => text.yellow().to_string(), "red" => text.red().to_string(), "cyan" => text.cyan().to_string(), "magenta" => text.magenta().to_string(), "bold" => text.bold().to_string(), _ => text.to_string(), } } else { text.to_string() } } fn is_high_level_directory(&self, nodes: &[FileNode], _stats: &TreeStats) -> bool { // Heuristics for detecting high-level directories: // 1. More than 20 subdirectories in root // 2. Has typical home directory folders (Documents, Downloads, etc.) // 3. Has multiple project-like directories // Count directories at root level (relative to scanned path) let mut root_dir_count = 0; let mut seen_paths = std::collections::HashSet::new(); for node in nodes { if node.is_dir { // Get the depth relative to the first node's parent if let Some(first) = nodes.first() { if let Some(base) = first.path.parent() { if let Ok(relative) = node.path.strip_prefix(base) { if relative.components().count() == 1 && seen_paths.insert(node.path.clone()) { root_dir_count += 1; } } } } } } if root_dir_count > 20 { return true; } // Check for home directory patterns let home_folders = [ "Documents", "Downloads", "Desktop", "Pictures", "Music", "Videos", ]; let mut home_folder_count = 0; for node in nodes { if node.is_dir { if let Some(name) = node.path.file_name().and_then(|f| f.to_str()) { if home_folders.contains(&name) { home_folder_count += 1; } } } } if home_folder_count >= 3 { return true; } // Check for multiple project-like directories let project_indicators = [ "Cargo.toml", "package.json", "pom.xml", ".git", "requirements.txt", ]; let mut project_dirs = std::collections::HashSet::new(); for node in nodes { if let Some(name) = node.path.file_name().and_then(|f| f.to_str()) { if project_indicators.contains(&name) { if let Some(parent) = node.path.parent() { project_dirs.insert(parent); } } } } project_dirs.len() > 5 } fn format_high_level_summary( &self, writer: &mut dyn Write, nodes: &[FileNode], stats: &TreeStats, root_path: &Path, ) -> Result<()> { // Header writeln!(writer, "{}", self.colorize("📊 Directory Overview", "bold"))?; writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?; writeln!(writer)?; // Path and basic stats writeln!( writer, "📁 {}: {}", self.colorize("Path", "cyan"), root_path.display() )?; writeln!( writer, "📈 {}: {} files, {} directories, {}", self.colorize("Total", "cyan"), self.colorize(&stats.total_files.to_string(), "green"), self.colorize(&stats.total_dirs.to_string(), "green"), self.colorize(&format_size(stats.total_size), "green") )?; writeln!(writer)?; // Analyze subdirectories let mut subdirs: HashMap<String, (usize, usize, u64)> = HashMap::new(); for node in nodes { if let Ok(relative) = node.path.strip_prefix(root_path) { if let Some(first) = relative.components().next() { if let Some(name) = first.as_os_str().to_str() { let entry = subdirs.entry(name.to_string()).or_insert((0, 0, 0)); if node.is_dir { entry.1 += 1; } else { entry.0 += 1; entry.2 += node.size; } } } } } // Sort by size let mut sorted_dirs: Vec<_> = subdirs.into_iter().collect(); sorted_dirs.sort_by(|a, b| b.1 .2.cmp(&a.1 .2)); // Show top directories writeln!( writer, "{}", self.colorize("Top Directories by Size:", "yellow") )?; writeln!(writer)?; for (name, (files, dirs, size)) in sorted_dirs.iter().take(10) { let size_str = format_size(*size); let size_bar = self.make_size_bar(*size, stats.total_size); writeln!( writer, " {} {} {}", self.colorize(&format!("{:20}", name), "cyan"), self.colorize(&format!("{:>10}", size_str), "green"), size_bar )?; writeln!( writer, " {:20} {} files, {} dirs", "", self.colorize(&files.to_string(), "blue"), self.colorize(&dirs.to_string(), "blue") )?; writeln!(writer)?; } // Detect projects let projects = self.detect_projects(nodes, root_path); if !projects.is_empty() { writeln!(writer, "{}", self.colorize("Detected Projects:", "yellow"))?; writeln!(writer)?; for (path, project_type) in projects.iter().take(10) { writeln!( writer, " • {} {}", self.colorize(path, "cyan"), self.colorize(&format!("({})", project_type), "magenta") )?; } writeln!(writer)?; } // Footer writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?; writeln!( writer, "💡 {}: Use {} to analyze a specific directory", self.colorize("Tip", "yellow"), self.colorize("st <directory>", "cyan") )?; Ok(()) } fn make_size_bar(&self, size: u64, total: u64) -> String { if total == 0 { return String::new(); } let percentage = (size as f64 / total as f64) * 100.0; let bar_width = 20; let filled = ((percentage / 100.0) * bar_width as f64) as usize; let bar = "█".repeat(filled) + &"░".repeat(bar_width - filled); format!("{} {:5.1}%", self.colorize(&bar, "blue"), percentage) } fn detect_projects(&self, nodes: &[FileNode], root_path: &Path) -> Vec<(String, String)> { let mut projects = Vec::new(); let mut checked_dirs = std::collections::HashSet::new(); for node in nodes { if let Some(parent) = node.path.parent() { if checked_dirs.contains(parent) { continue; } checked_dirs.insert(parent); let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let project_type = match name { "Cargo.toml" => Some("Rust"), "package.json" => Some("Node.js"), "requirements.txt" | "setup.py" | "pyproject.toml" => Some("Python"), "go.mod" => Some("Go"), "pom.xml" => Some("Java/Maven"), "build.gradle" | "build.gradle.kts" => Some("Java/Gradle"), "Gemfile" => Some("Ruby"), ".git" if node.is_dir => Some("Git Repository"), _ => None, }; if let Some(ptype) = project_type { if let Ok(relative) = parent.strip_prefix(root_path) { projects.push((relative.display().to_string(), ptype.to_string())); } } } } projects.sort(); projects.dedup(); projects } } impl Formatter for SummaryFormatter { fn format( &self, writer: &mut dyn Write, nodes: &[FileNode], stats: &TreeStats, root_path: &Path, ) -> Result<()> { // Check if this looks like a high-level directory (home, root, etc) let is_high_level = self.is_high_level_directory(nodes, stats); if is_high_level { return self.format_high_level_summary(writer, nodes, stats, root_path); } // Detect directory type for project-level analysis let dir_type = ContentDetector::detect(nodes, root_path); // Header writeln!(writer, "{}", self.colorize("📊 Directory Summary", "bold"))?; writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?; writeln!(writer)?; // Path and basic stats writeln!( writer, "📁 {}: {}", self.colorize("Path", "cyan"), root_path.display() )?; writeln!( writer, "📈 {}: {} files, {} directories, {}", self.colorize("Stats", "cyan"), self.colorize(&stats.total_files.to_string(), "green"), self.colorize(&stats.total_dirs.to_string(), "green"), self.colorize(&format_size(stats.total_size), "green") )?; writeln!(writer)?; // Content-specific analysis match &dir_type { DirectoryType::CodeProject { language, framework, has_tests, has_docs, } => { writeln!( writer, "🔧 {}: {} Project", self.colorize("Type", "yellow"), self.colorize(&format!("{:?}", language), "magenta") )?; if let Some(fw) = framework { writeln!( writer, "🚀 {}: {:?}", self.colorize("Framework", "yellow"), fw )?; } writeln!( writer, "✅ Tests: {} | 📚 Docs: {}", if *has_tests { self.colorize("Yes", "green") } else { self.colorize("No", "red") }, if *has_docs { self.colorize("Yes", "green") } else { self.colorize("No", "red") } )?; // Show main files writeln!(writer)?; writeln!(writer, "{}", self.colorize("Key Files:", "cyan"))?; // Find and display important files let important_files = find_important_code_files(nodes, language); for file in important_files.iter().take(self.max_examples) { writeln!(writer, " • {}", file)?; } // Language-specific tips writeln!(writer)?; writeln!(writer, "{}", self.colorize("Quick Commands:", "cyan"))?; match language { Language::Rust => { writeln!(writer, " • cargo build --release")?; writeln!(writer, " • cargo test")?; writeln!(writer, " • cargo run")?; } Language::Python => { writeln!(writer, " • python -m venv venv")?; writeln!(writer, " • pip install -r requirements.txt")?; writeln!(writer, " • python main.py")?; } Language::JavaScript | Language::TypeScript => { writeln!(writer, " • npm install")?; writeln!(writer, " • npm test")?; writeln!(writer, " • npm start")?; } _ => { writeln!(writer, " • Check README for build instructions")?; } } } DirectoryType::PhotoCollection { image_count, date_range, cameras, } => { writeln!( writer, "📷 {}: Photo Collection", self.colorize("Type", "yellow") )?; writeln!( writer, "🖼️ {}: {} images", self.colorize("Count", "cyan"), self.colorize(&image_count.to_string(), "green") )?; if let Some((start, end)) = date_range { writeln!( writer, "📅 {}: {} to {}", self.colorize("Date Range", "cyan"), start, end )?; } if !cameras.is_empty() { writeln!( writer, "📸 {}: {}", self.colorize("Cameras", "cyan"), cameras.join(", ") )?; } // Show file type breakdown let mut type_counts: HashMap<&str, usize> = HashMap::new(); for node in nodes { if !node.is_dir { if let Some(ext) = node.path.extension().and_then(|e| e.to_str()) { *type_counts.entry(ext).or_insert(0) += 1; } } } writeln!(writer)?; writeln!(writer, "{}", self.colorize("File Types:", "cyan"))?; for (ext, count) in type_counts.iter() { writeln!(writer, " • .{}: {}", ext, count)?; } } DirectoryType::DocumentArchive { categories, total_docs, } => { writeln!( writer, "📚 {}: Document Archive", self.colorize("Type", "yellow") )?; writeln!( writer, "📄 {}: {} documents", self.colorize("Count", "cyan"), self.colorize(&total_docs.to_string(), "green") )?; if !categories.is_empty() { writeln!(writer)?; writeln!(writer, "{}", self.colorize("Categories:", "cyan"))?; for (category, count) in categories.iter() { writeln!(writer, " • {}: {}", category, count)?; } } } DirectoryType::MediaLibrary { video_count, audio_count, total_duration, quality, } => { writeln!( writer, "🎬 {}: Media Library", self.colorize("Type", "yellow") )?; writeln!( writer, "🎥 Videos: {} | 🎵 Audio: {}", self.colorize(&video_count.to_string(), "green"), self.colorize(&audio_count.to_string(), "green") )?; if let Some(duration) = total_duration { writeln!( writer, "⏱️ {}: {}", self.colorize("Total Duration", "cyan"), duration )?; } if !quality.is_empty() { writeln!( writer, "📺 {}: {}", self.colorize("Quality", "cyan"), quality.join(", ") )?; } } DirectoryType::DataScience { notebooks, datasets, languages, } => { writeln!( writer, "🔬 {}: Data Science Workspace", self.colorize("Type", "yellow") )?; writeln!( writer, "📓 Notebooks: {} | 📊 Datasets: {}", self.colorize(&notebooks.to_string(), "green"), self.colorize(&datasets.to_string(), "green") )?; if !languages.is_empty() { writeln!( writer, "🐍 {}: {}", self.colorize("Languages", "cyan"), languages.join(", ") )?; } writeln!(writer)?; writeln!(writer, "{}", self.colorize("Quick Commands:", "cyan"))?; writeln!(writer, " • jupyter notebook")?; writeln!(writer, " • jupyter lab")?; writeln!(writer, " • python -m notebook")?; } DirectoryType::MixedContent { dominant_type, file_types, total_files, } => { writeln!( writer, "📦 {}: Mixed Content", self.colorize("Type", "yellow") )?; if let Some(dominant) = dominant_type { writeln!( writer, "🎯 {}: {}", self.colorize("Dominant Type", "cyan"), dominant )?; } writeln!( writer, "📊 {}: {}", self.colorize("Total Files", "cyan"), self.colorize(&total_files.to_string(), "green") )?; // Show top file types let mut types: Vec<_> = file_types.iter().collect(); types.sort_by(|a, b| b.1.cmp(a.1)); writeln!(writer)?; writeln!(writer, "{}", self.colorize("Top File Types:", "cyan"))?; for (ext, count) in types.iter().take(self.max_examples) { writeln!(writer, " • .{}: {}", ext, count)?; } } } // Footer with suggestions writeln!(writer)?; writeln!(writer, "{}", self.colorize("─".repeat(50).as_str(), "blue"))?; writeln!( writer, "💡 {}: Use {} for detailed analysis", self.colorize("Tip", "yellow"), self.colorize("st --mode relations", "cyan") )?; Ok(()) } } fn format_size(bytes: u64) -> String { const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; 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!("{:.2} {}", size, UNITS[unit_index]) } } fn find_important_code_files(nodes: &[FileNode], language: &Language) -> Vec<String> { let mut important = Vec::new(); for node in nodes { if node.is_dir { continue; } let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let is_important = match language { Language::Rust => { matches!(name, "main.rs" | "lib.rs" | "Cargo.toml" | "build.rs") } Language::Python => { matches!( name, "main.py" | "__init__.py" | "setup.py" | "requirements.txt" | "pyproject.toml" ) } Language::JavaScript | Language::TypeScript => { matches!( name, "index.js" | "index.ts" | "package.json" | "tsconfig.json" | "webpack.config.js" ) } Language::Go => { matches!(name, "main.go" | "go.mod" | "go.sum") } Language::Java => { matches!(name, "Main.java" | "pom.xml" | "build.gradle") } _ => false, }; if is_important { important.push(node.path.display().to_string()); } } important } #[cfg(test)] mod tests { use super::*; use crate::scanner::FileNode; use std::path::PathBuf; fn create_test_nodes() -> Vec<FileNode> { use crate::scanner::{FileCategory, FileType, FilesystemType}; vec![ FileNode { path: PathBuf::from("/test/src/main.rs"), is_dir: false, size: 1000, permissions: 0o644, uid: 1000, gid: 1000, modified: std::time::SystemTime::now(), is_symlink: false, is_hidden: false, permission_denied: false, is_ignored: false, depth: 2, file_type: FileType::RegularFile, category: FileCategory::Rust, search_matches: None, filesystem_type: FilesystemType::Ext4, }, FileNode { path: PathBuf::from("/test/Cargo.toml"), is_dir: false, size: 500, permissions: 0o644, uid: 1000, gid: 1000, modified: std::time::SystemTime::now(), is_symlink: false, is_hidden: false, permission_denied: false, is_ignored: false, depth: 1, file_type: FileType::RegularFile, category: FileCategory::Toml, search_matches: None, filesystem_type: FilesystemType::Ext4, }, FileNode { path: PathBuf::from("/test/src"), is_dir: true, size: 0, permissions: 0o755, uid: 1000, gid: 1000, modified: std::time::SystemTime::now(), is_symlink: false, is_hidden: false, permission_denied: false, is_ignored: false, depth: 1, file_type: FileType::Directory, category: FileCategory::Unknown, search_matches: None, filesystem_type: FilesystemType::Ext4, }, ] } #[test] fn test_summary_formatter_rust_project() { let formatter = SummaryFormatter::new(false); let nodes = create_test_nodes(); let stats = TreeStats { total_files: 2, total_dirs: 1, total_size: 1500, file_types: HashMap::new(), largest_files: vec![], newest_files: vec![], oldest_files: vec![], }; let mut output = Vec::new(); let result = formatter.format(&mut output, &nodes, &stats, &PathBuf::from("/test")); assert!(result.is_ok()); let output_str = String::from_utf8(output).unwrap(); assert!(output_str.contains("Rust Project")); assert!(output_str.contains("cargo build")); } #[test] fn test_high_level_directory_detection() { let formatter = SummaryFormatter::new(false); // Create many directories to trigger high-level detection use crate::scanner::{FileCategory, FileType, FilesystemType}; let mut nodes = vec![]; for i in 0..25 { nodes.push(FileNode { path: PathBuf::from(format!("/home/user/dir{}", i)), is_dir: true, size: 0, permissions: 0o755, uid: 1000, gid: 1000, modified: std::time::SystemTime::now(), is_symlink: false, is_hidden: false, permission_denied: false, is_ignored: false, depth: 1, file_type: FileType::Directory, category: FileCategory::Unknown, search_matches: None, filesystem_type: FilesystemType::Ext4, }); } let stats = TreeStats { total_files: 0, total_dirs: 25, total_size: 0, file_types: HashMap::new(), largest_files: vec![], newest_files: vec![], oldest_files: vec![], }; let is_high_level = formatter.is_high_level_directory(&nodes, &stats); assert!(is_high_level); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/8b-is/smart-tree'

If you have feedback or need assistance with the MCP directory API, please join our Discord server