feat: parallize initial app discovery

This commit is contained in:
ByteAtATime 2025-06-17 11:10:56 -07:00
parent a15db4946c
commit f02e4cafc7
No known key found for this signature in database
3 changed files with 230 additions and 54 deletions

View file

@ -1,11 +1,17 @@
use serde::{Deserialize, Serialize};
use std::{
collections::HashSet, env, fs, path::{Path, PathBuf}, process::Command
collections::{HashMap, HashSet},
env, fs,
path::{Path, PathBuf},
process::Command,
thread,
time::{Duration, SystemTime},
};
use freedesktop_file_parser::{parse, EntryType};
use serde::Serialize;
use rayon::prelude::*;
#[derive(Debug, Serialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct App {
pub name: String,
pub comment: Option<String>,
@ -13,25 +19,13 @@ pub struct App {
pub icon_path: Option<String>,
}
fn find_desktop_files(path: &Path) -> Vec<PathBuf> {
let mut desktop_files = Vec::new();
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
desktop_files.extend(find_desktop_files(&path));
} else if let Some(extension) = path.extension() {
if extension == "desktop" {
desktop_files.push(path);
}
}
}
}
desktop_files
#[derive(Serialize, Deserialize)]
struct AppCache {
apps: Vec<App>,
dir_mod_times: HashMap<PathBuf, SystemTime>,
}
#[tauri::command]
fn get_installed_apps() -> Vec<App> {
fn get_app_dirs() -> Vec<PathBuf> {
let mut app_dirs = vec![
PathBuf::from("/usr/share/applications"),
PathBuf::from("/usr/local/share/applications"),
@ -42,44 +36,137 @@ fn get_installed_apps() -> Vec<App> {
user_app_dir.push(".local/share/applications");
app_dirs.push(user_app_dir);
}
app_dirs
}
fn get_cache_path() -> Result<PathBuf, String> {
let cache_dir = env::var("XDG_CACHE_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| {
env::var("HOME")
.ok()
.map(|home| PathBuf::from(home).join(".cache"))
})
.ok_or("Could not determine cache directory")?;
let mut apps = Vec::new();
let mut seen_app_ids = HashSet::new();
let app_cache_dir = cache_dir.join("raycast-linux");
fs::create_dir_all(&app_cache_dir).map_err(|e| e.to_string())?;
Ok(app_cache_dir.join("apps.bincode"))
}
for dir in app_dirs {
for file_path in find_desktop_files(&dir) {
let content = fs::read_to_string(&file_path).unwrap();
if let Ok(desktop_file) = parse(&content) {
let entry = desktop_file.entry;
fn scan_and_cache_apps() -> Result<Vec<App>, String> {
let app_dirs = get_app_dirs();
let mut desktop_files = Vec::new();
if entry.hidden.unwrap_or(false) || entry.no_display.unwrap_or(false) {
continue;
}
if let EntryType::Application(app_fields) = entry.entry_type {
let app_id = file_path.file_stem().unwrap_or_default().to_string_lossy().to_string();
if !seen_app_ids.contains(&app_id) {
let app = App {
name: entry.name.default,
comment: entry.comment.map(|lc| lc.default),
exec: app_fields.exec,
icon_path: entry.icon
.and_then(|ic| ic.get_icon_path())
.and_then(|p| p.to_str().map(String::from)),
};
if app.exec.is_some() && !app.name.is_empty() {
apps.push(app);
seen_app_ids.insert(app_id);
}
}
}
}
for dir in &app_dirs {
if dir.exists() {
desktop_files.extend(find_desktop_files(dir));
}
}
apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
apps
let apps: Vec<App> = desktop_files
.par_iter()
.filter_map(|file_path| {
let content = fs::read_to_string(file_path).ok()?;
let desktop_file = parse(&content).ok()?;
let entry = desktop_file.entry;
if entry.hidden.unwrap_or(false) || entry.no_display.unwrap_or(false) {
return None;
}
if let EntryType::Application(app_fields) = entry.entry_type {
let app = App {
name: entry.name.default,
comment: entry.comment.map(|lc| lc.default),
exec: app_fields.exec,
icon_path: entry
.icon
.and_then(|ic| ic.get_icon_path())
.and_then(|p| p.to_str().map(String::from)),
};
if app.exec.is_some() && !app.name.is_empty() {
return Some(app);
}
}
None
})
.collect();
let mut unique_apps = Vec::new();
let mut seen_app_names = HashSet::new();
for app in apps {
if seen_app_names.insert(app.name.clone()) {
unique_apps.push(app);
}
}
unique_apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
let dir_mod_times = app_dirs
.into_iter()
.filter_map(|dir| {
let metadata = fs::metadata(&dir).ok()?;
let mod_time = metadata.modified().ok()?;
Some((dir, mod_time))
})
.collect();
let cache_data = AppCache {
apps: unique_apps.clone(),
dir_mod_times,
};
if let Ok(cache_path) = get_cache_path() {
let encoded = bincode::serde::encode_to_vec(&cache_data, bincode::config::standard()).map_err(|e| e.to_string())?;
fs::write(cache_path, encoded).map_err(|e| e.to_string())?;
}
Ok(unique_apps)
}
fn find_desktop_files(path: &Path) -> Vec<PathBuf> {
let mut desktop_files = Vec::new();
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
desktop_files.extend(find_desktop_files(&path));
} else if path.extension().map_or(false, |ext| ext == "desktop") {
desktop_files.push(path);
}
}
}
desktop_files
}
#[tauri::command]
fn get_installed_apps() -> Vec<App> {
let cache_path = match get_cache_path() {
Ok(path) => path,
Err(_) => return scan_and_cache_apps().unwrap_or_default(),
};
if let Ok(file_content) = fs::read(&cache_path) {
if let Ok((cached_data, _)) = bincode::serde::decode_from_slice::<AppCache, _>(&file_content, bincode::config::standard()) {
let is_stale = get_app_dirs().into_iter().any(|dir| {
let current_mod_time = fs::metadata(&dir).ok().and_then(|m| m.modified().ok());
let cached_mod_time = cached_data.dir_mod_times.get(&dir);
match (current_mod_time, cached_mod_time) {
(Some(current), Some(cached)) => current != *cached,
_ => true,
}
});
if !is_stale {
return cached_data.apps;
}
}
}
scan_and_cache_apps().unwrap_or_default()
}
#[tauri::command]
@ -90,7 +177,6 @@ fn launch_app(exec: String) -> Result<(), String> {
}
let mut command = Command::new(exec_parts[0]);
for arg in &exec_parts[1..] {
if !arg.starts_with('%') {
command.arg(arg);
@ -111,6 +197,15 @@ pub fn run() {
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![get_installed_apps, launch_app])
.setup(|_app| {
thread::spawn(|| {
loop {
thread::sleep(Duration::from_secs(300));
let _ = scan_and_cache_apps();
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
}