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.
This commit is contained in:
ByteAtATime 2025-06-16 20:42:43 -07:00
parent 94bce52e62
commit cd0147fb16
No known key found for this signature in database
6 changed files with 266 additions and 16 deletions

91
src-tauri/Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<String>,
pub exec: Option<String>,
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
}
#[tauri::command]
fn get_installed_apps() -> Vec<App> {
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");
}

View file

@ -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": ["**"]
}
}
},

View file

@ -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 @@
<span class="ml-auto text-xs text-gray-500">{plugin.pluginName}</span>
</button>
{/each}
{#each filteredApps as app, index}
{@const itemIndex = index + filteredPlugins.length + (hasMathResult ? 1 : 0)}
<button
type="button"
class="hover:bg-accent/50 flex w-full items-center gap-3 px-4 py-2 text-left"
class:bg-accent={selectedIndex === itemIndex}
onclick={() => handleItemClick(itemIndex)}
>
<div class="flex size-5 shrink-0 items-center justify-center">
{#if app.icon_path}
<img src={convertFileSrc(app.icon_path)} alt="" class="size-4" />
{:else}
<Icon icon="app-window-16" class="size-4" />
{/if}
</div>
<div class="flex flex-col">
<span class="font-medium">{app.name}</span>
<span class="text-muted-foreground text-sm">{app.comment || 'No description'}</span>
</div>
<span class="ml-auto text-xs text-gray-500">System App</span>
</button>
{/each}
</div>
</div>
</main>

View file

@ -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<ViewState>('plugin-list');
let installedApps = $state<any[]>([]);
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[];
});
</script>
<svelte:window onkeydown={handleKeydown} />
{#if viewState === 'plugin-list'}
<PluginList plugins={pluginList} onRunPlugin={handleRunPlugin} />
<PluginList plugins={pluginList} onRunPlugin={handleRunPlugin} {installedApps} />
{:else if viewState === 'settings'}
<SettingsView
plugins={pluginList}