use crate::types::{EntryType, FileEntry};
use colored::*;
use std::fs;
use std::path::PathBuf;
use std::process::Command as ProcessCommand;
use std::time::UNIX_EPOCH;
use walkdir::WalkDir;
#[cfg(windows)]
use winreg::enums::*;
#[cfg(windows)]
use winreg::RegKey;
pub struct AppScanner;
impl AppScanner {
pub fn new() -> Self {
Self
}
pub fn scan_applications(&self, entries: &mut Vec<FileEntry>) {
println!("{}", "Indexing installed applications...".yellow());
#[cfg(windows)]
self.scan_windows_apps(entries);
#[cfg(target_os = "macos")]
self.scan_macos_apps(entries);
#[cfg(all(not(windows), not(target_os = "macos")))]
println!("{}", "Application indexing not implemented for this platform.".yellow());
}
pub fn launch_application(&self, entry: &FileEntry) -> Result<(), String> {
if let Some(exec_path) = &entry.executable_path {
let result = self.try_launch_methods(exec_path, &entry.name);
match result {
Ok(_) => {
println!("{} {}", "✓ Launched:".green(), entry.name.cyan());
Ok(())
}
Err(e) => {
let error_msg = format!("Failed to launch {}: {}", entry.name, e);
println!("{}", error_msg.red());
Err(error_msg)
}
}
} else {
let error_msg = format!("No executable path found for {}", entry.name);
println!("{}", error_msg.red());
Err(error_msg)
}
}
pub fn open_file(&self, entry: &FileEntry) -> Result<(), String> {
let path = &entry.path;
let result = self.try_open_file_methods(path, &entry.name);
match result {
Ok(_) => {
println!("{} {}", "✓ Opened:".green(), entry.name.cyan());
Ok(())
}
Err(e) => {
let error_msg = format!("Failed to open {}: {}", entry.name, e);
println!("{}", error_msg.red());
Err(error_msg)
}
}
}
pub fn open_entry(&self, entry: &FileEntry) -> Result<(), String> {
match entry.entry_type {
EntryType::Application => self.launch_application(entry),
EntryType::File | EntryType::Directory => self.open_file(entry),
}
}
fn try_launch_methods(&self, exec_path: &PathBuf, app_name: &str) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_os = "macos")]
{
if exec_path.extension().map_or(false, |ext| ext == "app") {
if ProcessCommand::new("open")
.arg("-a")
.arg(exec_path)
.spawn().is_ok() {
return Ok(());
}
}
if exec_path.is_file() && exec_path.extension().map_or(true, |ext| ext != "app") {
if ProcessCommand::new(exec_path).spawn().is_ok() {
return Ok(());
}
}
if ProcessCommand::new("open")
.arg(exec_path)
.spawn().is_ok() {
return Ok(());
}
}
#[cfg(windows)]
{
if exec_path.extension().map_or(false, |ext| ext == "exe") {
if ProcessCommand::new(exec_path).spawn().is_ok() {
return Ok(());
}
}
if ProcessCommand::new("cmd")
.args(&["/C", "start", "", &exec_path.to_string_lossy()])
.spawn().is_ok() {
return Ok(());
}
let ps_command = format!("Start-Process -FilePath '{}'", exec_path.to_string_lossy());
if ProcessCommand::new("powershell")
.args(&["-Command", &ps_command])
.spawn().is_ok() {
return Ok(());
}
}
#[cfg(all(not(windows), not(target_os = "macos")))]
{
if ProcessCommand::new(exec_path).spawn().is_ok() {
return Ok(());
}
if ProcessCommand::new("xdg-open")
.arg(exec_path)
.spawn().is_ok() {
return Ok(());
}
}
Err(format!("All launch methods failed for {}", app_name).into())
}
fn try_open_file_methods(&self, file_path: &PathBuf, file_name: &str) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_os = "macos")]
{
if ProcessCommand::new("open")
.arg(file_path)
.spawn().is_ok() {
return Ok(());
}
}
#[cfg(windows)]
{
if ProcessCommand::new("cmd")
.args(&["/C", "start", "", &file_path.to_string_lossy()])
.spawn().is_ok() {
return Ok(());
}
let ps_command = format!("Invoke-Item '{}'", file_path.to_string_lossy());
if ProcessCommand::new("powershell")
.args(&["-Command", &ps_command])
.spawn().is_ok() {
return Ok(());
}
}
#[cfg(all(not(windows), not(target_os = "macos")))]
{
if ProcessCommand::new("xdg-open")
.arg(file_path)
.spawn().is_ok() {
return Ok(());
}
if ProcessCommand::new("gnome-open")
.arg(file_path)
.spawn().is_ok() {
return Ok(());
}
if ProcessCommand::new("kde-open")
.arg(file_path)
.spawn().is_ok() {
return Ok(());
}
}
Err(format!("All open methods failed for {}", file_name).into())
}
}
#[cfg(windows)]
impl AppScanner {
fn scan_windows_apps(&self, entries: &mut Vec<FileEntry>) {
self.scan_registry_programs(entries);
self.scan_start_menu(entries);
self.scan_program_files(entries);
}
fn scan_registry_programs(&self, entries: &mut Vec<FileEntry>) {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
if let Ok(uninstall_key) = hklm.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall") {
for subkey_name in uninstall_key.enum_keys().filter_map(|k| k.ok()) {
if let Ok(subkey) = uninstall_key.open_subkey(&subkey_name) {
if let (Ok(display_name), Ok(install_location)) =
(subkey.get_value::<String, _>("DisplayName"),
subkey.get_value::<String, _>("InstallLocation")) {
let exe_path = if let Ok(exe) = subkey.get_value::<String, _>("DisplayIcon") {
Some(PathBuf::from(exe.split(',').next().unwrap_or(&exe)))
} else {
let install_path = PathBuf::from(&install_location);
if install_path.exists() {
self.find_main_executable(&install_path)
} else {
None
}
};
if let Some(exec_path) = exe_path {
if exec_path.exists() {
entries.push(FileEntry {
path: PathBuf::from(&install_location),
name: display_name.clone(),
size: 0,
modified: 0,
entry_type: EntryType::Application,
executable_path: Some(exec_path),
description: Some(format!("Installed application: {}", display_name)),
});
}
}
}
}
}
}
}
fn scan_start_menu(&self, entries: &mut Vec<FileEntry>) {
let start_menu_paths = vec![
dirs::home_dir().map(|p| p.join(r"AppData\Roaming\Microsoft\Windows\Start Menu\Programs")),
Some(PathBuf::from(r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs")),
];
for path_opt in start_menu_paths {
if let Some(path) = path_opt {
if path.exists() {
self.scan_shortcut_directory(&path, entries);
}
}
}
}
fn scan_shortcut_directory(&self, dir: &PathBuf, entries: &mut Vec<FileEntry>) {
for entry in WalkDir::new(dir)
.follow_links(false)
.max_depth(5)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "lnk" {
if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
if !["uninstall", "readme", "help", "documentation"].iter()
.any(|&s| name.to_lowercase().contains(s)) {
entries.push(FileEntry {
path: path.to_path_buf(),
name: name.to_string(),
size: 0,
modified: 0,
entry_type: EntryType::Application,
executable_path: Some(path.to_path_buf()),
description: Some("Start Menu shortcut".to_string()),
});
}
}
}
}
}
}
fn scan_program_files(&self, entries: &mut Vec<FileEntry>) {
let program_paths = vec![
PathBuf::from(r"C:\Program Files"),
PathBuf::from(r"C:\Program Files (x86)"),
];
for base_path in program_paths {
if base_path.exists() {
for entry in WalkDir::new(&base_path)
.follow_links(false)
.max_depth(3)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "exe" {
if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
if !["unins", "uninst", "setup", "install", "update"].iter()
.any(|&s| name.to_lowercase().contains(s)) {
entries.push(FileEntry {
path: path.to_path_buf(),
name: name.to_string(),
size: 0,
modified: 0,
entry_type: EntryType::Application,
executable_path: Some(path.to_path_buf()),
description: Some("Executable program".to_string()),
});
}
}
}
}
}
}
}
}
fn find_main_executable(&self, install_path: &PathBuf) -> Option<PathBuf> {
for entry in WalkDir::new(install_path)
.follow_links(false)
.max_depth(2)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "exe" {
if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
if !["unins", "uninst", "setup", "install", "update"].iter()
.any(|&s| name.to_lowercase().contains(s)) {
return Some(path.to_path_buf());
}
}
}
}
}
None
}
}
#[cfg(target_os = "macos")]
impl AppScanner {
fn scan_macos_apps(&self, entries: &mut Vec<FileEntry>) {
let app_paths = vec![
Some(PathBuf::from("/Applications")),
Some(PathBuf::from("/System/Applications")),
dirs::home_dir().map(|p| p.join("Applications")),
Some(PathBuf::from("/System/Library/CoreServices")),
];
for path_opt in app_paths {
if let Some(path) = path_opt {
if path.exists() {
self.scan_applications_directory(&path, entries);
}
}
}
}
fn scan_applications_directory(&self, dir: &PathBuf, entries: &mut Vec<FileEntry>) {
for entry in WalkDir::new(dir)
.follow_links(false)
.max_depth(if dir.to_string_lossy().contains("/System/") { 2 } else { 1 })
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "app" && path.is_dir() {
if let Some(app_name) = path.file_stem().and_then(|n| n.to_str()) {
if app_name.starts_with('.') ||
["Wireless Diagnostics", "System Information", "Disk Utility",
"Activity Monitor", "Console", "Terminal"].iter()
.any(|&s| app_name.contains(s)) {
continue;
}
let info_plist_path = path.join("Contents/Info.plist");
let (display_name, description) = if info_plist_path.exists() {
self.parse_app_info(&info_plist_path, app_name)
} else {
(app_name.to_string(), None)
};
let executable_path = self.find_macos_executable(&path.to_path_buf());
entries.push(FileEntry {
path: path.to_path_buf(),
name: display_name,
size: 0,
modified: entry.metadata().ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0),
entry_type: EntryType::Application,
executable_path,
description: description.or_else(|| Some("macOS Application".to_string())),
});
}
}
}
}
}
fn parse_app_info(&self, info_plist_path: &PathBuf, fallback_name: &str) -> (String, Option<String>) {
if let Ok(content) = fs::read_to_string(info_plist_path) {
let display_name = if let Some(start) = content.find("<key>CFBundleDisplayName</key>") {
self.extract_plist_string_value(&content, start)
} else if let Some(start) = content.find("<key>CFBundleName</key>") {
self.extract_plist_string_value(&content, start)
} else {
None
};
let description = if let Some(start) = content.find("<key>CFBundleGetInfoString</key>") {
self.extract_plist_string_value(&content, start)
} else {
None
};
(display_name.unwrap_or_else(|| fallback_name.to_string()), description)
} else {
(fallback_name.to_string(), None)
}
}
fn extract_plist_string_value(&self, content: &str, key_start: usize) -> Option<String> {
let search_area = &content[key_start..];
if let Some(string_start) = search_area.find("<string>") {
let value_start = key_start + string_start + 8;
if let Some(string_end) = content[value_start..].find("</string>") {
let value = &content[value_start..value_start + string_end];
return Some(value.to_string());
}
}
None
}
fn find_macos_executable(&self, app_path: &PathBuf) -> Option<PathBuf> {
let macos_dir = app_path.join("Contents/MacOS");
if macos_dir.exists() {
for entry in fs::read_dir(&macos_dir).ok()? {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() {
if let Ok(metadata) = entry.metadata() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o111 != 0 {
return Some(path);
}
}
}
}
}
}
}
Some(app_path.to_path_buf())
}
}