mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
[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:
parent
1889a767e8
commit
e6d0e6098c
2
.github/scripts/setup-system.ps1
vendored
2
.github/scripts/setup-system.ps1
vendored
|
@ -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
124
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
] }
|
||||
|
||||
|
|
17
apps/desktop/crates/linux/Cargo.toml
Normal file
17
apps/desktop/crates/linux/Cargo.toml
Normal 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"
|
||||
|
9
apps/desktop/crates/linux/README.md
Normal file
9
apps/desktop/crates/linux/README.md
Normal 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
|
178
apps/desktop/crates/linux/src/desktop_entry.rs
Normal file
178
apps/desktop/crates/linux/src/desktop_entry.rs
Normal 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");
|
||||
}
|
||||
}
|
54
apps/desktop/crates/linux/src/handler.rs
Normal file
54
apps/desktop/crates/linux/src/handler.rs
Normal 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)
|
||||
}
|
||||
}
|
29
apps/desktop/crates/linux/src/lib.rs
Normal file
29
apps/desktop/crates/linux/src/lib.rs
Normal 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;
|
65
apps/desktop/crates/linux/src/system.rs
Normal file
65
apps/desktop/crates/linux/src/system.rs
Normal 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))
|
||||
}
|
||||
}
|
12
apps/desktop/crates/linux/tests/cmus.desktop
Normal file
12
apps/desktop/crates/linux/tests/cmus.desktop
Normal 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;
|
|
@ -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"] }
|
||||
|
|
|
@ -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 = [] }
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue