diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs new file mode 100644 index 0000000..2233d72 --- /dev/null +++ b/src-tauri/src/app.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct App { + pub name: String, + pub comment: Option, + pub exec: Option, + pub icon_path: Option, +} + +impl App { + pub fn new(name: String) -> Self { + Self { + name, + comment: None, + exec: None, + icon_path: None, + } + } + + pub fn with_comment(mut self, comment: Option) -> Self { + self.comment = comment; + self + } + + pub fn with_exec(mut self, exec: Option) -> Self { + self.exec = exec; + self + } + + pub fn with_icon_path(mut self, icon_path: Option) -> Self { + self.icon_path = icon_path; + self + } +} \ No newline at end of file diff --git a/src-tauri/src/cache.rs b/src-tauri/src/cache.rs new file mode 100644 index 0000000..0c41ade --- /dev/null +++ b/src-tauri/src/cache.rs @@ -0,0 +1,88 @@ +use crate::{app::App, desktop::DesktopFileManager, error::AppError}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + env, fs, + path::{Path, PathBuf}, + time::SystemTime, +}; + +#[derive(Serialize, Deserialize)] +pub struct AppCache { + apps: Vec, + dir_mod_times: HashMap, +} + +impl AppCache { + pub fn get_cache_path() -> Result { + let cache_dir = env::var("XDG_CACHE_HOME") + .map(PathBuf::from) + .or_else(|_| env::var("HOME").map(|home| PathBuf::from(home).join(".cache"))) + .map_err(|_| AppError::DirectoryNotFound)?; + + let app_cache_dir = cache_dir.join("raycast-linux"); + fs::create_dir_all(&app_cache_dir)?; + Ok(app_cache_dir.join("apps.bincode")) + } + + pub fn read_from_file(path: &Path) -> Result { + let file_content = fs::read(path)?; + let (decoded, _) = + bincode::serde::decode_from_slice(&file_content, bincode::config::standard())?; + Ok(decoded) + } + + pub fn write_to_file(&self, path: &Path) -> Result<(), AppError> { + let encoded = bincode::serde::encode_to_vec(self, bincode::config::standard())?; + fs::write(path, encoded)?; + Ok(()) + } + + pub fn is_stale(&self) -> bool { + DesktopFileManager::get_app_directories() + .into_iter() + .any(|dir| { + let current_mod_time = fs::metadata(&dir).ok().and_then(|m| m.modified().ok()); + let cached_mod_time = self.dir_mod_times.get(&dir); + + match (current_mod_time, cached_mod_time) { + (Some(current), Some(cached)) => current > *cached, + _ => true, + } + }) + } + + pub fn get_apps() -> Result, AppError> { + let cache_path = Self::get_cache_path()?; + + if let Ok(cached_data) = Self::read_from_file(&cache_path) { + if !cached_data.is_stale() { + return Ok(cached_data.apps); + } + } + + Self::refresh_and_get_apps() + } + + pub fn refresh_and_get_apps() -> Result, AppError> { + let (apps, dir_mod_times) = DesktopFileManager::scan_and_parse_apps()?; + let cache_data = AppCache { + apps: apps.clone(), + dir_mod_times, + }; + + if let Ok(cache_path) = Self::get_cache_path() { + if let Err(e) = cache_data.write_to_file(&cache_path) { + eprintln!("Failed to write to app cache: {:?}", e); + } + } + + Ok(apps) + } + + pub fn refresh_background() { + if let Err(e) = Self::refresh_and_get_apps() { + eprintln!("Error refreshing app cache in background: {:?}", e); + } + } +} \ No newline at end of file diff --git a/src-tauri/src/desktop.rs b/src-tauri/src/desktop.rs new file mode 100644 index 0000000..50c26c5 --- /dev/null +++ b/src-tauri/src/desktop.rs @@ -0,0 +1,117 @@ +use crate::{app::App, error::AppError}; +use freedesktop_file_parser::{parse, EntryType}; +use rayon::prelude::*; +use std::{ + collections::{HashMap, HashSet}, + env, fs, + path::{Path, PathBuf}, + time::SystemTime, +}; + +pub struct DesktopFileManager; + +impl DesktopFileManager { + pub fn get_app_directories() -> Vec { + let mut app_dirs = vec![ + PathBuf::from("/usr/share/applications"), + PathBuf::from("/usr/local/share/applications"), + ]; + + if let Ok(home_dir) = env::var("HOME") { + app_dirs.push(PathBuf::from(home_dir).join(".local/share/applications")); + } + app_dirs + } + + pub fn find_desktop_files(path: &Path) -> Vec { + 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(Self::find_desktop_files(&path)); + } else if path.extension().map_or(false, |ext| ext == "desktop") { + desktop_files.push(path); + } + } + } + desktop_files + } + + pub fn scan_and_parse_apps() -> Result<(Vec, HashMap), AppError> { + let app_dirs = Self::get_app_directories(); + let desktop_files: Vec = app_dirs + .iter() + .filter(|dir| dir.exists()) + .flat_map(|dir| Self::find_desktop_files(dir)) + .collect(); + + let apps: Vec = desktop_files + .par_iter() + .filter_map(|file_path| Self::parse_desktop_file(file_path)) + .collect(); + + let unique_apps = Self::deduplicate_and_sort_apps(apps); + + let dir_mod_times = Self::get_directory_modification_times(app_dirs)?; + + Ok((unique_apps, dir_mod_times)) + } + + fn parse_desktop_file(file_path: &Path) -> Option { + let content = fs::read_to_string(file_path).ok()?; + let desktop_file = parse(&content).ok()?; + + if desktop_file.entry.hidden.unwrap_or(false) + || desktop_file.entry.no_display.unwrap_or(false) + { + return None; + } + + if let EntryType::Application(app_fields) = desktop_file.entry.entry_type { + if app_fields.exec.is_some() && !desktop_file.entry.name.default.is_empty() { + return Some( + App::new(desktop_file.entry.name.default) + .with_comment(desktop_file.entry.comment.map(|lc| lc.default)) + .with_exec(app_fields.exec) + .with_icon_path( + desktop_file + .entry + .icon + .and_then(|ic| ic.get_icon_path()) + .and_then(|p| p.to_str().map(String::from)), + ), + ); + } + } + None + } + + fn deduplicate_and_sort_apps(apps: Vec) -> Vec { + 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())); + unique_apps + } + + fn get_directory_modification_times( + app_dirs: Vec, + ) -> Result, AppError> { + Ok(app_dirs + .into_iter() + .filter_map(|dir| { + fs::metadata(&dir) + .and_then(|m| m.modified()) + .ok() + .map(|mod_time| (dir, mod_time)) + }) + .collect()) + } +} \ No newline at end of file diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs new file mode 100644 index 0000000..cbf8387 --- /dev/null +++ b/src-tauri/src/error.rs @@ -0,0 +1,39 @@ +use std::io; + +#[derive(Debug)] +pub enum AppError { + Io(io::Error), + Serialization(String), + DirectoryNotFound +} + +impl From for AppError { + fn from(error: io::Error) -> Self { + AppError::Io(error) + } +} + +impl From for AppError { + fn from(error: bincode::error::DecodeError) -> Self { + AppError::Serialization(format!("Decode error: {}", error)) + } +} + +impl From for AppError { + fn from(error: bincode::error::EncodeError) -> Self { + AppError::Serialization(format!("Encode error: {}", error)) + } +} + +impl std::fmt::Display for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AppError::Io(err) => write!(f, "IO error: {}", err), + AppError::Serialization(msg) => write!(f, "Serialization error: {}", msg), + AppError::DirectoryNotFound => write!(f, "Directory not found"), + AppError::CacheError(msg) => write!(f, "Cache error: {}", msg), + } + } +} + +impl std::error::Error for AppError {} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 309da36..41b9137 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,216 +1,21 @@ -use freedesktop_file_parser::{parse, EntryType}; -use rayon::prelude::*; -use selection::get_text; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{HashMap, HashSet}, - env, fs, - io, - path::{Path, PathBuf}, - process::Command, - thread, - time::{Duration, SystemTime}, +mod app; +mod cache; +mod desktop; +mod error; + +use crate::{ + app::App, + cache::AppCache, }; - -#[derive(Debug)] -enum CacheError { - Io, - Bincode, - DirectoryNotFound, -} - -impl From for CacheError { - fn from(_: io::Error) -> Self { - CacheError::Io - } -} - -impl From for CacheError { - fn from(_: bincode::error::DecodeError) -> Self { - CacheError::Bincode - } -} - -impl From for CacheError { - fn from(_: bincode::error::EncodeError) -> Self { - CacheError::Bincode - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct App { - pub name: String, - pub comment: Option, - pub exec: Option, - pub icon_path: Option, -} - -#[derive(Serialize, Deserialize)] -struct AppCache { - apps: Vec, - dir_mod_times: HashMap, -} - -fn get_app_dirs() -> Vec { - let mut app_dirs = vec![ - PathBuf::from("/usr/share/applications"), - PathBuf::from("/usr/local/share/applications"), - ]; - - if let Ok(home_dir) = env::var("HOME") { - app_dirs.push(PathBuf::from(home_dir).join(".local/share/applications")); - } - app_dirs -} - -fn get_cache_path() -> Result { - let cache_dir = env::var("XDG_CACHE_HOME") - .map(PathBuf::from) - .or_else(|_| env::var("HOME").map(|home| PathBuf::from(home).join(".cache"))) - .map_err(|_| CacheError::DirectoryNotFound)?; - - let app_cache_dir = cache_dir.join("raycast-linux"); - fs::create_dir_all(&app_cache_dir)?; - Ok(app_cache_dir.join("apps.bincode")) -} - -fn find_desktop_files(path: &Path) -> Vec { - 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 -} - -fn scan_and_parse_apps() -> Result<(Vec, HashMap), CacheError> { - let app_dirs = get_app_dirs(); - let desktop_files: Vec = app_dirs - .iter() - .filter(|dir| dir.exists()) - .flat_map(|dir| find_desktop_files(dir)) - .collect(); - - let apps: Vec = desktop_files - .par_iter() - .filter_map(|file_path| { - let content = fs::read_to_string(file_path).ok()?; - let desktop_file = parse(&content).ok()?; - - if desktop_file.entry.hidden.unwrap_or(false) - || desktop_file.entry.no_display.unwrap_or(false) - { - return None; - } - - if let EntryType::Application(app_fields) = desktop_file.entry.entry_type { - if app_fields.exec.is_some() && !desktop_file.entry.name.default.is_empty() { - return Some(App { - name: desktop_file.entry.name.default, - comment: desktop_file.entry.comment.map(|lc| lc.default), - exec: app_fields.exec, - icon_path: desktop_file - .entry - .icon - .and_then(|ic| ic.get_icon_path()) - .and_then(|p| p.to_str().map(String::from)), - }); - } - } - 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| { - fs::metadata(&dir) - .and_then(|m| m.modified()) - .ok() - .map(|mod_time| (dir, mod_time)) - }) - .collect(); - - Ok((unique_apps, dir_mod_times)) -} - -fn read_cache(path: &Path) -> Result { - let file_content = fs::read(path)?; - let (decoded, _) = bincode::serde::decode_from_slice(&file_content, bincode::config::standard())?; - Ok(decoded) -} - -fn write_cache(path: &Path, cache: &AppCache) -> Result<(), CacheError> { - let encoded = bincode::serde::encode_to_vec(cache, bincode::config::standard())?; - fs::write(path, encoded)?; - Ok(()) -} - -fn refresh_app_cache() { - if let (Ok(cache_path), Ok((apps, dir_mod_times))) = - (get_cache_path(), scan_and_parse_apps()) - { - let cache_data = AppCache { - apps, - dir_mod_times, - }; - if let Err(e) = write_cache(&cache_path, &cache_data) { - eprintln!("Error refreshing app cache in background: {:?}", e); - } - } -} +use selection::get_text; +use std::{process::Command, thread, time::Duration}; #[tauri::command] fn get_installed_apps() -> Vec { - let cache_path = match get_cache_path() { - Ok(path) => path, + match AppCache::get_apps() { + Ok(apps) => apps, Err(e) => { - eprintln!("Could not get cache path: {:?}. Falling back to scan.", e); - return scan_and_parse_apps().map_or_else(|_| Vec::new(), |(apps, _)| apps); - } - }; - - if let Ok(cached_data) = read_cache(&cache_path) { - 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; - } - } - - match scan_and_parse_apps() { - Ok((apps, dir_mod_times)) => { - let cache_data = AppCache { apps: apps.clone(), dir_mod_times }; - if let Err(e) = write_cache(&cache_path, &cache_data) { - eprintln!("Failed to write to app cache: {:?}", e); - } - apps - } - Err(e) => { - eprintln!("Failed to scan and parse apps: {:?}", e); + eprintln!("Failed to get apps: {:?}", e); Vec::new() } } @@ -257,7 +62,7 @@ pub fn run() { thread::spawn(|| { thread::sleep(Duration::from_secs(60)); loop { - refresh_app_cache(); + AppCache::refresh_background(); thread::sleep(Duration::from_secs(300)); } });