use crate::cli::{Cli, ColorOption, FormatOption}; use crate::descriptions::DescriptionManager; use crate::error::Result; use crate::fs::{EntryType, FileEntry}; use chrono::{DateTime, Local}; use console::{style, Color}; use std::io::{self, Write};
pub async fn render_entries(entries: &[FileEntry], cli: &Cli) -> Result<()> {
match cli.format {
FormatOption::Default => render_default(entries, cli).await, FormatOption::Long => render_long(entries, cli).await, FormatOption::Tree => render_tree(entries, None).await, FormatOption::Grid => render_grid(entries, cli).await, FormatOption::Json => render_json(entries).await, }
}
pub async fn render_tree(entries: &[FileEntry], _max_depth: Option<usize>) -> Result<()> {
for entry in entries {
let icon = get_file_icon(&entry.file_type); let color = get_file_color(&entry.file_type, &entry.name);
println!("├── {} {}", icon, style(&entry.name).fg(color));
}
Ok(())
}
pub async fn render_search_results(entries: &[FileEntry], cli: &Cli) -> Result<()> {
println!("Found {} matches:", entries.len());
render_entries(entries, cli).await
}
async fn render_default(entries: &[FileEntry], cli: &Cli) -> Result<()> {
let use_colors = should_use_colors(&cli.color);
let desc_manager = DescriptionManager::new().ok();
for entry in entries {
let mut output = String::new();
if cli.icons {
output.push_str(&get_file_icon(&entry.file_type));
output.push(' ');
}
if use_colors {
let color = get_file_color(&entry.file_type, &entry.name);
output.push_str(&style(&entry.name).fg(color).to_string());
} else {
output.push_str(&entry.name);
}
if entry.is_symlink {
if let Some(ref target) = entry.symlink_target {
output.push_str(" -> ");
if use_colors {
output.push_str(&style(target.display()).dim().to_string());
} else {
output.push_str(&target.display().to_string());
}
}
}
if entry.file_type == EntryType::Directory {
if let Some(ref manager) = desc_manager {
if let Some(desc) = manager.get_description(&entry.path) {
if use_colors {
output.push_str(&format!(
" {}",
style(format!("({})", desc.description)).dim().italic()
));
} else {
output.push_str(&format!(" ({})", desc.description));
}
}
}
}
println!("{output}");
}
Ok(())
}
async fn render_long(entries: &[FileEntry], cli: &Cli) -> Result<()> {
let use_colors = should_use_colors(&cli.color);
let desc_manager = DescriptionManager::new().ok();
for entry in entries {
let mut output = String::new();
output.push_str(&entry.permissions);
output.push(' ');
let size_str = if cli.human_readable {
format_human_size(entry.size)
} else {
entry.size.to_string()
};
if use_colors {
output.push_str(&style(format!("{size_str:>8}")).cyan().to_string());
} else {
output.push_str(&format!("{size_str:>8}"));
}
output.push(' ');
let time_str = format_time(&entry.modified);
if use_colors {
output.push_str(&style(&time_str).yellow().to_string());
} else {
output.push_str(&time_str);
}
output.push(' ');
if cli.icons {
output.push_str(&get_file_icon(&entry.file_type));
output.push(' ');
}
if use_colors {
let color = get_file_color(&entry.file_type, &entry.name);
output.push_str(&style(&entry.name).fg(color).to_string());
} else {
output.push_str(&entry.name);
}
if entry.is_symlink {
if let Some(ref target) = entry.symlink_target {
output.push_str(" -> ");
if use_colors {
output.push_str(&style(target.display()).dim().to_string());
} else {
output.push_str(&target.display().to_string());
}
}
}
println!("{output}");
if entry.file_type == EntryType::Directory {
if let Some(ref manager) = desc_manager {
if let Some(desc) = manager.get_description(&entry.path) {
if use_colors {
println!(" 📝 {}", style(&desc.description).dim().italic());
} else {
println!(" 📝 {}", desc.description);
}
}
}
}
}
Ok(())
}
async fn render_grid(entries: &[FileEntry], cli: &Cli) -> Result<()> {
let use_colors = should_use_colors(&cli.color);
let terminal_width = get_terminal_width();
let max_name_len = entries.iter().map(|e| e.name.len()).max().unwrap_or(10);
let column_width = max_name_len + 2;
let columns = (terminal_width / column_width).max(1);
for (i, entry) in entries.iter().enumerate() {
let mut output = String::new();
if cli.icons {
output.push_str(&get_file_icon(&entry.file_type));
output.push(' ');
}
if use_colors {
let color = get_file_color(&entry.file_type, &entry.name);
output.push_str(&style(&entry.name).fg(color).to_string());
} else {
output.push_str(&entry.name);
}
let padding = column_width.saturating_sub(output.len());
output.push_str(&" ".repeat(padding));
print!("{output}");
if (i + 1) % columns == 0 {
println!();
}
}
if entries.len() % columns != 0 {
println!();
}
io::stdout().flush().unwrap();
Ok(())
}
async fn render_json(entries: &[FileEntry]) -> Result<()> {
let json_entries: Vec<serde_json::Value> = entries
.iter()
.map(|entry| {
serde_json::json!({
"name": entry.name, "path": entry.path, "size": entry.size, "modified": entry.modified.to_rfc3339(), "permissions": entry.permissions, "type": match entry.file_type {
EntryType::File => "file",
EntryType::Directory => "directory",
EntryType::Symlink => "symlink",
EntryType::Other => "other"
},
"is_hidden": entry.is_hidden, "is_symlink": entry.is_symlink, "symlink_target": entry.symlink_target })
})
.collect();
let output = serde_json::to_string_pretty(&json_entries).map_err(|e| {
crate::error::LsPlusError::format_error(format!("JSON serialization failed: {e}"))
})?;
println!("{output}");
Ok(())
}
fn should_use_colors(color_option: &ColorOption) -> bool {
match color_option {
ColorOption::Always => true, ColorOption::Never => false, ColorOption::Auto => atty::is(atty::Stream::Stdout), }
}
fn get_file_icon(file_type: &EntryType) -> String {
match file_type {
EntryType::Directory => "📁".to_string(), EntryType::File => "📄".to_string(), EntryType::Symlink => "🔗".to_string(), EntryType::Other => "❓".to_string(), }
}
fn get_file_color(file_type: &EntryType, name: &str) -> Color {
match file_type {
EntryType::Directory => Color::Blue, EntryType::Symlink => Color::Cyan, EntryType::File => {
if name.starts_with('.') {
Color::Black } else if is_executable(name) {
Color::Green } else {
Color::White }
}
EntryType::Other => Color::Magenta, }
}
fn is_executable(name: &str) -> bool {
name.ends_with(".exe") || name.ends_with(".sh") || name.ends_with(".bat") || (!name.contains('.') && !name.starts_with('.')) }
fn format_human_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "K", "M", "G", "T"]; let mut size = size 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!("{}B", size as u64) } else {
format!("{:.1}{}", size, UNITS[unit_index]) }
}
fn format_time(time: &DateTime<Local>) -> String {
time.format("%b %d %H:%M").to_string()
}
fn get_terminal_width() -> usize {
crossterm::terminal::size()
.map(|(width, _)| width as usize) .unwrap_or(80) }
mod atty {
pub enum Stream {
Stdout,
}
pub fn is(_stream: Stream) -> bool {
true
}
}