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'}