[ENG-593] Implement Open With for Linux (#939)

* Implement `Open With` for Linux

* Fix TS typechecker failing and Rust formatting

* Take args by ref

---------

Co-authored-by: Ericson Fogo Soares <ericson.ds999@gmail.com>
This commit is contained in:
Vítor Vasconcellos 2023-06-13 21:54:43 -03:00 committed by GitHub
parent 1889a767e8
commit e6d0e6098c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 553 additions and 47 deletions

View file

@ -319,7 +319,7 @@ if (-not ($filename -and $downloadUri)) {
}
Write-Host "Dowloading protobuf zip from ${downloadUri}..." -ForegroundColor Yellow
Start-BitsTransfer -TransferType Download -Source $downloadUri -Destination "$temp\protobuf.zip"
Invoke-RestMethodGithub -Uri $downloadUri -OutFile "$temp\protobuf.zip"
Write-Host 'Expanding protobuf zip...' -ForegroundColor Yellow
Expand-Archive "$temp\protobuf.zip" "$projectRoot\target\Frameworks" -Force

124
Cargo.lock generated
View file

@ -156,9 +156,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
dependencies = [
"memchr",
]
@ -271,6 +271,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.2"
@ -292,7 +298,7 @@ dependencies = [
"asn1-rs-derive 0.1.0",
"asn1-rs-impl",
"displaydoc",
"nom",
"nom 7.1.3",
"num-traits",
"rusticata-macros",
"thiserror",
@ -308,7 +314,7 @@ dependencies = [
"asn1-rs-derive 0.4.0",
"asn1-rs-impl",
"displaydoc",
"nom",
"nom 7.1.3",
"num-traits",
"rusticata-macros",
"thiserror",
@ -733,7 +739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
dependencies = [
"arrayref",
"arrayvec",
"arrayvec 0.7.2",
"cc",
"cfg-if 1.0.0",
"constant_time_eq",
@ -988,7 +994,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
"nom 7.1.3",
]
[[package]]
@ -1786,7 +1792,7 @@ checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82"
dependencies = [
"asn1-rs 0.3.1",
"displaydoc",
"nom",
"nom 7.1.3",
"num-bigint",
"num-traits",
"rusticata-macros",
@ -1800,7 +1806,7 @@ checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e"
dependencies = [
"asn1-rs 0.5.2",
"displaydoc",
"nom",
"nom 7.1.3",
"num-bigint",
"num-traits",
"rusticata-macros",
@ -2388,6 +2394,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d73a076bdabd78c2f9045dba1b90664a655fa8372581c238596e1eb3a5e1b7"
[[package]]
name = "freedesktop_entry_parser"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4"
dependencies = [
"nom 7.1.3",
"thiserror",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@ -3075,6 +3091,15 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "home"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "hostname"
version = "0.3.1"
@ -3724,6 +3749,19 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec 0.5.2",
"bitflags",
"cfg-if 1.0.0",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.144"
@ -3925,7 +3963,7 @@ version = "0.43.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39d5ef876a2b2323d63c258e63c2f8e36f205fe5a11f0b3095d59635650790ff"
dependencies = [
"arrayvec",
"arrayvec 0.7.2",
"asynchronous-codec",
"bytes",
"either",
@ -4786,6 +4824,17 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "5.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b"
dependencies = [
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -5690,7 +5739,7 @@ dependencies = [
[[package]]
name = "prisma-client-rust"
version = "0.6.8"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=7e67b4550dd5323d479c96008678032f396b9060#7e67b4550dd5323d479c96008678032f396b9060"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
dependencies = [
"base64 0.13.1",
"bigdecimal",
@ -5723,7 +5772,7 @@ dependencies = [
[[package]]
name = "prisma-client-rust-cli"
version = "0.6.8"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=7e67b4550dd5323d479c96008678032f396b9060#7e67b4550dd5323d479c96008678032f396b9060"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
dependencies = [
"directories",
"flate2",
@ -5743,7 +5792,7 @@ dependencies = [
[[package]]
name = "prisma-client-rust-macros"
version = "0.6.8"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=7e67b4550dd5323d479c96008678032f396b9060#7e67b4550dd5323d479c96008678032f396b9060"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
dependencies = [
"convert_case 0.6.0",
"proc-macro2",
@ -5755,7 +5804,7 @@ dependencies = [
[[package]]
name = "prisma-client-rust-sdk"
version = "0.6.8"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=7e67b4550dd5323d479c96008678032f396b9060#7e67b4550dd5323d479c96008678032f396b9060"
source = "git+https://github.com/Brendonovich/prisma-client-rust?rev=687a00812130454613eee2c8e804bc615e755180#687a00812130454613eee2c8e804bc615e755180"
dependencies = [
"convert_case 0.5.0",
"dmmf",
@ -6373,7 +6422,7 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
dependencies = [
"aho-corasick 1.0.1",
"aho-corasick 1.0.2",
"memchr",
"regex-syntax 0.7.2",
]
@ -6654,7 +6703,7 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
"nom 7.1.3",
]
[[package]]
@ -6934,6 +6983,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "sd-desktop-linux"
version = "0.1.0"
dependencies = [
"aho-corasick 1.0.2",
"atty",
"freedesktop_entry_parser",
"mime",
"shlex",
"thiserror",
"xdg",
"xdg-mime",
]
[[package]]
name = "sd-desktop-macos"
version = "0.1.0"
@ -7065,7 +7128,7 @@ dependencies = [
name = "sd-sync-generator"
version = "0.1.0"
dependencies = [
"nom",
"nom 7.1.3",
"once_cell",
"prisma-client-rust-sdk",
"proc-macro2",
@ -7576,6 +7639,7 @@ dependencies = [
"rand 0.8.5",
"rspc",
"sd-core",
"sd-desktop-linux",
"sd-desktop-macos",
"serde",
"specta",
@ -7743,7 +7807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e"
dependencies = [
"itertools",
"nom",
"nom 7.1.3",
"unicode_categories",
]
@ -10086,7 +10150,7 @@ dependencies = [
"data-encoding",
"der-parser 7.0.0",
"lazy_static",
"nom",
"nom 7.1.3",
"oid-registry 0.4.0",
"ring",
"rusticata-macros",
@ -10105,7 +10169,7 @@ dependencies = [
"data-encoding",
"der-parser 8.2.0",
"lazy_static",
"nom",
"nom 7.1.3",
"oid-registry 0.6.1",
"rusticata-macros",
"thiserror",
@ -10121,6 +10185,28 @@ dependencies = [
"libc",
]
[[package]]
name = "xdg"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee"
dependencies = [
"home",
]
[[package]]
name = "xdg-mime"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87bf7b69bb50588d70a36e467be29d3df3e8c32580276d62eded9738c1a797aa"
dependencies = [
"dirs-next",
"glob",
"mime",
"nom 5.1.3",
"unicase",
]
[[package]]
name = "yasna"
version = "0.5.2"

View file

@ -18,19 +18,19 @@ edition = "2021"
repository = "https://github.com/spacedriveapp/spacedrive"
[workspace.dependencies]
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "7e67b4550dd5323d479c96008678032f396b9060", features = [
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "687a00812130454613eee2c8e804bc615e755180", features = [
"rspc",
"sqlite-create-many",
"migrations",
"sqlite",
] }
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "7e67b4550dd5323d479c96008678032f396b9060", features = [
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "687a00812130454613eee2c8e804bc615e755180", features = [
"rspc",
"sqlite-create-many",
"migrations",
"sqlite",
] }
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "7e67b4550dd5323d479c96008678032f396b9060", features = [
prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "687a00812130454613eee2c8e804bc615e755180", features = [
"sqlite",
] }

View file

@ -0,0 +1,17 @@
[package]
name = "sd-desktop-linux"
version = "0.1.0"
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
[dependencies]
aho-corasick = "1.0.2"
atty = "0.2.14"
freedesktop_entry_parser = "1.3.0"
mime = "0.3.17"
shlex = "1.1.0"
thiserror = "1.0.40"
xdg = "2.5.0"
xdg-mime = "0.3.3"

View file

@ -0,0 +1,9 @@
# Linux crate
For some OS specific operations
> The code for parsing Desktop Entries and finding which programs handle a certain mime-type is based on:
>
> https://github.com/chmln/handlr (MIT)
>
> thanks @chmln

View file

@ -0,0 +1,178 @@
use std::{
collections::HashMap,
convert::TryFrom,
ffi::OsString,
path::{Path, PathBuf},
process::{Command, Stdio},
str::FromStr,
};
use aho_corasick::AhoCorasick;
use mime::Mime;
use crate::{Error, Result, SystemApps};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DesktopEntry {
pub name: String,
pub exec: String,
pub file_name: OsString,
pub terminal: bool,
pub mimes: Vec<Mime>,
pub categories: HashMap<String, ()>,
}
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum Mode {
Launch,
Open,
}
fn terminal() -> Result<String> {
SystemApps::get_entries()
.ok()
.and_then(|mut entries| {
entries.find(|(_handler, entry)| entry.categories.contains_key("TerminalEmulator"))
})
.map(|e| e.1.exec)
.ok_or(Error::NoTerminal)
}
impl DesktopEntry {
pub fn exec(&self, mode: Mode, arguments: &[&str]) -> Result<()> {
let supports_multiple = self.exec.contains("%F") || self.exec.contains("%U");
if arguments.is_empty() {
self.exec_inner(&[])?
} else if supports_multiple || mode == Mode::Launch {
self.exec_inner(arguments)?;
} else {
for arg in arguments {
self.exec_inner(&[*arg])?;
}
};
Ok(())
}
fn exec_inner(&self, args: &[&str]) -> Result<()> {
let mut cmd = {
let (cmd, args) = self.get_cmd(args)?;
let mut cmd = Command::new(cmd);
cmd.args(args);
cmd
};
if self.terminal && atty::is(atty::Stream::Stdout) {
cmd.spawn()?.wait()?;
} else {
cmd.stdout(Stdio::null()).stderr(Stdio::null()).spawn()?;
}
Ok(())
}
pub fn get_cmd(&self, args: &[&str]) -> Result<(String, Vec<String>)> {
let special = AhoCorasick::new(["%f", "%F", "%u", "%U"]).expect("Failed to build pattern");
let mut exec = shlex::split(&self.exec).ok_or(Error::InvalidExec(self.exec.clone()))?;
// The desktop entry doesn't contain arguments - we make best effort and append them at
// the end
if special.is_match(&self.exec) {
exec = exec
.into_iter()
.flat_map(|s| match s.as_str() {
"%f" | "%F" | "%u" | "%U" => {
args.iter().map(|arg| str::to_string(arg)).collect()
}
s if special.is_match(s) => vec![{
let mut replaced = String::with_capacity(s.len() + args.len() * 2);
special.replace_all_with(s, &mut replaced, |_, _, dst| {
dst.push_str(args.join(" ").as_str());
false
});
replaced
}],
_ => vec![s],
})
.collect()
} else {
exec.extend(args.iter().map(|arg| str::to_string(arg)));
}
// If the entry expects a terminal (emulator), but this process is not running in one, we
// launch a new one.
if self.terminal && !atty::is(atty::Stream::Stdout) {
exec = shlex::split(&terminal()?)
.ok_or(Error::InvalidExec(self.exec.clone()))?
.into_iter()
.chain(["-e".to_owned()])
.chain(exec)
.collect();
}
Ok((exec.remove(0), exec))
}
}
fn parse_file(path: &Path) -> Option<DesktopEntry> {
let raw_entry = freedesktop_entry_parser::parse_entry(path).ok()?;
let section = raw_entry.section("Desktop Entry");
let mut entry = DesktopEntry {
file_name: path.file_name()?.to_owned(),
..Default::default()
};
for attr in section.attrs().filter(|a| a.has_value()) {
match attr.name {
"Name" if entry.name.is_empty() => {
entry.name = attr.value?.into();
}
"Exec" => entry.exec = attr.value?.into(),
"MimeType" => {
entry.mimes = attr
.value?
.split(';')
.filter_map(|m| Mime::from_str(m).ok())
.collect::<Vec<_>>();
}
"Terminal" => entry.terminal = attr.value? == "true",
"Categories" => {
entry.categories = attr
.value?
.split(';')
.filter(|s| !s.is_empty())
.map(|cat| (cat.to_owned(), ()))
.collect();
}
_ => {}
}
}
if !entry.name.is_empty() && !entry.exec.is_empty() {
Some(entry)
} else {
None
}
}
impl TryFrom<PathBuf> for DesktopEntry {
type Error = Error;
fn try_from(path: PathBuf) -> Result<DesktopEntry> {
parse_file(&path).ok_or(Error::BadEntry(path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn complex_exec() {
let entry = parse_file(Path::new("tests/cmus.desktop")).unwrap();
assert_eq!(entry.mimes.len(), 2);
assert_eq!(entry.mimes[0].essence_str(), "audio/mp3");
assert_eq!(entry.mimes[1].essence_str(), "audio/ogg");
}
}

View file

@ -0,0 +1,54 @@
use std::{convert::TryFrom, ffi::OsString, fmt::Display, path::PathBuf, str::FromStr};
use mime::Mime;
use crate::{DesktopEntry, Error, ExecMode, Result};
pub enum HandlerType {
Mime(Mime),
Ext(String),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Handler(OsString);
impl Display for Handler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0.to_string_lossy())
}
}
impl FromStr for Handler {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let handler = Self::assume_valid(s.into());
handler.get_entry()?;
Ok(handler)
}
}
impl Handler {
pub fn assume_valid(name: OsString) -> Self {
Self(name)
}
pub fn get_path(&self) -> Result<PathBuf> {
let mut path = PathBuf::from("applications");
path.push(&self.0);
xdg::BaseDirectories::new()?
.find_data_file(path)
.ok_or(Error::BadPath(self.0.to_string_lossy().to_string()))
}
pub fn get_entry(&self) -> Result<DesktopEntry> {
DesktopEntry::try_from(self.get_path()?)
}
pub fn launch(&self, args: &[&str]) -> Result<()> {
self.get_entry()?.exec(ExecMode::Launch, args)
}
pub fn open(&self, args: &[&str]) -> Result<()> {
self.get_entry()?.exec(ExecMode::Open, args)
}
}

View file

@ -0,0 +1,29 @@
#![cfg(target_os = "linux")]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Xdg(#[from] xdg::BaseDirectoriesError),
#[error("no handlers found for '{0}'")]
NotFound(String),
#[error("bad Desktop Entry exec line: {0}")]
InvalidExec(String),
#[error("malformed desktop entry at {0}")]
BadEntry(std::path::PathBuf),
#[error("Please specify the default terminal with handlr set x-scheme-handler/terminal")]
NoTerminal,
#[error("Bad path: {0}")]
BadPath(String),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
mod desktop_entry;
mod handler;
mod system;
pub use desktop_entry::{DesktopEntry, Mode as ExecMode};
pub use handler::{Handler, HandlerType};
pub use system::SystemApps;

View file

@ -0,0 +1,65 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
convert::TryFrom,
ffi::OsString,
};
use mime::Mime;
use xdg_mime::SharedMimeInfo;
use crate::{DesktopEntry, Handler, HandlerType, Result};
#[derive(Debug, Default, Clone)]
pub struct SystemApps(pub HashMap<Mime, VecDeque<Handler>>);
impl SystemApps {
pub fn get_handlers(&self, handler_type: HandlerType) -> VecDeque<Handler> {
let mime_db = SharedMimeInfo::new();
match handler_type {
HandlerType::Ext(ext) => {
let mut handlers: HashSet<Handler> = HashSet::new();
for mime in mime_db.get_mime_types_from_file_name(ext.as_str()) {
if let Some(mime_handlers) = self.0.get(&mime) {
for handler in mime_handlers {
handlers.insert(handler.clone());
}
}
}
handlers.into_iter().collect()
}
HandlerType::Mime(mime) => self.0.get(&mime).unwrap_or(&VecDeque::new()).clone(),
}
}
pub fn get_handler(&self, handler_type: HandlerType) -> Option<Handler> {
Some(self.get_handlers(handler_type).get(0)?.clone())
}
pub fn get_entries() -> Result<impl Iterator<Item = (OsString, DesktopEntry)>> {
Ok(xdg::BaseDirectories::new()?
.list_data_files_once("applications")
.into_iter()
.filter(|p| p.extension().and_then(|x| x.to_str()) == Some("desktop"))
.filter_map(|p| {
Some((
p.file_name()?.to_owned(),
DesktopEntry::try_from(p.clone()).ok()?,
))
}))
}
pub fn populate() -> Result<Self> {
let mut map = HashMap::<Mime, VecDeque<Handler>>::with_capacity(50);
Self::get_entries()?.for_each(|(_, entry)| {
let (file_name, mimes) = (entry.file_name, entry.mimes);
mimes.into_iter().for_each(|mime| {
map.entry(mime)
.or_default()
.push_back(Handler::assume_valid(file_name.clone()));
});
});
Ok(Self(map))
}
}

View file

@ -0,0 +1,12 @@
[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
Display=true
Exec=bash -c "(! pgrep cmus && tilix -e cmus && tilix -a session-add-down -e cava); sleep 0.1 && cmus-remote -q %f"
Terminal=false
Name=cmus-remote
Comment=Music player cmus-remote control
NoDisplay=true
Icon=cmus
MimeType=audio/mp3;audio/ogg;

View file

@ -1,9 +1,9 @@
[package]
name = "sd-desktop-macos"
version = "0.1.0"
license.workspace = true
repository.workspace = true
edition.workspace = true
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
[dependencies]
swift-rs = { workspace = true, features = ["serde"] }

View file

@ -4,9 +4,9 @@ version = "0.1.0"
description = "The universal file manager."
authors = ["Spacedrive Technology Inc."]
default-run = "spacedrive"
license.workspace = true
repository.workspace = true
edition.workspace = true
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
[dependencies]
tauri = { version = "1.3.0", features = ["dialog-all", "linux-protocol-headers", "macos-private-api", "os-all", "path-all", "protocol-all", "shell-all", "window-all"] }
@ -27,16 +27,17 @@ serde = "1.0.163"
percent-encoding = "2.2.0"
http = "0.2.9"
opener = "0.6.1"
specta.workspace = true
specta = { workspace = true }
tauri-specta = { workspace = true, features = ["typescript"] }
uuid = { version = "1.3.3", features = ["serde"] }
[target.'cfg(target_os = "linux")'.dependencies]
axum = { version = "0.6.18", features = ["headers", "query"] }
rand = "0.8.5"
sd-desktop-linux = { path = "../crates/linux" }
[target.'cfg(target_os = "macos")'.dependencies]
sd-desktop-macos.path = "../crates/macos"
sd-desktop-macos = { path = "../crates/macos" }
[build-dependencies]
tauri-build = { version = "1.3.0", features = [] }

View file

@ -38,9 +38,12 @@ pub async fn open_file_path(
Ok(res)
}
#[derive(Type, serde::Serialize)]
#[derive(Type, Debug, serde::Serialize)]
pub struct OpenWithApplication {
name: String,
#[cfg(target_os = "linux")]
url: std::path::PathBuf,
#[cfg(not(target_os = "linux"))]
url: String,
}
@ -55,7 +58,7 @@ pub async fn get_file_path_open_with_apps(
return Err(())
};
let Ok(Some(_path)) = library
let Ok(Some(path)) = library
.get_file_path(id)
.await
else {
@ -64,7 +67,7 @@ pub async fn get_file_path_open_with_apps(
#[cfg(target_os = "macos")]
return Ok(unsafe {
sd_desktop_macos::get_open_with_applications(&_path.to_str().unwrap().into())
sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into())
}
.as_slice()
.iter()
@ -74,7 +77,42 @@ pub async fn get_file_path_open_with_apps(
})
.collect());
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
{
use sd_desktop_linux::{DesktopEntry, HandlerType, SystemApps};
// TODO: cache this, and only update when the underlying XDG desktop apps changes
let system_apps = SystemApps::populate().map_err(|_| ())?;
let handlers = system_apps.get_handlers(HandlerType::Ext(
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_string())
.ok_or(
// io::Error::new(
// io::ErrorKind::Other,
// "Missing file name from path",
// )
(),
)?,
));
let data = handlers
.iter()
.map(|handler| {
let path = handler.get_path().map_err(|_| ())?;
let entry = DesktopEntry::try_from(path.clone()).map_err(|_| ())?;
Ok(OpenWithApplication {
name: entry.name,
url: path,
})
})
.collect::<Result<Vec<OpenWithApplication>, _>>()?;
return Ok(data);
}
#[allow(unreachable_code)]
Err(())
}
@ -83,14 +121,14 @@ pub async fn get_file_path_open_with_apps(
pub async fn open_file_path_with(
library: uuid::Uuid,
id: i32,
_with_url: String,
url: String,
node: tauri::State<'_, Arc<Node>>,
) -> Result<(), ()> {
let Some(library) = node.library_manager.get_library(library).await else {
return Err(())
};
let Ok(Some(_path)) = library
let Ok(Some(path)) = library
.get_file_path(id)
.await
else {
@ -100,10 +138,17 @@ pub async fn open_file_path_with(
#[cfg(target_os = "macos")]
unsafe {
sd_desktop_macos::open_file_path_with(
&_path.to_str().unwrap().into(),
&_with_url.as_str().into(),
&path.to_str().ok_or(())?.into(),
&url.as_str().into(),
)
};
#[cfg(target_os = "linux")]
{
sd_desktop_linux::Handler::assume_valid(url.into())
.open(&[path.to_str().ok_or(())?])
.map_err(|_| ())?;
}
Ok(())
}

View file

@ -30,8 +30,8 @@ export function getFilePathOpenWithApps(library: string, id: number) {
return invoke()<OpenWithApplication[]>("get_file_path_open_with_apps", { library,id })
}
export function openFilePathWith(library: string, id: number, withUrl: string) {
return invoke()<null>("open_file_path_with", { library,id,withUrl })
export function openFilePathWith(library: string, id: number, url: string) {
return invoke()<null>("open_file_path_with", { library,id,url })
}
export function lockAppTheme(themeType: AppThemeType) {

View file

@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { Suspense } from 'react';
import { FilePath, useLibraryContext } from '@sd/client';
import { ContextMenu } from '@sd/ui';
import { showAlertDialog } from '~/components';
import { Platform, usePlatform } from '~/util/Platform';
export default (props: { filePath: FilePath }) => {
@ -33,7 +34,7 @@ const Items = ({
}) => {
const { library } = useLibraryContext();
const items = useQuery<any[]>(
const items = useQuery(
['openWith', filePath.id],
() => actions.getFilePathOpenWithApps(library.uuid, filePath.id),
{ suspense: true }
@ -44,11 +45,20 @@ const Items = ({
{items.data?.map((d) => (
<ContextMenu.Item
key={d.name}
onClick={() => actions.openFilePathWith(library.uuid, filePath.id, d.url)}
onClick={async () => {
try {
await actions.openFilePathWith(library.uuid, filePath.id, d.url);
} catch {
showAlertDialog({
title: 'Error',
value: `Failed to open file, with: ${d.url}`
});
}
}}
>
{d.name}
</ContextMenu.Item>
))}
)) ?? <p> No apps available </p>}
</>
);
};

View file

@ -1,4 +1,4 @@
import { PropsWithChildren, createContext, useContext, useState } from 'react';
import { PropsWithChildren, createContext, useContext } from 'react';
export type OperatingSystem = 'browser' | 'linux' | 'macOS' | 'windows' | 'unknown';
@ -24,8 +24,8 @@ export type Platform = {
openLogsDir?(): void;
// Opens a file path with a given ID
openFilePath?(library: string, id: number): any;
getFilePathOpenWithApps?(library: string, id: number): any;
openFilePathWith?(library: string, id: number, appUrl: string): any;
getFilePathOpenWithApps?(library: string, id: number): Promise<{ name: string; url: string }[]>;
openFilePathWith?(library: string, id: number, appUrl: string): Promise<unknown>;
lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any;
};