mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-30 18:57:25 +00:00
feat: implement getSelectedFinderItems functionality
Added schemas and message handling for the new get-selected-finder-items command. Implemented the logic to retrieve selected items from the Finder on macOS, Windows, and Linux, enhancing the application's ability to interact with the file system.
This commit is contained in:
parent
a3c2929e1c
commit
0c94e973a0
8 changed files with 357 additions and 9 deletions
|
@ -140,6 +140,14 @@ const GetSelectedTextMessageSchema = z.object({
|
|||
payload: GetSelectedTextPayloadSchema
|
||||
});
|
||||
|
||||
const GetSelectedFinderItemsPayloadSchema = z.object({
|
||||
requestId: z.string()
|
||||
});
|
||||
const GetSelectedFinderItemsMessageSchema = z.object({
|
||||
type: z.literal('get-selected-finder-items'),
|
||||
payload: GetSelectedFinderItemsPayloadSchema
|
||||
});
|
||||
|
||||
export const SidecarMessageWithPluginsSchema = z.union([
|
||||
BatchUpdateSchema,
|
||||
CommandSchema,
|
||||
|
@ -148,6 +156,7 @@ export const SidecarMessageWithPluginsSchema = z.union([
|
|||
PreferenceValuesSchema,
|
||||
GoBackToPluginListSchema,
|
||||
OpenMessageSchema,
|
||||
GetSelectedTextMessageSchema
|
||||
GetSelectedTextMessageSchema,
|
||||
GetSelectedFinderItemsMessageSchema
|
||||
]);
|
||||
export type SidecarMessageWithPlugins = z.infer<typeof SidecarMessageWithPluginsSchema>;
|
||||
|
|
|
@ -33,8 +33,45 @@ export const environment = {
|
|||
}
|
||||
};
|
||||
|
||||
const pendingFinderItemsRequests = new Map<
|
||||
string,
|
||||
{ resolve: (items: FileSystemItem[]) => void; reject: (error: Error) => void }
|
||||
>();
|
||||
|
||||
export async function getSelectedFinderItems(): Promise<FileSystemItem[]> {
|
||||
return Promise.reject(new Error('Finder is not the frontmost application.'));
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = Math.random().toString(36).substring(7);
|
||||
|
||||
pendingFinderItemsRequests.set(requestId, { resolve, reject });
|
||||
|
||||
writeOutput({
|
||||
type: 'get-selected-finder-items',
|
||||
payload: { requestId }
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (pendingFinderItemsRequests.has(requestId)) {
|
||||
pendingFinderItemsRequests.delete(requestId);
|
||||
reject(new Error('Timeout: Could not get selected finder items'));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
export function handleGetSelectedFinderItemsResponse(
|
||||
requestId: string,
|
||||
items: FileSystemItem[] | null,
|
||||
error?: string
|
||||
) {
|
||||
const pending = pendingFinderItemsRequests.get(requestId);
|
||||
if (pending) {
|
||||
pendingFinderItemsRequests.delete(requestId);
|
||||
if (error) {
|
||||
pending.reject(new Error(error));
|
||||
} else {
|
||||
pending.resolve(items || []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pendingTextRequests = new Map<
|
||||
|
|
|
@ -5,7 +5,11 @@ import { instances, navigationStack } from './state';
|
|||
import { batchedUpdates, updateContainer } from './reconciler';
|
||||
import { preferencesStore } from './preferences';
|
||||
import type { RaycastInstance } from './types';
|
||||
import { handleSelectedTextResponse } from './api/environment';
|
||||
import {
|
||||
handleGetSelectedFinderItemsResponse,
|
||||
handleSelectedTextResponse,
|
||||
type FileSystemItem
|
||||
} from './api/environment';
|
||||
|
||||
process.on('unhandledRejection', (reason: unknown) => {
|
||||
writeLog(`--- UNHANDLED PROMISE REJECTION ---`);
|
||||
|
@ -102,6 +106,15 @@ rl.on('line', (line) => {
|
|||
handleSelectedTextResponse(requestId, text ?? null, error);
|
||||
break;
|
||||
}
|
||||
case 'selected-finder-items-response': {
|
||||
const { requestId, items, error } = command.payload as {
|
||||
requestId: string;
|
||||
items?: FileSystemItem[] | null;
|
||||
error?: string;
|
||||
};
|
||||
handleGetSelectedFinderItemsResponse(requestId, items ?? null, error);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
writeLog(`Unknown command action: ${command.action}`);
|
||||
}
|
||||
|
|
3
src-tauri/Cargo.lock
generated
3
src-tauri/Cargo.lock
generated
|
@ -3323,6 +3323,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
|||
name = "raycast-linux"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"bincode",
|
||||
"freedesktop-file-parser",
|
||||
"rayon",
|
||||
|
@ -3334,6 +3335,8 @@ dependencies = [
|
|||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
"url",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -28,4 +28,7 @@ freedesktop-file-parser = "0.2.0"
|
|||
bincode = { version = "2.0.1", features = ["serde"] }
|
||||
rayon = "1.10.0"
|
||||
selection = "1.2.0"
|
||||
url = "2.5.4"
|
||||
arboard = "3.5.0"
|
||||
zbus = "5.7.1"
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ use std::io;
|
|||
pub enum AppError {
|
||||
Io(io::Error),
|
||||
Serialization(String),
|
||||
DirectoryNotFound
|
||||
DirectoryNotFound,
|
||||
CacheError(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for AppError {
|
||||
|
|
|
@ -3,12 +3,274 @@ mod cache;
|
|||
mod desktop;
|
||||
mod error;
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
cache::AppCache,
|
||||
};
|
||||
#[cfg(target_os = "linux")]
|
||||
use arboard;
|
||||
use crate::{app::App, cache::AppCache};
|
||||
use selection::get_text;
|
||||
use std::{process::Command, thread, time::Duration};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::path::Path;
|
||||
#[cfg(target_os = "linux")]
|
||||
use url::Url;
|
||||
#[cfg(target_os = "linux")]
|
||||
use zbus;
|
||||
|
||||
#[derive(serde::Serialize, Clone, Debug)]
|
||||
pub struct FileSystemItem {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_selected_finder_items() -> Result<Vec<FileSystemItem>, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
get_selected_finder_items_macos()
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
get_selected_finder_items_windows()
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
get_selected_finder_items_linux().await
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
Err("Unsupported operating system".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_selected_finder_items_macos() -> Result<Vec<FileSystemItem>, String> {
|
||||
let script = r#"
|
||||
tell application "Finder"
|
||||
if not running then
|
||||
return ""
|
||||
end if
|
||||
try
|
||||
set theSelection to selection
|
||||
if theSelection is {} then
|
||||
return ""
|
||||
end if
|
||||
set thePaths to {}
|
||||
repeat with i from 1 to count of theSelection
|
||||
set end of thePaths to (POSIX path of (item i of theSelection as alias))
|
||||
end repeat
|
||||
return thePaths
|
||||
on error
|
||||
return ""
|
||||
end try
|
||||
end tell
|
||||
"#;
|
||||
|
||||
let output = std::process::Command::new("osascript")
|
||||
.arg("-l")
|
||||
.arg("AppleScript")
|
||||
.arg("-e")
|
||||
.arg(script)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute osascript: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error_message = String::from_utf8_lossy(&output.stderr);
|
||||
if error_message.contains("Finder is not running") {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
return Err(format!("osascript failed with error: {}", error_message));
|
||||
}
|
||||
|
||||
let result_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
|
||||
if result_str.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let paths: Vec<FileSystemItem> = result_str
|
||||
.split(", ")
|
||||
.map(|p| p.trim())
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(|p| FileSystemItem {
|
||||
path: p.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_selected_finder_items_windows() -> Result<Vec<FileSystemItem>, String> {
|
||||
let script = r#"
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class Win32 {
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
}
|
||||
"@
|
||||
$foreground_hwnd = [Win32]::GetForegroundWindow()
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$window = $shell.Windows() | Where-Object { $_.HWND -eq $foreground_hwnd } | Select-Object -First 1
|
||||
if ($window) {
|
||||
if ($window.FullName -like "*\explorer.exe") {
|
||||
$selection = $window.Document.SelectedItems()
|
||||
if ($selection) {
|
||||
$paths = $selection | ForEach-Object { $_.Path }
|
||||
if ($paths) {
|
||||
return $paths -join [System.Environment]::NewLine
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
"#;
|
||||
|
||||
let output = std::process::Command::new("powershell")
|
||||
.arg("-NoProfile")
|
||||
.arg("-ExecutionPolicy")
|
||||
.arg("Bypass")
|
||||
.arg("-Command")
|
||||
.arg(script)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute powershell: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"powershell failed with error: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let result_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
|
||||
if result_str.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let paths: Vec<FileSystemItem> = result_str
|
||||
.lines()
|
||||
.map(|p| p.trim())
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(|p| FileSystemItem {
|
||||
path: p.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_selected_finder_items_linux() -> Result<Vec<FileSystemItem>, String> {
|
||||
if let Ok(paths) = get_from_file_manager().await {
|
||||
if !paths.is_empty() {
|
||||
return Ok(paths);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(paths) = get_from_clipboard() {
|
||||
if !paths.is_empty() {
|
||||
return Ok(paths);
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not determine selected files. Please copy them to your clipboard.".to_string())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_from_file_manager() -> Result<Vec<FileSystemItem>, String> {
|
||||
let connection = match zbus::Connection::session().await {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Ok(vec![]),
|
||||
};
|
||||
|
||||
let proxy = match zbus::Proxy::new(
|
||||
&connection,
|
||||
"org.freedesktop.FileManager1",
|
||||
"/org/freedesktop/FileManager1",
|
||||
"org.freedesktop.FileManager1",
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => return Ok(vec![]),
|
||||
};
|
||||
|
||||
let fm_service: String = proxy.destination().to_string();
|
||||
if fm_service.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let fm_name = fm_service.split('.').last().unwrap_or_default();
|
||||
let window_interface = format!("org.{}.Window", fm_name);
|
||||
|
||||
if fm_name != "nautilus" && fm_name != "nemo" {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let response = match proxy.call_method("GetWindows", &()).await {
|
||||
Ok(r) => r,
|
||||
Err(_) => return Ok(vec![]),
|
||||
};
|
||||
|
||||
let body = response.body();
|
||||
let windows: Vec<zbus::zvariant::ObjectPath> = body.deserialize().unwrap_or_default();
|
||||
|
||||
for window_path in windows.iter().rev() {
|
||||
let fm_service_ref = fm_service.as_str();
|
||||
let window_interface_ref = window_interface.as_str();
|
||||
let window_proxy = match zbus::Proxy::new(
|
||||
&connection,
|
||||
fm_service_ref,
|
||||
window_path,
|
||||
window_interface_ref,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Ok(is_active) = window_proxy.get_property::<bool>("Active").await {
|
||||
if is_active {
|
||||
if let Ok(uris) = window_proxy.get_property::<Vec<String>>("SelectedUris").await {
|
||||
let paths = uris
|
||||
.iter()
|
||||
.filter_map(|uri_str| Url::parse(uri_str).ok())
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.map(|path_buf| FileSystemItem {
|
||||
path: path_buf.to_string_lossy().into_owned(),
|
||||
})
|
||||
.collect();
|
||||
return Ok(paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_from_clipboard() -> Result<Vec<FileSystemItem>, String> {
|
||||
let mut clipboard = arboard::Clipboard::new().map_err(|e| e.to_string())?;
|
||||
if let Ok(text) = clipboard.get_text() {
|
||||
let paths: Vec<FileSystemItem> = text
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("file://") {
|
||||
Url::parse(trimmed).ok().and_then(|u| u.to_file_path().ok())
|
||||
} else {
|
||||
Some(Path::new(trimmed).to_path_buf())
|
||||
}
|
||||
})
|
||||
.filter(|p| p.exists())
|
||||
.map(|p| FileSystemItem {
|
||||
path: p.to_string_lossy().to_string(),
|
||||
})
|
||||
.collect();
|
||||
return Ok(paths);
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_installed_apps() -> Vec<App> {
|
||||
|
@ -56,7 +318,8 @@ pub fn run() {
|
|||
.invoke_handler(tauri::generate_handler![
|
||||
get_installed_apps,
|
||||
launch_app,
|
||||
get_selected_text
|
||||
get_selected_text,
|
||||
get_selected_finder_items
|
||||
])
|
||||
.setup(|_app| {
|
||||
thread::spawn(|| {
|
||||
|
|
|
@ -169,6 +169,25 @@ class SidecarService {
|
|||
return;
|
||||
}
|
||||
|
||||
if (typedMessage.type === 'get-selected-finder-items') {
|
||||
const { requestId } = typedMessage.payload;
|
||||
invoke('get_selected_finder_items')
|
||||
.then((items) => {
|
||||
this.dispatchEvent('selected-finder-items-response', {
|
||||
requestId,
|
||||
items
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.#log(`ERROR getting selected finder items: ${error}`);
|
||||
this.dispatchEvent('selected-finder-items-response', {
|
||||
requestId,
|
||||
error: String(error)
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = typedMessage.type === 'BATCH_UPDATE' ? typedMessage.payload : [typedMessage];
|
||||
if (commands.length > 0) {
|
||||
uiStore.applyCommands(commands);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue