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

79
src-tauri/Cargo.lock generated
View file

@ -278,6 +278,26 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -626,6 +646,25 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@ -881,6 +920,12 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "embed-resource" name = "embed-resource"
version = "3.0.3" version = "3.0.3"
@ -3055,7 +3100,9 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
name = "raycast-linux" name = "raycast-linux"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode",
"freedesktop-file-parser", "freedesktop-file-parser",
"rayon",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@ -3065,6 +3112,26 @@ dependencies = [
"tauri-plugin-shell", "tauri-plugin-shell",
] ]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.12" version = "0.5.12"
@ -4421,6 +4488,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@ -4481,6 +4554,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]] [[package]]
name = "vswhom" name = "vswhom"
version = "0.1.0" version = "0.1.0"

View file

@ -25,4 +25,6 @@ serde_json = "1"
tauri-plugin-clipboard-manager = "2" tauri-plugin-clipboard-manager = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
freedesktop-file-parser = "0.2.0" freedesktop-file-parser = "0.2.0"
bincode = { version = "2.0.1", features = ["serde"] }
rayon = "1.10.0"

View file

@ -1,11 +1,17 @@
use serde::{Deserialize, Serialize};
use std::{ 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 freedesktop_file_parser::{parse, EntryType};
use serde::Serialize; use rayon::prelude::*;
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct App { pub struct App {
pub name: String, pub name: String,
pub comment: Option<String>, pub comment: Option<String>,
@ -13,25 +19,13 @@ pub struct App {
pub icon_path: Option<String>, pub icon_path: Option<String>,
} }
fn find_desktop_files(path: &Path) -> Vec<PathBuf> { #[derive(Serialize, Deserialize)]
let mut desktop_files = Vec::new(); struct AppCache {
if let Ok(entries) = fs::read_dir(path) { apps: Vec<App>,
for entry in entries.flatten() { dir_mod_times: HashMap<PathBuf, SystemTime>,
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
} }
#[tauri::command] fn get_app_dirs() -> Vec<PathBuf> {
fn get_installed_apps() -> Vec<App> {
let mut app_dirs = vec![ let mut app_dirs = vec![
PathBuf::from("/usr/share/applications"), PathBuf::from("/usr/share/applications"),
PathBuf::from("/usr/local/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"); user_app_dir.push(".local/share/applications");
app_dirs.push(user_app_dir); 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 app_cache_dir = cache_dir.join("raycast-linux");
let mut seen_app_ids = HashSet::new(); 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 { fn scan_and_cache_apps() -> Result<Vec<App>, String> {
for file_path in find_desktop_files(&dir) { let app_dirs = get_app_dirs();
let content = fs::read_to_string(&file_path).unwrap(); let mut desktop_files = Vec::new();
if let Ok(desktop_file) = parse(&content) {
let entry = desktop_file.entry;
if entry.hidden.unwrap_or(false) || entry.no_display.unwrap_or(false) { for dir in &app_dirs {
continue; if dir.exists() {
} desktop_files.extend(find_desktop_files(dir));
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);
}
}
}
}
} }
} }
apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); let apps: Vec<App> = desktop_files
apps .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] #[tauri::command]
@ -90,7 +177,6 @@ fn launch_app(exec: String) -> Result<(), String> {
} }
let mut command = Command::new(exec_parts[0]); let mut command = Command::new(exec_parts[0]);
for arg in &exec_parts[1..] { for arg in &exec_parts[1..] {
if !arg.starts_with('%') { if !arg.starts_with('%') {
command.arg(arg); command.arg(arg);
@ -111,6 +197,15 @@ pub fn run() {
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![get_installed_apps, launch_app]) .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!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }