mod apps;
mod display;
mod history;
mod index;
mod search;
mod types;
use clap::{Arg, Command};
use colored::*;
use display::DisplayFormatter;
use history::SearchHistory;
use index::FileIndex;
use rustyline::error::ReadlineError;
use rustyline::{DefaultEditor, Result};
use std::path::PathBuf;
use types::{Config, EntryType, SearchResult};
fn parse_config() -> Config {
let matches = Command::new("listary")
.version("0.1.0")
.author("qiutian00")
.about("A CLI search utility like Listary - search files and applications")
.arg(
Arg::new("query")
.help("Search query")
.required(false)
.index(1),
)
.arg(
Arg::new("paths")
.short('p')
.long("paths")
.help("Paths to search in (comma-separated)")
.value_name("PATHS")
.default_value("."),
)
.arg(
Arg::new("limit")
.short('l')
.long("limit")
.help("Maximum number of results")
.value_name("NUMBER")
.default_value("50"),
)
.arg(
Arg::new("rebuild")
.short('r')
.long("rebuild")
.help("Rebuild file index")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("apps")
.short('a')
.long("apps")
.help("Include installed applications in search")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("launch")
.long("launch")
.help("Launch the first matching application")
.value_name("APP_NAME"),
)
.arg(
Arg::new("no-auto-open")
.long("no-auto-open")
.help("Disable auto-opening of single application matches")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
let include_apps = matches.get_flag("apps");
let rebuild = matches.get_flag("rebuild");
let auto_open = !matches.get_flag("no-auto-open");
let paths_str = matches.get_one::<String>("paths").unwrap();
let search_paths: Vec<PathBuf> = paths_str
.split(',')
.map(|p| PathBuf::from(p.trim()))
.collect();
let limit: usize = matches
.get_one::<String>("limit")
.unwrap()
.parse()
.unwrap_or(50);
Config {
include_apps,
limit,
rebuild,
search_paths,
auto_open,
}
}
fn handle_launch_command(index: &FileIndex, app_name: &str) {
let results = index.search(app_name, 1);
if let Some(result) = results.first() {
if matches!(result.entry.entry_type, EntryType::Application) {
index.launch_application(&result.entry).ok();
} else {
println!("{}", "Found item is not an application.".red());
}
} else {
println!("{} '{}'", "No application found with name:".red(), app_name.cyan());
}
}
fn handle_query_command(index: &FileIndex, formatter: &DisplayFormatter, query: &str, limit: usize) {
let results = index.search(query, limit);
formatter.print_results(&results);
}
fn handle_interactive_mode(index: &FileIndex, formatter: &DisplayFormatter, config: &Config) -> Result<()> {
println!("\n{}", "🔍 Interactive Search Mode".cyan().bold());
println!("{}", "Type your search query and press Enter. Use ↑↓ arrows for history.".bright_black());
if config.auto_open && config.include_apps {
println!("{}", "💡 Single application matches will auto-open automatically.".bright_black());
}
let mut rl = DefaultEditor::new()?;
let mut search_history = SearchHistory::new(100);
for entry in search_history.get_history().iter().rev() {
rl.add_history_entry(entry).ok();
}
let mut last_results: Vec<SearchResult> = Vec::new();
loop {
let readline = rl.readline(&format!("{} ", "search>".green().bold()));
match readline {
Ok(line) => {
let query = line.trim();
if query.is_empty() {
continue;
}
if query == "q" || query == "quit" {
println!("{}", "👋 Goodbye!".cyan());
break;
}
if query == "history" {
show_search_history(&search_history);
continue;
}
if query == "clear" {
search_history.clear();
let _ = rl.clear_history();
println!("{}", "✓ Search history cleared.".green());
continue;
}
if let Ok(num) = query.parse::<usize>() {
handle_number_selection(index, &last_results, num);
continue;
}
rl.add_history_entry(&line).ok();
search_history.add_query(query);
if config.auto_open && config.include_apps {
if let Some(single_match) = index.find_single_application_match(query) {
println!("{} Found single application match: {}",
"🎯".cyan(),
single_match.name.yellow()
);
println!("{} Auto-opening...", "🚀".cyan());
if let Err(e) = index.open_entry(single_match) {
println!("{}", e.red());
}
continue;
}
}
let start = std::time::Instant::now();
let results = index.search(query, config.limit);
let duration = start.elapsed();
formatter.print_results(&results);
last_results = results;
println!("\n{} in {:.2}ms",
"Search completed".bright_black(),
duration.as_millis()
);
formatter.print_interactive_help(last_results.len(), config.auto_open && config.include_apps);
},
Err(ReadlineError::Interrupted) => {
println!("{}", "👋 Goodbye!".cyan());
break;
},
Err(ReadlineError::Eof) => {
println!("{}", "👋 Goodbye!".cyan());
break;
},
Err(err) => {
println!("Error: {:?}", err);
break;
}
}
}
Ok(())
}
fn show_search_history(history: &SearchHistory) {
let entries = history.get_history();
if entries.is_empty() {
println!("{}", "📜 No search history available.".yellow());
} else {
println!("\n{}", "📜 Search History:".cyan().bold());
println!("{}", "─".repeat(50).bright_black());
for (i, entry) in entries.iter().enumerate() {
println!(" {}. {}", (i + 1).to_string().bright_blue(), entry.cyan());
}
println!("{}", "─".repeat(50).bright_black());
}
}
fn handle_number_selection(index: &FileIndex, last_results: &[SearchResult], num: usize) {
if num > 0 && num <= last_results.len() {
let result = &last_results[num - 1];
let entry = &result.entry;
println!("{} {}...", "🚀 Opening".cyan(), entry.name.yellow());
if let Err(e) = index.open_entry(entry) {
println!("{}", e.red());
}
} else {
if last_results.is_empty() {
println!("{}", "⚠️ No search results available. Please search first.".yellow());
} else {
println!("{} Please enter a number between 1 and {}.",
"⚠️ Invalid selection.".yellow(),
last_results.len().to_string().cyan()
);
}
}
}
fn main() {
let config = parse_config();
let mut index = FileIndex::new(config.include_apps);
let formatter = DisplayFormatter::new();
if config.rebuild || !index.load_cache() {
println!("{}", "Building file index...".yellow());
index.build_index(&config.search_paths);
println!("{} indexed {} files", "✓".green(), index.entries_count().to_string().cyan());
} else {
println!("{} loaded {} files from cache", "✓".green(), index.entries_count().to_string().cyan());
}
let matches = Command::new("listary")
.version("0.1.0")
.author("qiutian00")
.about("A CLI search utility like Listary - search files and applications")
.arg(Arg::new("query").help("Search query").required(false).index(1))
.arg(Arg::new("paths").short('p').long("paths").help("Paths to search in (comma-separated)").value_name("PATHS").default_value("."))
.arg(Arg::new("limit").short('l').long("limit").help("Maximum number of results").value_name("NUMBER").default_value("50"))
.arg(Arg::new("rebuild").short('r').long("rebuild").help("Rebuild file index").action(clap::ArgAction::SetTrue))
.arg(Arg::new("apps").short('a').long("apps").help("Include installed applications in search").action(clap::ArgAction::SetTrue))
.arg(Arg::new("launch").long("launch").help("Launch the first matching application").value_name("APP_NAME"))
.get_matches();
if let Some(app_name) = matches.get_one::<String>("launch") {
handle_launch_command(&index, app_name);
return;
}
if let Some(query) = matches.get_one::<String>("query") {
handle_query_command(&index, &formatter, query, config.limit);
return;
}
if let Err(e) = handle_interactive_mode(&index, &formatter, &config) {
eprintln!("Error in interactive mode: {}", e);
std::process::exit(1);
}
}