From cd0147fb16d2f0bd9bdbb22958119170fb7fbd05 Mon Sep 17 00:00:00 2001 From: ByteAtATime Date: Mon, 16 Jun 2025 20:42:43 -0700 Subject: [PATCH] feat: list installed apps to PluginList Added functionality to the PluginList component to display installed applications alongside plugins. Implemented search filtering for installed apps and integrated app launching capabilities. Updated the page to fetch installed apps from the backend, improving user interaction and experience. --- src-tauri/Cargo.lock | 91 +++++++++++++++++++++-- src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 107 +++++++++++++++++++++++++++ src-tauri/tauri.conf.json | 4 +- src/lib/components/PluginList.svelte | 70 ++++++++++++++++-- src/routes/+page.svelte | 9 ++- 6 files changed, 266 insertions(+), 16 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 90e2194..f37bb4d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -747,13 +747,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -764,7 +785,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.0", "windows-sys 0.59.0", ] @@ -1052,6 +1073,30 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freedesktop-file-parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9874624824ee3ca96cf728451815bd93ae3978cdcd715c1d098edef7130fb0da" +dependencies = [ + "freedesktop-icons", + "thiserror 2.0.12", +] + +[[package]] +name = "freedesktop-icons" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f87364ea709292a3b3f74014ce3ee78412c89807eea75a358c8e029b000994" +dependencies = [ + "dirs 5.0.1", + "ini_core", + "once_cell", + "thiserror 1.0.69", + "tracing", + "xdg", +] + [[package]] name = "futf" version = "0.1.5" @@ -1793,6 +1838,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "ini_core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a467a31a9f439b5262fa99c17084537bff57f24703d5a09a2b5c9657ec73a61" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3001,6 +3055,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" name = "raycast-linux" version = "0.1.0" dependencies = [ + "freedesktop-file-parser", "serde", "serde_json", "tauri", @@ -3019,6 +3074,17 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.0" @@ -3669,7 +3735,7 @@ checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc" dependencies = [ "anyhow", "bytes", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "futures-util", @@ -3721,7 +3787,7 @@ checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4245,7 +4311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2 0.6.1", @@ -4882,6 +4948,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5240,6 +5315,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "yoke" version = "0.8.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5bd74a5..4a6beb4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,4 +24,5 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-clipboard-manager = "2" tauri-plugin-shell = "2" +freedesktop-file-parser = "0.2.0" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 71e6a5d..e24c093 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,9 +1,116 @@ +use std::{ + collections::HashSet, env, fs, path::{Path, PathBuf}, process::Command +}; + +use freedesktop_file_parser::{parse, EntryType}; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone)] +pub struct App { + pub name: String, + pub comment: Option, + pub exec: Option, + pub icon_path: Option, +} + +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 let Some(extension) = path.extension() { + if extension == "desktop" { + desktop_files.push(path); + } + } + } + } + desktop_files +} + +#[tauri::command] +fn get_installed_apps() -> 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") { + let mut user_app_dir = PathBuf::from(home_dir); + user_app_dir.push(".local/share/applications"); + app_dirs.push(user_app_dir); + } + + let mut apps = Vec::new(); + let mut seen_app_ids = HashSet::new(); + + 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; + + 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); + } + } + } + } + } + } + + apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + apps +} + +#[tauri::command] +fn launch_app(exec: String) -> Result<(), String> { + let exec_parts: Vec<&str> = exec.split_whitespace().collect(); + if exec_parts.is_empty() { + return Err("Empty exec command".to_string()); + } + + let mut command = Command::new(exec_parts[0]); + + for arg in &exec_parts[1..] { + if !arg.starts_with('%') { + command.arg(arg); + } + } + + command + .spawn() + .map_err(|e| format!("Failed to launch app: {}", e))?; + + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_opener::init()) + .invoke_handler(tauri::generate_handler![get_installed_apps, launch_app]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2ba90cc..6fa32e6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -18,10 +18,10 @@ } ], "security": { - "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost", + "csp": "http://asset.localhost", "assetProtocol": { "enable": true, - "scope": ["/home/byte/code/raycast-linux/sidecar/dist/plugin/assets/**/*"] + "scope": ["**"] } } }, diff --git a/src/lib/components/PluginList.svelte b/src/lib/components/PluginList.svelte index b4bef4d..7049ad6 100644 --- a/src/lib/components/PluginList.svelte +++ b/src/lib/components/PluginList.svelte @@ -5,13 +5,16 @@ import { create, all } from 'mathjs'; import { ArrowRight } from '@lucide/svelte'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; + import { convertFileSrc } from '@tauri-apps/api/core'; + import { invoke } from '@tauri-apps/api/core'; type Props = { plugins: PluginInfo[]; onRunPlugin: (plugin: PluginInfo) => void; + installedApps?: any[]; }; - let { plugins, onRunPlugin }: Props = $props(); + let { plugins, onRunPlugin, installedApps = [] }: Props = $props(); let searchText = $state(''); let selectedIndex = $state(0); @@ -32,6 +35,17 @@ ); }); + const filteredApps = $derived.by(() => { + if (!searchText) return installedApps; + const lowerCaseSearch = searchText.toLowerCase(); + return installedApps.filter( + (app: any) => + app.name.toLowerCase().includes(lowerCaseSearch) || + app.comment?.toLowerCase().includes(lowerCaseSearch) || + app.exec.toLowerCase().includes(lowerCaseSearch) + ); + }); + $effect(() => { if (!searchText.trim()) { mathResult = null; @@ -82,14 +96,14 @@ }); $effect(() => { - const totalItems = (hasMathResult ? 1 : 0) + filteredPlugins.length; + const totalItems = (hasMathResult ? 1 : 0) + filteredPlugins.length + filteredApps.length; if (selectedIndex >= totalItems) { selectedIndex = Math.max(0, totalItems - 1); } }); function handleKeydown(event: KeyboardEvent) { - const totalItems = (hasMathResult ? 1 : 0) + filteredPlugins.length; + const totalItems = (hasMathResult ? 1 : 0) + filteredPlugins.length + filteredApps.length; if (totalItems === 0) return; if (event.key === 'ArrowDown') { @@ -103,9 +117,17 @@ if (hasMathResult && selectedIndex === 0) { if (mathResult) writeText(mathResult); } else { - const pluginIndex = selectedIndex - (hasMathResult ? 1 : 0); - if (filteredPlugins[pluginIndex]) { - onRunPlugin(filteredPlugins[pluginIndex]); + const itemIndex = selectedIndex - (hasMathResult ? 1 : 0); + if (itemIndex < filteredPlugins.length) { + onRunPlugin(filteredPlugins[itemIndex]); + } else { + const appIndex = itemIndex - filteredPlugins.length; + if (filteredApps[appIndex]) { + const app = filteredApps[appIndex]; + if (app.exec) { + invoke('launch_app', { exec: app.exec }).catch(console.error); + } + } } } } @@ -113,8 +135,18 @@ function handleItemClick(index: number) { selectedIndex = index; - const pluginIndex = index - (hasMathResult ? 1 : 0); - onRunPlugin(filteredPlugins[pluginIndex]); + const itemIndex = index - (hasMathResult ? 1 : 0); + if (itemIndex < filteredPlugins.length) { + onRunPlugin(filteredPlugins[itemIndex]); + } else { + const appIndex = itemIndex - filteredPlugins.length; + if (filteredApps[appIndex]) { + const app = filteredApps[appIndex]; + if (app.exec) { + invoke('launch_app', { exec: app.exec }).catch(console.error); + } + } + } } function numberToWords(numStr: string | null): string { @@ -202,6 +234,28 @@ {plugin.pluginName} {/each} + {#each filteredApps as app, index} + {@const itemIndex = index + filteredPlugins.length + (hasMathResult ? 1 : 0)} + + {/each} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f7e71de..38aa24c 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -10,10 +10,12 @@ import PluginList from '$lib/components/PluginList.svelte'; import SettingsView from '$lib/components/SettingsView.svelte'; import type { PluginInfo } from '@raycast-linux/protocol'; + import { invoke } from '@tauri-apps/api/core'; type ViewState = 'plugin-list' | 'plugin-running' | 'settings'; let viewState = $state('plugin-list'); + let installedApps = $state([]); const { uiTree, rootNodeId, selectedNodeId, pluginList, currentPreferences } = $derived(uiStore); @@ -149,12 +151,17 @@ function handleGetPreferences(pluginName: string) { sidecarService.getPreferences(pluginName); } + + invoke('get_installed_apps').then((apps) => { + console.log(apps); + installedApps = apps as any[]; + }); {#if viewState === 'plugin-list'} - + {:else if viewState === 'settings'}