Skip to main content
Glama
8b-is
by 8b-is
ls.rs17.7 kB
// ----------------------------------------------------------------------------- // 🗂️ LS MODE - The Classic Unix Experience with Smart-Tree Magic! // ----------------------------------------------------------------------------- // This formatter replicates the beloved `ls -Alh` command that every Unix user // knows and loves. We take that familiar format and supercharge it with // smart-tree's intelligence and beautiful formatting. // // Output format matches: drwxrwxr-x 1 hue hue 1.2K Jul 9 14:56 filename // - Permissions (like drwxrwxr-x) // - Link count // - Owner and group // - Human-readable file size (1.2K, 45M, 2.3G) // - Last modified date and time // - Filename with proper coloring and emojis (optional) // // Hue gets the comfort of familiar ls output, Trish gets beautiful formatting, // and Aye gets to show off some Rust file system wizardry! 🎭 // ----------------------------------------------------------------------------- use super::Formatter; use crate::emoji_mapper; use crate::scanner::{FileNode, TreeStats}; use anyhow::Result; use chrono::{DateTime, Local}; use std::fs; use std::io::Write; use std::path::Path; #[cfg(unix)] use std::os::unix::fs::{MetadataExt, PermissionsExt}; /// LS Formatter - Unix ls -Alh output with smart-tree enhancements /// /// This formatter provides the classic Unix `ls -Alh` experience: /// - Long format with detailed file information /// - Human-readable file sizes /// - All files including hidden ones /// - Familiar permissions display /// - Proper date/time formatting /// /// Perfect for users who want smart-tree's power with familiar ls output! pub struct LsFormatter { /// Whether to show emojis alongside filenames (default: true) show_emojis: bool, /// Whether to use colors in output (default: true) use_colors: bool, } impl Default for LsFormatter { fn default() -> Self { Self::new(true, true) } } impl LsFormatter { /// Create a new LS formatter /// /// # Arguments /// * `show_emojis` - Whether to include emojis in the output (Trish loves these!) /// * `use_colors` - Whether to colorize the output for better readability pub fn new(show_emojis: bool, use_colors: bool) -> Self { Self { show_emojis, use_colors, } } /// Format file permissions in the classic Unix style (e.g., drwxrwxr-x) /// /// This creates the familiar 10-character permission string that every /// Unix user recognizes. First character is file type, then 3 groups of /// 3 characters each for owner, group, and other permissions. /// On Windows, we show a simplified version. fn format_permissions(&self, node: &FileNode) -> String { let metadata = match fs::metadata(&node.path) { Ok(meta) => meta, Err(_) => return "?---------".to_string(), // Permission denied or file missing }; let file_type = if metadata.is_dir() { 'd' } else if metadata.is_symlink() { 'l' } else { '-' }; #[cfg(unix)] { let mode = metadata.permissions().mode(); // Extract permission bits (owner, group, other) let owner_perms = format!( "{}{}{}", if mode & 0o400 != 0 { 'r' } else { '-' }, if mode & 0o200 != 0 { 'w' } else { '-' }, if mode & 0o100 != 0 { 'x' } else { '-' } ); let group_perms = format!( "{}{}{}", if mode & 0o040 != 0 { 'r' } else { '-' }, if mode & 0o020 != 0 { 'w' } else { '-' }, if mode & 0o010 != 0 { 'x' } else { '-' } ); let other_perms = format!( "{}{}{}", if mode & 0o004 != 0 { 'r' } else { '-' }, if mode & 0o002 != 0 { 'w' } else { '-' }, if mode & 0o001 != 0 { 'x' } else { '-' } ); format!("{}{}{}{}", file_type, owner_perms, group_perms, other_perms) } #[cfg(windows)] { // On Windows, show simplified permissions let readonly = metadata.permissions().readonly(); if readonly { format!("{}r--r--r--", file_type) } else { format!("{}rw-rw-rw-", file_type) } } #[cfg(not(any(unix, windows)))] { // For other platforms, show a generic format format!("{}rwxrwxrwx", file_type) } } /// Format file size in human-readable format (like ls -h) /// /// Converts bytes to human-readable units (B, K, M, G, T) /// Uses binary units (1024) like traditional ls command fn format_size(&self, size: u64) -> String { const UNITS: &[&str] = &["B", "K", "M", "G", "T"]; if size == 0 { return "0".to_string(); } let mut size_f = size as f64; let mut unit_index = 0; while size_f >= 1024.0 && unit_index < UNITS.len() - 1 { size_f /= 1024.0; unit_index += 1; } if unit_index == 0 { format!("{}", size) } else if size_f >= 10.0 { format!("{:.0}{}", size_f, UNITS[unit_index]) } else { format!("{:.1}{}", size_f, UNITS[unit_index]) } } /// Get the appropriate emoji for a file node /// /// This adds visual flair to the output, making it easier to quickly /// identify file types. Uses the centralized emoji mapper for rich file type representation! fn get_emoji(&self, node: &FileNode) -> &'static str { if !self.show_emojis { return ""; } emoji_mapper::get_file_emoji(node, false) } /// Format the filename with optional emoji and coloring /// Ensures consistent spacing by padding emoji field to 2 characters fn format_filename(&self, node: &FileNode) -> String { let emoji = self.get_emoji(node); let filename = node .path .file_name() .unwrap_or_else(|| node.path.as_os_str()) .to_string_lossy(); // Format emoji with consistent spacing let emoji_field = if emoji.is_empty() { String::new() } else { // Always add a space after emoji for consistent alignment format!("{} ", emoji) }; if self.use_colors { if node.is_dir { // Blue color for directories (ANSI color code 34) format!("{}\x1b[34m{}\x1b[0m", emoji_field, filename) } else if node.path.extension().and_then(|s| s.to_str()) == Some("rs") { // Orange color for Rust files (Hue's favorite!) format!("{}\x1b[38;5;208m{}\x1b[0m", emoji_field, filename) } else { // Default color for regular files format!("{}{}", emoji_field, filename) } } else if emoji_field.is_empty() { filename.to_string() } else { format!("{}{}", emoji_field, filename) } } /// Get owner and group information /// /// On Unix systems, this attempts to resolve uid/gid to actual names. /// Falls back to numeric IDs if resolution fails. fn get_owner_group(&self, node: &FileNode) -> (String, String) { #[cfg(unix)] { use std::ffi::CStr; // Get username from uid let owner = unsafe { let passwd = libc::getpwuid(node.uid); if passwd.is_null() { // User not found, use numeric ID node.uid.to_string() } else { // Convert username to String CStr::from_ptr((*passwd).pw_name) .to_string_lossy() .to_string() } }; // Get group name from gid let group = unsafe { let grp = libc::getgrgid(node.gid); if grp.is_null() { // Group not found, use numeric ID node.gid.to_string() } else { // Convert group name to String CStr::from_ptr((*grp).gr_name).to_string_lossy().to_string() } }; (owner, group) } #[cfg(not(unix))] { // On non-Unix systems, just show the numeric IDs (node.uid.to_string(), node.gid.to_string()) } } /// Get hard link count (simplified) fn get_link_count(&self, node: &FileNode) -> u64 { #[cfg(unix)] { match fs::metadata(&node.path) { Ok(meta) => meta.nlink(), Err(_) => 1, // Default to 1 if we can't read metadata } } #[cfg(not(unix))] { // On non-Unix systems, always return 1 for files, 2 for directories // This is a reasonable approximation if node.is_dir { 2 } else { 1 } } } } impl Formatter for LsFormatter { fn format( &self, writer: &mut dyn Write, nodes: &[FileNode], _stats: &TreeStats, root_path: &Path, ) -> Result<()> { // Check if this appears to be a filtered result set (from --find or other filters) // Heuristic: if nodes don't include all direct children of root, it's likely filtered let direct_child_count = nodes .iter() .filter(|n| n.path != root_path && n.path.parent() == Some(root_path)) .count(); let total_non_root = nodes.iter().filter(|n| n.path != root_path).count(); let is_filtered = total_non_root > 0 && (direct_child_count == 0 || total_non_root > direct_child_count * 2); let display_nodes: Vec<&FileNode> = if is_filtered { // For filtered results, show all matching nodes with full paths nodes .iter() .filter(|node| node.path != root_path) // Still exclude the root .collect() } else { // Normal ls behavior: only show direct children of root_path nodes .iter() .filter(|node| { if node.path == root_path { return false; // Don't show the root directory itself } // Only show direct children (depth 1 from root) node.path.parent() == Some(root_path) }) .collect() }; // If no files/directories to display, show a message if display_nodes.is_empty() { writeln!(writer, "No matching files or directories found")?; if is_filtered { writeln!(writer)?; writeln!( writer, "💡 Tip: Try using --everything to search in ignored directories like .cache" )?; writeln!( writer, "💡 Tip: Use -d 10 or higher to search deeper (default is 5 levels)" )?; writeln!( writer, "💡 Tip: Hidden directories need -a flag, ignored ones need --everything" )?; } return Ok(()); } // Note: Nodes are already sorted by the scanner based on user's --sort preference // We don't re-sort here to preserve the requested sort order // Format each file/directory in ls -Alh style for node in display_nodes { let permissions = self.format_permissions(node); let link_count = self.get_link_count(node); let (owner, group) = self.get_owner_group(node); let size = self.format_size(node.size); // Format the modification time let modified_time = match fs::metadata(&node.path) { Ok(meta) => match meta.modified() { Ok(time) => { let datetime: DateTime<Local> = time.into(); datetime.format("%b %d %H:%M").to_string() } Err(_) => "??? ?? ??:??".to_string(), }, Err(_) => "??? ?? ??:??".to_string(), }; // Determine filename display strategy: // - When filtering results (search/pattern match): Show relative path for context // - Otherwise: Show only the filename for cleaner output let filename = if is_filtered { // Format with relative path to help identify match locations let emoji = self.get_emoji(node); // Format emoji with consistent spacing let emoji_field = if emoji.is_empty() { String::new() } else { // Always add a space after emoji for consistent alignment format!("{} ", emoji) }; // Get relative path from root_path let relative_path = node .path .strip_prefix(root_path) .unwrap_or(&node.path) .display(); // Apply directory coloring if colors are enabled if self.use_colors && node.is_dir { // Blue color (ANSI 34) for directories format!("{}\x1b[34m{}\x1b[0m", emoji_field, relative_path) } else { // Default formatting for files or when colors are disabled format!("{}{}", emoji_field, relative_path) } } else { self.format_filename(node) }; // Write the ls -Alh formatted line writeln!( writer, "{:<10} {:>1} {:<4} {:<4} {:>6} {} {}", permissions, link_count, owner, group, size, modified_time, filename )?; } Ok(()) } } // ----------------------------------------------------------------------------- // 🎭 Tests - Because Trish insists on quality assurance! // ----------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use crate::scanner::{FileCategory, FileType, FilesystemType}; use std::path::PathBuf; use std::time::SystemTime; #[test] fn test_format_size() { let formatter = LsFormatter::new(false, false); assert_eq!(formatter.format_size(0), "0"); assert_eq!(formatter.format_size(500), "500"); assert_eq!(formatter.format_size(1024), "1.0K"); assert_eq!(formatter.format_size(1536), "1.5K"); assert_eq!(formatter.format_size(1048576), "1.0M"); assert_eq!(formatter.format_size(1073741824), "1.0G"); } #[test] fn test_emoji_selection() { let formatter = LsFormatter::new(true, false); // Test directory emojis let empty_dir = FileNode { path: PathBuf::from("/test"), file_type: FileType::Directory, size: 0, is_dir: true, depth: 0, permissions: 0o755, modified: SystemTime::now(), uid: 1000, gid: 1000, is_symlink: false, is_hidden: false, permission_denied: false, is_ignored: false, category: FileCategory::Unknown, search_matches: None, filesystem_type: FilesystemType::Unknown, }; assert_eq!(formatter.get_emoji(&empty_dir), "📂"); // Test file emojis let empty_file = FileNode { path: PathBuf::from("/test.txt"), file_type: FileType::RegularFile, size: 0, is_dir: false, depth: 0, permissions: 0o644, modified: SystemTime::now(), uid: 1000, gid: 1000, is_symlink: false, is_hidden: false, permission_denied: false, is_ignored: false, category: FileCategory::Unknown, search_matches: None, filesystem_type: FilesystemType::Unknown, }; assert_eq!(formatter.get_emoji(&empty_file), "🪹"); } #[test] fn test_permissions_format() { let formatter = LsFormatter::new(false, false); // This is a basic test - in real usage, format_permissions // reads actual file metadata let test_node = FileNode { path: PathBuf::from("/test"), file_type: FileType::Directory, size: 0, is_dir: true, depth: 0, permissions: 0o755, modified: SystemTime::now(), uid: 1000, gid: 1000, is_symlink: false, is_hidden: false, permission_denied: false, is_ignored: false, category: FileCategory::Unknown, search_matches: None, filesystem_type: FilesystemType::Unknown, }; let perms = formatter.format_permissions(&test_node); // Should start with 'd' for directory or '?' if we can't read it assert!(perms.starts_with('d') || perms.starts_with('?')); assert_eq!(perms.len(), 10); // Always 10 characters } }

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