mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-09-12 08:56:23 +00:00
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:
parent
94bce52e62
commit
cd0147fb16
6 changed files with 266 additions and 16 deletions
91
src-tauri/Cargo.lock
generated
91
src-tauri/Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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": ["**"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue