use crate::types::{EntryType, SearchResult};
use colored::*;
use std::collections::HashSet;
pub struct DisplayFormatter;
impl DisplayFormatter {
pub fn new() -> Self {
Self
}
pub fn print_results(&self, results: &[SearchResult]) {
if results.is_empty() {
println!("{}", "No results found.".red());
return;
}
println!("\n{} {}:", "Found".green().bold(), results.len().to_string().cyan().bold());
println!("{}", "─".repeat(80).bright_black());
for (i, result) in results.iter().enumerate() {
let entry = &result.entry;
let highlighted_name = self.highlight_matches(&entry.name, &result.matched_indices);
let file_type = self.get_file_type_icon(&entry.entry_type, &entry.path);
let size_str = self.format_entry_size(&entry.entry_type, entry.size);
println!(
"{:2}. {} {} {} {}",
(i + 1).to_string().bright_blue(),
file_type,
highlighted_name,
size_str,
entry.path.parent()
.and_then(|p| p.to_str())
.unwrap_or("")
.bright_black()
);
}
}
pub fn print_interactive_help(&self, results_count: usize, auto_open_enabled: bool) {
if results_count > 0 {
println!("\n{}", "─".repeat(80).bright_black());
println!("{}", "💡 Actions:".yellow().bold());
println!(" {} Type a number (1-{}) to open the file/application", "•".cyan(), results_count);
println!(" {} Type a new search query to search again", "•".cyan());
if auto_open_enabled {
println!(" {} Single app matches will auto-open", "•".cyan());
}
println!(" {} Use ↑↓ arrow keys to browse search history", "•".cyan());
println!(" {} Type 'history' to view search history", "•".cyan());
println!(" {} Type 'clear' to clear search history", "•".cyan());
println!(" {} Type 'q' or 'quit' to exit", "•".cyan());
println!(" {} Press Ctrl+C to exit", "•".cyan());
} else {
let auto_hint = if auto_open_enabled { ", single app matches auto-open" } else { "" };
println!("{}", format!("Type a new search query{}, use ↑↓ for history, or 'q' to quit.", auto_hint).bright_black());
}
}
fn highlight_matches(&self, text: &str, indices: &[usize]) -> String {
let mut result = String::new();
let chars: Vec<char> = text.chars().collect();
let index_set: HashSet<usize> = indices.iter().copied().collect();
for (i, &ch) in chars.iter().enumerate() {
if index_set.contains(&i) {
result.push_str(&ch.to_string().yellow().bold().to_string());
} else {
result.push(ch);
}
}
result
}
fn get_file_type_icon(&self, entry_type: &EntryType, path: &std::path::PathBuf) -> String {
match entry_type {
EntryType::Directory => "📁".to_string(),
EntryType::Application => "🚀".to_string(),
EntryType::File => {
match path.extension().and_then(|s| s.to_str()) {
Some("txt") | Some("md") | Some("rst") => "📄",
Some("rs") | Some("py") | Some("js") | Some("ts") | Some("go") | Some("java") => "💻",
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("svg") => "🖼️",
Some("mp3") | Some("wav") | Some("flac") | Some("ogg") => "🎵",
Some("mp4") | Some("avi") | Some("mkv") | Some("mov") => "🎬",
Some("zip") | Some("rar") | Some("7z") | Some("tar") | Some("gz") => "📦",
Some("pdf") => "📕",
Some("exe") | Some("msi") => "⚙️",
_ => "📄",
}.to_string()
}
}
}
fn format_entry_size(&self, entry_type: &EntryType, size: u64) -> String {
match entry_type {
EntryType::Directory => "<DIR>".bright_black().to_string(),
EntryType::Application => "<APP>".bright_green().to_string(),
EntryType::File => self.format_file_size(size).bright_black().to_string(),
}
}
fn format_file_size(&self, size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
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!("{} {}", size as u64, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}
}