mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 19:27:24 +00:00
refactor: split lib.rs file
This commit is contained in:
parent
2cb858a143
commit
a598ccf50b
4 changed files with 480 additions and 469 deletions
|
@ -158,4 +158,58 @@ pub async fn run_server(app_handle: AppHandle) {
|
|||
while let Ok((stream, _)) = listener.accept().await {
|
||||
tokio::spawn(handle_connection(stream, app_handle.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn browser_extension_check_connection(state: tauri::State<'_, WsState>) -> Result<bool, String> {
|
||||
Ok(*state.is_connected.lock().unwrap())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn browser_extension_request(
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
state: tauri::State<'_, WsState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
use std::time::Duration;
|
||||
|
||||
let tx = {
|
||||
let lock = state.connection.lock().unwrap();
|
||||
lock.clone()
|
||||
};
|
||||
|
||||
if let Some(tx) = tx {
|
||||
let request_id = {
|
||||
let mut counter = state.request_id_counter.lock().unwrap();
|
||||
*counter += 1;
|
||||
*counter
|
||||
};
|
||||
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": request_id
|
||||
});
|
||||
|
||||
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
|
||||
state
|
||||
.pending_requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(request_id, response_tx);
|
||||
|
||||
if tx.send(request.to_string()).await.is_err() {
|
||||
return Err("Failed to send message to browser extension".into());
|
||||
}
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(5), response_rx).await {
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(_)) => Err("Request cancelled".into()),
|
||||
Err(_) => Err("Request timed out".into()),
|
||||
}
|
||||
} else {
|
||||
Err("Browser extension not connected".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
115
src-tauri/src/extensions.rs
Normal file
115
src-tauri/src/extensions.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use std::fs;
|
||||
use std::io::{self, Cursor};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_extension(
|
||||
app: tauri::AppHandle,
|
||||
download_url: String,
|
||||
slug: String,
|
||||
) -> Result<(), String> {
|
||||
let data_dir = app
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.or_else(|_| Err("Failed to get app local data dir".to_string()))?;
|
||||
|
||||
let plugins_dir = data_dir.join("plugins");
|
||||
let extension_dir = plugins_dir.join(&slug);
|
||||
|
||||
if !plugins_dir.exists() {
|
||||
fs::create_dir_all(&plugins_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
if extension_dir.exists() {
|
||||
fs::remove_dir_all(&extension_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let response = reqwest::get(&download_url)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download extension: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"Failed to download extension: status code {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let content = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response bytes: {}", e))?;
|
||||
|
||||
let mut archive = zip::ZipArchive::new(Cursor::new(content)).map_err(|e| e.to_string())?;
|
||||
|
||||
let prefix_to_strip = {
|
||||
let file_names: Vec<PathBuf> = archive.file_names().map(PathBuf::from).collect();
|
||||
|
||||
if file_names.len() <= 1 {
|
||||
None
|
||||
} else {
|
||||
let first_path = &file_names[0];
|
||||
if let Some(first_component) = first_path.components().next() {
|
||||
if file_names
|
||||
.iter()
|
||||
.all(|path| path.starts_with(first_component))
|
||||
{
|
||||
Some(PathBuf::from(first_component.as_os_str()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
|
||||
|
||||
let enclosed_path = match file.enclosed_name() {
|
||||
Some(path) => path.to_path_buf(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let final_path_part = if let Some(ref prefix) = prefix_to_strip {
|
||||
enclosed_path
|
||||
.strip_prefix(prefix)
|
||||
.unwrap_or(&enclosed_path)
|
||||
.to_path_buf()
|
||||
} else {
|
||||
enclosed_path
|
||||
};
|
||||
|
||||
if final_path_part.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let outpath = extension_dir.join(final_path_part);
|
||||
|
||||
if file.name().ends_with('/') {
|
||||
fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
if let Some(p) = outpath.parent() {
|
||||
if !p.exists() {
|
||||
fs::create_dir_all(&p).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
let mut outfile = fs::File::create(&outpath).map_err(|e| e.to_string())?;
|
||||
io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Some(mode) = file.unix_mode() {
|
||||
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
266
src-tauri/src/filesystem.rs
Normal file
266
src-tauri/src/filesystem.rs
Normal file
|
@ -0,0 +1,266 @@
|
|||
#[derive(serde::Serialize, Clone, Debug)]
|
||||
pub struct FileSystemItem {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub 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_from_file_manager() -> Result<Vec<FileSystemItem>, String> {
|
||||
use url::Url;
|
||||
use zbus;
|
||||
|
||||
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> {
|
||||
use arboard;
|
||||
use std::path::Path;
|
||||
use url::Url;
|
||||
|
||||
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![])
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
|
@ -3,286 +3,16 @@ mod browser_extension;
|
|||
mod cache;
|
||||
mod desktop;
|
||||
mod error;
|
||||
mod extensions;
|
||||
mod filesystem;
|
||||
|
||||
use crate::{app::App, cache::AppCache};
|
||||
#[cfg(target_os = "linux")]
|
||||
use arboard;
|
||||
use browser_extension::WsState;
|
||||
use serde_json::json;
|
||||
use selection::get_text;
|
||||
use std::fs;
|
||||
use std::io::{self, Cursor};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::{Manager, State};
|
||||
#[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_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![])
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
fn get_installed_apps() -> Vec<App> {
|
||||
|
@ -321,167 +51,51 @@ fn get_selected_text() -> String {
|
|||
get_text()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn install_extension(
|
||||
app: tauri::AppHandle,
|
||||
download_url: String,
|
||||
slug: String,
|
||||
) -> Result<(), String> {
|
||||
let data_dir = app
|
||||
.path()
|
||||
.app_local_data_dir()
|
||||
.or_else(|_| Err("Failed to get app local data dir".to_string()))?;
|
||||
fn setup_background_refresh() {
|
||||
thread::spawn(|| {
|
||||
thread::sleep(Duration::from_secs(60));
|
||||
loop {
|
||||
AppCache::refresh_background();
|
||||
thread::sleep(Duration::from_secs(300));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let plugins_dir = data_dir.join("plugins");
|
||||
let extension_dir = plugins_dir.join(&slug);
|
||||
fn setup_global_shortcut(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use tauri_plugin_global_shortcut::{
|
||||
Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState,
|
||||
};
|
||||
|
||||
if !plugins_dir.exists() {
|
||||
fs::create_dir_all(&plugins_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let spotlight_shortcut = Shortcut::new(Some(Modifiers::ALT), Code::Space);
|
||||
let handle = app.handle().clone();
|
||||
|
||||
if extension_dir.exists() {
|
||||
fs::remove_dir_all(&extension_dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
println!("Spotlight shortcut: {:?}", spotlight_shortcut);
|
||||
|
||||
let response = reqwest::get(&download_url)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to download extension: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"Failed to download extension: status code {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
let content = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read response bytes: {}", e))?;
|
||||
|
||||
let mut archive = zip::ZipArchive::new(Cursor::new(content)).map_err(|e| e.to_string())?;
|
||||
|
||||
let prefix_to_strip = {
|
||||
let file_names: Vec<PathBuf> = archive.file_names().map(PathBuf::from).collect();
|
||||
|
||||
if file_names.len() <= 1 {
|
||||
None
|
||||
} else {
|
||||
let first_path = &file_names[0];
|
||||
if let Some(first_component) = first_path.components().next() {
|
||||
if file_names
|
||||
.iter()
|
||||
.all(|path| path.starts_with(first_component))
|
||||
app.handle().plugin(
|
||||
tauri_plugin_global_shortcut::Builder::new()
|
||||
.with_handler(move |_app, shortcut, event| {
|
||||
println!("Shortcut: {:?}, Event: {:?}", shortcut, event);
|
||||
if shortcut == &spotlight_shortcut
|
||||
&& event.state() == ShortcutState::Pressed
|
||||
{
|
||||
Some(PathBuf::from(first_component.as_os_str()))
|
||||
} else {
|
||||
None
|
||||
let spotlight_window =
|
||||
handle.get_webview_window("raycast-linux").unwrap();
|
||||
println!("Spotlight window: {:?}", spotlight_window);
|
||||
if spotlight_window.is_visible().unwrap_or(false) {
|
||||
spotlight_window.hide().unwrap();
|
||||
} else {
|
||||
spotlight_window.show().unwrap();
|
||||
spotlight_window.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
|
||||
|
||||
let enclosed_path = match file.enclosed_name() {
|
||||
Some(path) => path.to_path_buf(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let final_path_part = if let Some(ref prefix) = prefix_to_strip {
|
||||
enclosed_path
|
||||
.strip_prefix(prefix)
|
||||
.unwrap_or(&enclosed_path)
|
||||
.to_path_buf()
|
||||
} else {
|
||||
enclosed_path
|
||||
};
|
||||
|
||||
if final_path_part.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let outpath = extension_dir.join(final_path_part);
|
||||
|
||||
if file.name().ends_with('/') {
|
||||
fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
if let Some(p) = outpath.parent() {
|
||||
if !p.exists() {
|
||||
fs::create_dir_all(&p).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
let mut outfile = fs::File::create(&outpath).map_err(|e| e.to_string())?;
|
||||
io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Some(mode) = file.unix_mode() {
|
||||
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(),
|
||||
)?;
|
||||
|
||||
app.global_shortcut().register(spotlight_shortcut)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn browser_extension_check_connection(state: State<'_, WsState>) -> Result<bool, String> {
|
||||
Ok(*state.is_connected.lock().unwrap())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn browser_extension_request(
|
||||
method: String,
|
||||
params: serde_json::Value,
|
||||
state: State<'_, WsState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let tx = {
|
||||
let lock = state.connection.lock().unwrap();
|
||||
lock.clone()
|
||||
};
|
||||
|
||||
if let Some(tx) = tx {
|
||||
let request_id = {
|
||||
let mut counter = state.request_id_counter.lock().unwrap();
|
||||
*counter += 1;
|
||||
*counter
|
||||
};
|
||||
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": request_id
|
||||
});
|
||||
|
||||
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
|
||||
state
|
||||
.pending_requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(request_id, response_tx);
|
||||
|
||||
if tx.send(request.to_string()).await.is_err() {
|
||||
return Err("Failed to send message to browser extension".into());
|
||||
}
|
||||
|
||||
match tokio::time::timeout(Duration::from_secs(5), response_rx).await {
|
||||
Ok(Ok(result)) => result,
|
||||
Ok(Err(_)) => Err("Request cancelled".into()),
|
||||
Err(_) => Err("Request timed out".into()),
|
||||
}
|
||||
} else {
|
||||
Err("Browser extension not connected".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
|
@ -504,55 +118,17 @@ pub fn run() {
|
|||
get_installed_apps,
|
||||
launch_app,
|
||||
get_selected_text,
|
||||
get_selected_finder_items,
|
||||
install_extension,
|
||||
browser_extension_check_connection,
|
||||
browser_extension_request
|
||||
filesystem::get_selected_finder_items,
|
||||
extensions::install_extension,
|
||||
browser_extension::browser_extension_check_connection,
|
||||
browser_extension::browser_extension_request
|
||||
])
|
||||
.setup(|app| {
|
||||
use tauri_plugin_global_shortcut::{
|
||||
Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState,
|
||||
};
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(browser_extension::run_server(app_handle));
|
||||
|
||||
thread::spawn(|| {
|
||||
thread::sleep(Duration::from_secs(60));
|
||||
loop {
|
||||
AppCache::refresh_background();
|
||||
thread::sleep(Duration::from_secs(300));
|
||||
}
|
||||
});
|
||||
|
||||
let spotlight_shortcut = Shortcut::new(Some(Modifiers::ALT), Code::Space);
|
||||
|
||||
let handle = app.handle().clone();
|
||||
|
||||
println!("Spotlight shortcut: {:?}", spotlight_shortcut);
|
||||
|
||||
app.handle().plugin(
|
||||
tauri_plugin_global_shortcut::Builder::new()
|
||||
.with_handler(move |_app, shortcut, event| {
|
||||
println!("Shortcut: {:?}, Event: {:?}", shortcut, event);
|
||||
if shortcut == &spotlight_shortcut
|
||||
&& event.state() == ShortcutState::Pressed
|
||||
{
|
||||
let spotlight_window =
|
||||
handle.get_webview_window("raycast-linux").unwrap();
|
||||
println!("Spotlight window: {:?}", spotlight_window);
|
||||
if spotlight_window.is_visible().unwrap_or(false) {
|
||||
spotlight_window.hide().unwrap();
|
||||
} else {
|
||||
spotlight_window.show().unwrap();
|
||||
spotlight_window.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(),
|
||||
)?;
|
||||
|
||||
app.global_shortcut().register(spotlight_shortcut)?;
|
||||
setup_background_refresh();
|
||||
setup_global_shortcut(app)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue