[ENG-767, ENG-916] Improvements and fixes for Open/Open With (#1082)

* Normalize PATH and XDG envvars on Linux

* Fix normalize_xdg_environment

* Replace custom Desktop file parsers with Glib

* Fix AppImage env influencing external apps
 - Normalize GStream plugin path

* Fix macos pulling linux deps

* Attempt to fix gnome apps failing to launch
 - Fix incorrect logic in `normalize_pathlist`

* Ensure empty envvars are not set

* Revert AppImage ovewritten GTK specific variables
 - `normalize_pathlist` now prefers entries with less priority when dealing with repeated entries, this is not compatible with the default behavior, but it is a more sane approach IMHO

* Remove 32-bit libs from release CI build host

* Remove 32-bit from github runner 2 attempt
 - Remove deprecated vscode config

* Remove libc6-i386

* [ENG-916] Implement `Open With` logic compatible with macOS < 12

* Add some missing gstreamer deps in Linux

* Replace opener with Glib API

* Fix reveal opening file instead of dir
 - Improve Open With logic in Windows
 - Expose functions to test if app is in a flatpak, snap or appimage

---------

Co-authored-by: Brendan Allan <brendonovich@outlook.com>
This commit is contained in:
Vítor Vasconcellos 2023-07-24 14:36:00 -03:00 committed by GitHub
parent 447069388b
commit 7e4ed2b547
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 664 additions and 619 deletions

View file

@ -86,5 +86,5 @@ indent_style = space
# Swift
# https://github.com/apple/swift-format/blob/main/Documentation/Configuration.md#example
[*.swift]
indent_size = 2
indent_size = 4
indent_style = space

View file

@ -150,7 +150,7 @@ if [ "$SYSNAME" = "Linux" ]; then
DEBIAN_FFMPEG_DEPS="libheif-dev libavcodec-dev libavdevice-dev libavfilter-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev ffmpeg"
# Webkit2gtk requires gstreamer plugins for video playback to work
DEBIAN_VIDEO_DEPS="gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly"
DEBIAN_VIDEO_DEPS="gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-libav gstreamer1.0-pipewire gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly gstreamer1.0-pulseaudio gstreamer1.0-vaapi libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev"
# Bindgen dependencies - it's used by a dependency of Spacedrive
DEBIAN_BINDGEN_DEPS="pkg-config clang"
@ -168,7 +168,7 @@ if [ "$SYSNAME" = "Linux" ]; then
ARCH_TAURI_DEPS="webkit2gtk base-devel curl wget openssl appmenu-gtk-module gtk3 libappindicator-gtk3 librsvg libvips patchelf"
# Webkit2gtk requires gstreamer plugins for video playback to work
ARCH_VIDEO_DEPS="gst-libav gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly"
ARCH_VIDEO_DEPS="gst-libav gst-plugins-bad gst-plugins-base gst-plugins-good gst-plugins-ugly gst-plugin-pipewire gstreamer-vaapi"
# FFmpeg dependencies
ARCH_FFMPEG_DEPS="libheif ffmpeg"
@ -202,7 +202,7 @@ if [ "$SYSNAME" = "Linux" ]; then
FEDORA_FFMPEG_DEPS="libheif-devel ffmpeg ffmpeg-devel"
# Webkit2gtk requires gstreamer plugins for video playback to work
FEDORA_VIDEO_DEPS="gstreamer1-plugin-libav gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-good-extras gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-extras gstreamer1-plugins-ugly-free"
FEDORA_VIDEO_DEPS="gstreamer1-devel gstreamer1-plugins-base-devel gstreamer1-plugins-good gstreamer1-plugins-good-gtk gstreamer1-plugins-good-extras gstreamer1-plugins-ugly-free gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-devel gstreamer1-plugins-bad-free-extras"
# Bindgen dependencies - it's used by a dependency of Spacedrive
FEDORA_BINDGEN_DEPS="clang"

View file

@ -39,6 +39,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Remove 32-bit libs
if: ${{ runner.os == 'Linux' }}
run: |
dpkg -l | grep i386
sudo apt-get purge --allow-remove-essential libc6-i386 ".*:i386"
sudo dpkg --remove-architecture i386
- name: Install Apple API key
if: ${{ runner.os == 'macOS' }}
run: |

View file

@ -65,7 +65,6 @@
"packages/config",
"packages/ui"
],
"eslint.packageManager": "pnpm",
"eslint.lintTask.enable": true,
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {

111
Cargo.lock generated
View file

@ -271,12 +271,6 @@ 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"
@ -298,7 +292,7 @@ dependencies = [
"asn1-rs-derive 0.1.0",
"asn1-rs-impl",
"displaydoc",
"nom 7.1.3",
"nom",
"num-traits",
"rusticata-macros",
"thiserror",
@ -314,7 +308,7 @@ dependencies = [
"asn1-rs-derive 0.4.0",
"asn1-rs-impl",
"displaydoc",
"nom 7.1.3",
"nom",
"num-traits",
"rusticata-macros",
"thiserror",
@ -745,7 +739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
dependencies = [
"arrayref",
"arrayvec 0.7.2",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
@ -1000,7 +994,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom 7.1.3",
"nom",
]
[[package]]
@ -1816,7 +1810,7 @@ checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82"
dependencies = [
"asn1-rs 0.3.1",
"displaydoc",
"nom 7.1.3",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
@ -1830,7 +1824,7 @@ checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e"
dependencies = [
"asn1-rs 0.5.2",
"displaydoc",
"nom 7.1.3",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
@ -2464,16 +2458,6 @@ 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"
@ -3173,15 +3157,6 @@ 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"
@ -3829,24 +3804,11 @@ 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",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.146"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libdbus-sys"
@ -4053,7 +4015,7 @@ version = "0.43.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39d5ef876a2b2323d63c258e63c2f8e36f205fe5a11f0b3095d59635650790ff"
dependencies = [
"arrayvec 0.7.2",
"arrayvec",
"asynchronous-codec",
"bytes",
"either",
@ -4914,17 +4876,6 @@ 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"
@ -6875,7 +6826,7 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom 7.1.3",
"nom",
]
[[package]]
@ -7179,14 +7130,9 @@ dependencies = [
name = "sd-desktop-linux"
version = "0.1.0"
dependencies = [
"aho-corasick 1.0.2",
"atty",
"freedesktop_entry_parser",
"mime",
"shlex",
"thiserror",
"xdg",
"xdg-mime",
"gtk",
"libc",
"tokio",
]
[[package]]
@ -7335,7 +7281,7 @@ dependencies = [
name = "sd-sync-generator"
version = "0.1.0"
dependencies = [
"nom 7.1.3",
"nom",
"once_cell",
"prisma-client-rust-sdk",
"proc-macro2",
@ -7870,6 +7816,7 @@ name = "spacedrive"
version = "0.1.0"
dependencies = [
"axum",
"futures",
"http",
"httpz 0.0.3",
"opener",
@ -8059,7 +8006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e"
dependencies = [
"itertools",
"nom 7.1.3",
"nom",
"unicode_categories",
]
@ -10402,7 +10349,7 @@ dependencies = [
"data-encoding",
"der-parser 7.0.0",
"lazy_static",
"nom 7.1.3",
"nom",
"oid-registry 0.4.0",
"ring",
"rusticata-macros",
@ -10421,7 +10368,7 @@ dependencies = [
"data-encoding",
"der-parser 8.2.0",
"lazy_static",
"nom 7.1.3",
"nom",
"oid-registry 0.6.1",
"rusticata-macros",
"thiserror",
@ -10437,28 +10384,6 @@ 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

@ -5,8 +5,6 @@ license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
indoc = "1.0.9"
clap = { version = "4.3.0", features = ["derive"] }

View file

@ -6,12 +6,9 @@ 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"
libc = "0.2"
tokio = { workspace = true, features = ["fs"] }
[target.'cfg(target_os = "linux")'.dependencies]
# WARNING: gtk should follow the same version used by tauri
gtk = "0.15"

View file

@ -0,0 +1,140 @@
use std::path::{Path, PathBuf};
use gtk::{
gio::{
content_type_guess,
prelude::AppInfoExt,
prelude::{AppLaunchContextExt, FileExt},
AppInfo, AppLaunchContext, DesktopAppInfo, File as GioFile, ResourceError,
},
glib::error::Error as GlibError,
prelude::IsA,
};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::env::remove_prefix_from_pathlist;
fn remove_prefix_from_env_in_ctx(
ctx: &impl IsA<AppLaunchContext>,
env_name: &str,
prefix: &impl AsRef<Path>,
) {
if let Some(value) = remove_prefix_from_pathlist(env_name, prefix) {
ctx.setenv(env_name, value);
} else {
ctx.unsetenv(env_name);
}
}
thread_local! {
static LAUNCH_CTX: AppLaunchContext = {
// TODO: Display supports requires GDK, which can only run on the main thread
// let ctx = Display::default()
// .and_then(|display| display.app_launch_context())
// .map(|display| display.to_value().get::<AppLaunchContext>().expect(
// "This is an Glib type conversion, it should never fail because GDKAppLaunchContext is a subclass of AppLaunchContext"
// )).unwrap_or_default();
let ctx = AppLaunchContext::default();
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) {
// Remove AppImage paths from environment variables to avoid external applications attempting to use the AppImage's libraries
// https://github.com/AppImage/AppImageKit/blob/701b711f42250584b65a88f6427006b1d160164d/src/AppRun.c#L168-L194
ctx.unsetenv("PYTHONHOME");
ctx.unsetenv("GTK_DATA_PREFIX");
ctx.unsetenv("GTK_THEME");
ctx.unsetenv("GDK_BACKEND");
ctx.unsetenv("GTK_EXE_PREFIX");
ctx.unsetenv("GTK_IM_MODULE_FILE");
ctx.unsetenv("GDK_PIXBUF_MODULE_FILE");
remove_prefix_from_env_in_ctx(&ctx, "PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "LD_LIBRARY_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "PYTHONPATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "XDG_DATA_DIRS", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "PERLLIB", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GSETTINGS_SCHEMA_DIR", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "QT_PLUGIN_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GST_PLUGIN_SYSTEM_PATH_1_0", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GTK_PATH", &appdir);
remove_prefix_from_env_in_ctx(&ctx, "GIO_EXTRA_MODULES", &appdir);
}
ctx
}
}
pub struct App {
pub id: String,
pub name: String,
// pub icon: Option<Vec<u8>>,
}
async fn recommended_for_type(file_path: impl AsRef<Path>) -> Vec<AppInfo> {
let data = if let Ok(mut file) = File::open(&file_path).await {
let mut data = [0; 1024];
if file.read_exact(&mut data).await.is_ok() {
Some(data)
} else {
None
}
} else {
None
};
let file_path = Some(file_path);
let (content_type, uncertain) = if let Some(data) = data {
content_type_guess(file_path, &data)
} else {
content_type_guess(file_path, &[])
};
if uncertain {
vec![]
} else {
AppInfo::recommended_for_type(content_type.as_str())
}
}
pub async fn list_apps_associated_with_ext(file_path: impl AsRef<Path>) -> Vec<App> {
recommended_for_type(file_path)
.await
.iter()
.flat_map(|app_info| {
app_info.id().map(|id| App {
id: id.to_string(),
name: app_info.name().to_string(),
// TODO: Icon supports requires GTK, which can only run on the main thread
// icon: app_info
// .icon()
// .and_then(|icon| {
// IconTheme::default().and_then(|icon_theme| {
// icon_theme.lookup_by_gicon(&icon, 128, IconLookupFlags::empty())
// })
// })
// .and_then(|icon_info| icon_info.load_icon().ok())
// .and_then(|pixbuf| pixbuf.save_to_bufferv("png", &[]).ok()),
})
})
.collect()
}
pub fn open_files_path_with(file_paths: &[impl AsRef<Path>], id: &str) -> Result<(), GlibError> {
let Some(app) = DesktopAppInfo::new(id) else {
return Err(GlibError::new(ResourceError::NotFound, "App not found"))
};
LAUNCH_CTX.with(|ctx| {
app.launch(
&file_paths.iter().map(GioFile::for_path).collect::<Vec<_>>(),
Some(ctx),
)
})
}
pub fn open_file_path(file_path: &impl AsRef<Path>) -> Result<(), GlibError> {
let file_uri = GioFile::for_path(file_path).uri().to_string();
LAUNCH_CTX.with(|ctx| AppInfo::launch_default_for_uri(&file_uri.to_string(), Some(ctx)))
}

View file

@ -1,180 +0,0 @@
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> {
// TODO: Attemtp to read x-terminal-emulator bin (Debian/Ubuntu spec for setting default terminal)
SystemApps::get_entries()
.ok()
.and_then(|mut entries| {
entries
.find(|DesktopEntry { categories, .. }| categories.contains_key("TerminalEmulator"))
})
.map(|e| e.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.clone()))
}
}
#[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,226 @@
use std::{
collections::HashSet,
env,
ffi::{CStr, OsStr, OsString},
mem,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
ptr,
};
pub fn get_current_user_home() -> Option<PathBuf> {
use libc::{getpwuid_r, getuid, passwd, ERANGE};
if let Some(home) = env::var_os("HOME") {
let home = PathBuf::from(home);
if home.is_absolute() && home.is_dir() {
return Some(home);
}
}
let uid = unsafe { getuid() };
let mut buf = vec![0; 2048];
let mut passwd = unsafe { mem::zeroed::<passwd>() };
let mut result = ptr::null_mut::<passwd>();
loop {
let r = unsafe { getpwuid_r(uid, &mut passwd, buf.as_mut_ptr(), buf.len(), &mut result) };
if r != ERANGE {
break;
}
let newsize = buf.len().checked_mul(2)?;
buf.resize(newsize, 0);
}
if result.is_null() {
// There is no such user, or an error has occurred.
// errno gets set if theres an error.
return None;
}
if result != &mut passwd {
// The result of getpwuid_r should be its input passwd.
return None;
}
let passwd: passwd = unsafe { result.read() };
if passwd.pw_dir.is_null() {
return None;
}
let home = PathBuf::from(OsStr::from_bytes(
unsafe { CStr::from_ptr(passwd.pw_dir) }.to_bytes(),
));
if home.is_absolute() && home.is_dir() {
env::set_var("HOME", &home);
Some(home)
} else {
None
}
}
fn normalize_pathlist(
env_name: &str,
default_dirs: &[PathBuf],
) -> Result<Vec<PathBuf>, env::JoinPathsError> {
let dirs = if let Some(value) = env::var_os(env_name) {
let mut dirs = env::split_paths(&value)
.filter(|entry| !entry.as_os_str().is_empty())
.collect::<Vec<_>>();
let mut insert_index = dirs.len();
for default_dir in default_dirs {
match dirs.iter().rev().position(|dir| dir == default_dir) {
Some(mut index) => {
index = dirs.len() - index - 1;
if index < insert_index {
insert_index = index
}
}
None => dirs.insert(insert_index, default_dir.to_path_buf()),
}
}
dirs
} else {
default_dirs.into()
};
let mut unique = HashSet::new();
let mut pathlist = dirs
.iter()
.rev() // Reverse order to remove duplicates from the end
.filter(|dir| unique.insert(*dir))
.cloned()
.collect::<Vec<_>>();
pathlist.reverse();
env::set_var(env_name, env::join_paths(&pathlist)?);
Ok(pathlist)
}
fn normalize_xdg_environment(name: &str, default_value: PathBuf) -> PathBuf {
if let Some(value) = env::var_os(name) {
if !value.is_empty() {
let path = PathBuf::from(value);
if path.is_absolute() && path.is_dir() {
return path;
}
}
}
env::set_var(name, &default_value);
default_value
}
pub fn normalize_environment() {
let home = get_current_user_home().expect("No user home directory found");
// Normalize user XDG dirs environment variables
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
let data_home = normalize_xdg_environment("XDG_DATA_HOME", home.join(".local/share"));
normalize_xdg_environment("XDG_CACHE_HOME", home.join(".cache"));
normalize_xdg_environment("XDG_CONFIG_HOME", home.join(".config"));
// Normalize system XDG dirs environment variables
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
normalize_pathlist(
"XDG_DATA_DIRS",
&[
PathBuf::from("/usr/share"),
PathBuf::from("/usr/local/share"),
PathBuf::from("/var/lib/flatpak/exports/share"),
data_home.join("flatpak/exports/share"),
],
)
.expect("XDG_DATA_DIRS must be successfully normalized");
normalize_pathlist("XDG_CONFIG_DIRS", &[PathBuf::from("/etc/xdg")])
.expect("XDG_CONFIG_DIRS must be successfully normalized");
// Normalize GStreamer plugin path
// https://gstreamer.freedesktop.org/documentation/gstreamer/gstregistry.html#gstregistry-page
normalize_pathlist(
"GST_PLUGIN_SYSTEM_PATH",
&[
PathBuf::from("/usr/lib/gstreamer"),
data_home.join("gstreamer/plugins"),
],
)
.expect("GST_PLUGIN_SYSTEM_PATH must be successfully normalized");
normalize_pathlist(
"GST_PLUGIN_SYSTEM_PATH_1_0",
&[
PathBuf::from("/usr/lib/gstreamer-1.0"),
data_home.join("gstreamer-1.0/plugins"),
],
)
.expect("GST_PLUGIN_SYSTEM_PATH_1_0 must be successfully normalized");
// Normalize PATH
normalize_pathlist(
"PATH",
&[
PathBuf::from("/sbin"),
PathBuf::from("/bin"),
PathBuf::from("/usr/sbin"),
PathBuf::from("/usr/bin"),
PathBuf::from("/usr/local/sbin"),
PathBuf::from("/usr/local/bin"),
PathBuf::from("/var/lib/flatpak/exports/bin"),
data_home.join("flatpak/exports/bin"),
],
)
.expect("PATH must be successfully normalized");
}
pub(crate) fn remove_prefix_from_pathlist(
env_name: &str,
prefix: &impl AsRef<Path>,
) -> Option<OsString> {
env::var_os(env_name).and_then(|value| {
let mut dirs = env::split_paths(&value)
.filter(|dir| !(dir.as_os_str().is_empty() || dir.starts_with(prefix)))
.peekable();
if dirs.peek().is_none() {
None
} else {
Some(env::join_paths(dirs).expect("Should not fail because we are only filtering a pathlist retrieved from the environmnet"))
}
})
}
// Check if snap by looking if SNAP is set and not empty and that the SNAP directory exists
pub fn is_snap() -> bool {
if let Some(snap) = std::env::var_os("SNAP") {
if !snap.is_empty() && PathBuf::from(snap).is_dir() {
return true;
}
}
false
}
// Check if appimage by looking if APPDIR is set and is a valid directory
pub fn is_appimage() -> bool {
if let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) {
appdir.is_absolute() && appdir.is_dir()
} else {
false
}
}
// Check if flatpak by looking if FLATPAK_ID is set and not empty and that the .flatpak-info file exists
pub fn is_flatpak() -> bool {
if let Some(flatpak_id) = std::env::var_os("FLATPAK_ID") {
if !flatpak_id.is_empty() && PathBuf::from("/.flatpak-info").is_file() {
return true;
}
}
false
}

View file

@ -1,54 +0,0 @@
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

@ -1,29 +1,7 @@
#![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),
}
mod app_info;
mod env;
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;
pub use app_info::{list_apps_associated_with_ext, open_file_path, open_files_path_with};
pub use env::{is_appimage, is_flatpak, is_snap, normalize_environment};

View file

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

View file

@ -1,12 +0,0 @@
[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

@ -6,8 +6,10 @@ repository = { workspace = true }
edition = { workspace = true }
[dependencies]
swift-rs = { workspace = true, features = ["serde"] }
serde = { version = "1.0" }
[build-dependencies]
[target.'cfg(target_os = "macos")'.dependencies]
swift-rs = { workspace = true, features = ["serde"] }
[target.'cfg(target_os = "macos")'.build-dependencies]
swift-rs = { workspace = true, features = ["build"] }

View file

@ -1,22 +1,35 @@
import AppKit
import SwiftRs
extension NSBitmapImageRep {
var png: Data? { representation(using: .png, properties: [:]) }
}
extension Data {
var bitmap: NSBitmapImageRep? { NSBitmapImageRep(data: self) }
}
extension NSImage {
var png: Data? { tiffRepresentation?.bitmap?.png }
}
class OpenWithApplication: NSObject {
var name: SRString;
var id: SRString;
var url: SRString;
var name: SRString
var id: SRString
var url: SRString
var icon: SRData
init(name: SRString, id: SRString, url: SRString) {
init(name: SRString, id: SRString, url: SRString, icon: SRData) {
self.name = name
self.id = id
self.url = url
self.icon = icon
}
}
@_cdecl("get_open_with_applications")
func getOpenWithApplications(urlString: SRString) -> SRObjectArray {
let url: URL;
let url: URL
if #available(macOS 13.0, *) {
url = URL(filePath: urlString.toString())
} else {
@ -24,48 +37,84 @@ func getOpenWithApplications(urlString: SRString) -> SRObjectArray {
url = URL(fileURLWithPath: urlString.toString())
}
let appURLs: [URL]
if #available(macOS 12.0, *) {
return SRObjectArray(NSWorkspace.shared.urlsForApplications(toOpen: url)
.compactMap { url in
Bundle(url: url)?.infoDictionary.map { ($0, url) }
}
.compactMap { (dict, url) -> NSObject? in
guard let name = (dict["CFBundleDisplayName"] ?? dict["CFBundleName"]) as? String else {
return nil
};
if !url.path.contains("/Applications/") {
return nil
}
return OpenWithApplication(
name: SRString(name),
id: SRString(dict["CFBundleIdentifier"] as! String),
url: SRString(url.path)
)
})
appURLs = NSWorkspace.shared.urlsForApplications(toOpen: url)
} else {
// Fallback on earlier versions
return SRObjectArray([])
// Fallback for macOS versions prior to 12
// Get type identifier from file URL
let fileType: String
if #available(macOS 11.0, *) {
guard let _fileType = (try? url.resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier
else {
print("Failed to fetch file type for the specified file URL")
return SRObjectArray([])
}
fileType = _fileType
} else {
// Fallback for macOS versions prior to 11
guard
let _fileType = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension, url.pathExtension as CFString, nil)?.takeRetainedValue()
else {
print("Failed to fetch file type for the specified file URL")
return SRObjectArray([])
}
fileType = _fileType as String
}
// Locates an array of bundle identifiers for apps capable of handling a specified content type with the specified roles.
guard
let bundleIds = LSCopyAllRoleHandlersForContentType(fileType as CFString, LSRolesMask.all)?
.takeRetainedValue() as? [String]
else {
print("Failed to fetch bundle IDs for the specified file type")
return SRObjectArray([])
}
// Retrieve all URLs for the app identified by a bundle id
appURLs = bundleIds.compactMap { bundleId -> URL? in
guard let retVal = LSCopyApplicationURLsForBundleIdentifier(bundleId as CFString, nil) else {
return nil
}
return retVal.takeRetainedValue() as? URL
}
}
return SRObjectArray(
appURLs.compactMap { url -> NSObject? in
guard !url.path.contains("/Applications/"),
let infoDict = Bundle(url: url)?.infoDictionary,
let name = (infoDict["CFBundleDisplayName"] ?? infoDict["CFBundleName"]) as? String,
let appId = infoDict["CFBundleIdentifier"] as? String
else {
return nil
}
let icon = NSWorkspace.shared.icon(forFile: url.path)
return OpenWithApplication(
name: SRString(name),
id: SRString(appId),
url: SRString(url.path),
icon: SRData([UInt8](icon.png ?? Data()))
)
})
}
@_cdecl("open_file_path_with")
func openFilePathWith(fileUrl: SRString, withUrl: SRString) {
let config = NSWorkspace.OpenConfiguration();
let at = URL(fileURLWithPath: withUrl.toString());
print(at);
func openFilePathsWith(filePath: SRString, withUrl: SRString) {
let config = NSWorkspace.OpenConfiguration()
let at = URL(fileURLWithPath: withUrl.toString())
NSWorkspace.shared.open(
[URL(fileURLWithPath: fileUrl.toString())],
withApplicationAt: at,
configuration: config
)
// FIX-ME(HACK): The NULL split here is because I was not able to make this function accept a SRArray<SRString> argument.
// So, considering these are file paths, and \0 is not a valid character for a file path,
// I am using it as a delimitor to allow the rust side to pass in an array of files paths to this function
let fileURLs = filePath.toString().split(separator: "\0").map {
filePath in URL(fileURLWithPath: String(filePath))
}
// NSWorkspace.shared.openApplication(at: at, configuration: config) { (app, err) in
// print(app)
// print(err)
// }
NSWorkspace.shared.open(fileURLs, withApplicationAt: at, configuration: config)
}

View file

@ -21,7 +21,13 @@ pub struct OpenWithApplication {
pub name: SRString,
pub id: SRString,
pub url: SRString,
pub icon: SRData,
}
swift!(pub fn get_open_with_applications(url: &SRString) -> SRObjectArray<OpenWithApplication>);
swift!(pub fn open_file_path_with(file_url: &SRString, with_url: &SRString));
swift!(pub(crate) fn open_file_path_with(file_url: &SRString, with_url: &SRString));
pub fn open_file_paths_with(file_urls: &[&str], with_url: &str) {
let file_url = file_urls.join("\0");
unsafe { open_file_path_with(&file_url.as_str().into(), &with_url.into()) }
}

View file

@ -8,8 +8,8 @@ edition = { workspace = true }
[dependencies]
thiserror = "1.0.40"
normpath = "1.1.1"
libc = "0.2.146"
libc = "0.2"
[dependencies.windows]
[target.'cfg(target_os = "windows")'.dependencies.windows]
version = "0.48"
features = ["Win32_UI_Shell", "Win32_System_Com"]

View file

@ -30,6 +30,7 @@ opener = { version = "0.6.1", features = ["reveal"] }
specta = { workspace = true }
tauri-specta = { workspace = true, features = ["typescript"] }
uuid = { version = "1.3.3", features = ["serde"] }
futures = "0.3"
prisma-client-rust = { workspace = true }
sd-prisma = { path = "../../../crates/prisma" }

View file

@ -1,5 +1,6 @@
use std::{
collections::{BTreeSet, HashMap},
collections::{BTreeSet, HashMap, HashSet},
hash::{Hash, Hasher},
sync::Arc,
};
@ -38,10 +39,17 @@ pub async fn open_file_paths(
.into_iter()
.map(|(id, maybe_path)| {
if let Some(path) = maybe_path {
opener::open(path)
#[cfg(target_os = "linux")]
let open_result = sd_desktop_linux::open_file_path(&path);
#[cfg(not(target_os = "linux"))]
let open_result = opener::open(path);
open_result
.map(|_| OpenFilePathResult::AllGood(id))
.unwrap_or_else(|e| {
OpenFilePathResult::OpenError(id, e.to_string())
.unwrap_or_else(|err| {
error!("Failed to open logs dir: {err}");
OpenFilePathResult::OpenError(id, err.to_string())
})
} else {
OpenFilePathResult::NoFile(id)
@ -57,16 +65,26 @@ pub async fn open_file_paths(
Ok(res)
}
#[derive(Serialize, Type)]
#[derive(Serialize, Type, Debug, Clone)]
pub struct OpenWithApplication {
id: i32,
name: String,
#[cfg(target_os = "linux")]
url: std::path::PathBuf,
#[cfg(not(target_os = "linux"))]
url: String,
name: String,
}
impl Hash for OpenWithApplication {
fn hash<H: Hasher>(&self, state: &mut H) {
self.url.hash(state);
}
}
impl PartialEq for OpenWithApplication {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
}
}
impl Eq for OpenWithApplication {}
#[tauri::command(async)]
#[specta::specta]
pub async fn get_file_path_open_with_apps(
@ -87,125 +105,110 @@ pub async fn get_file_path_open_with_apps(
};
#[cfg(target_os = "macos")]
return Ok(paths
.into_iter()
.flat_map(|(id, path)| {
let Some(path) = path
else {
error!("File not found in database");
return vec![];
};
return {
Ok(paths
.into_values()
.flat_map(|path| {
let Some(path) = path.and_then(|path| path.into_os_string().into_string().ok())
else {
error!("File not found in database");
return None;
};
unsafe { sd_desktop_macos::get_open_with_applications(&path.to_str().unwrap().into()) }
.as_slice()
.iter()
.map(|app| OpenWithApplication {
id,
name: app.name.to_string(),
url: app.url.to_string(),
})
.collect::<Vec<_>>()
})
.collect());
Some(
unsafe { sd_desktop_macos::get_open_with_applications(&path.as_str().into()) }
.as_slice()
.iter()
.map(|app| OpenWithApplication {
url: app.url.to_string(),
name: app.name.to_string(),
})
.collect::<HashSet<_>>(),
)
})
.reduce(|intersection, set| intersection.intersection(&set).cloned().collect())
.map(|set| set.into_iter().collect())
.unwrap_or(vec![]))
};
#[cfg(target_os = "linux")]
{
use sd_desktop_linux::{DesktopEntry, HandlerType, SystemApps};
use futures::future;
use sd_desktop_linux::list_apps_associated_with_ext;
// TODO: cache this, and only update when the underlying XDG desktop apps changes
let Ok(system_apps) = SystemApps::populate()
.map_err(|e| { error!("{e:#?}"); })
else {
return Ok(vec![]);
};
return Ok(paths
.into_iter()
.flat_map(|(id, path)| {
let Some(path) = path
let apps = future::join_all(paths.into_values().map(|path| async {
let Some(path) = path
else {
error!("File not found in database");
return vec![];
return None;
};
let Some(name) = path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_string())
else {
error!("Failed to extract file name");
return vec![];
};
system_apps
.get_handlers(HandlerType::Ext(name))
.map(|handler| {
handler
.get_path()
.map_err(|e| {
error!("{e:#?}");
})
.and_then(|path| {
DesktopEntry::try_from(&path)
// TODO: Ignore desktop entries that have commands that don't exist/aren't available in path
.map(|entry| OpenWithApplication {
id,
name: entry.name,
url: path,
})
.map_err(|e| {
error!("{e:#?}");
})
})
Some(
list_apps_associated_with_ext(&path)
.await
.into_iter()
.map(|app| OpenWithApplication {
url: app.id,
name: app.name,
})
.collect::<Result<Vec<_>, _>>()
.unwrap_or(vec![])
})
.collect());
.collect::<HashSet<_>>(),
)
}))
.await;
return Ok(apps
.into_iter()
.flatten()
.reduce(|intersection, set| intersection.intersection(&set).cloned().collect())
.map(|set| set.into_iter().collect())
.unwrap_or(vec![]));
}
#[cfg(windows)]
return Ok(paths
.into_iter()
.flat_map(|(id, path)| {
.into_values()
.filter_map(|path| {
let Some(path) = path
else {
error!("File not found in database");
return vec![];
return None;
};
let Some(ext) = path.extension()
else {
error!("Failed to extract file extension");
return vec![];
return None;
};
sd_desktop_windows::list_apps_associated_with_ext(ext)
.map_err(|e| {
error!("{e:#?}");
})
.map(|handlers| {
handlers
.iter()
.filter_map(|handler| {
let (Ok(name), Ok(url)) = (
unsafe { handler.GetUIName() }.map_err(|e| { error!("{e:#?}");})
.and_then(|name| unsafe { name.to_string() }
.map_err(|e| { error!("{e:#?}");})),
unsafe { handler.GetName() }.map_err(|e| { error!("{e:#?}");})
.and_then(|name| unsafe { name.to_string() }
.map_err(|e| { error!("{e:#?}");})),
) else {
error!("Failed to get handler info");
return None
};
Some(OpenWithApplication { id, name, url })
})
.collect::<Vec<_>>()
})
.unwrap_or(vec![])
.ok()
})
.collect());
.map(|handler| {
handler
.iter()
.filter_map(|handler| {
let (Ok(name), Ok(url)) = (
unsafe { handler.GetUIName() }.map_err(|e| { error!("{e:#?}");})
.and_then(|name| unsafe { name.to_string() }
.map_err(|e| { error!("{e:#?}");})),
unsafe { handler.GetName() }.map_err(|e| { error!("{e:#?}");})
.and_then(|name| unsafe { name.to_string() }
.map_err(|e| { error!("{e:#?}");})),
) else {
error!("Failed to get handler info");
return None
};
Some(OpenWithApplication { name, url })
})
.collect::<HashSet<_>>()
})
.reduce(|intersection, set| intersection.intersection(&set).cloned().collect())
.map(|set| set.into_iter().collect())
.unwrap_or(vec![]));
#[allow(unreachable_code)]
Ok(vec![])
@ -252,21 +255,14 @@ pub async fn open_file_path_with(
#[cfg(target_os = "macos")]
return {
unsafe {
sd_desktop_macos::open_file_path_with(
&path.into(),
&url.as_str().into(),
)
};
sd_desktop_macos::open_file_paths_with(&[path], url);
Ok(())
};
#[cfg(target_os = "linux")]
return sd_desktop_linux::Handler::assume_valid(url.into())
.open(&[path])
.map_err(|e| {
error!("{e:#?}");
});
return sd_desktop_linux::open_files_path_with(&[path], url).map_err(|e| {
error!("{e:#?}");
});
#[cfg(windows)]
return sd_desktop_windows::open_file_path_with(path, url).map_err(|e| {
@ -344,7 +340,34 @@ pub async fn reveal_items(
}
for path in paths_to_open {
opener::reveal(path).ok();
#[cfg(target_os = "linux")]
if sd_desktop_linux::is_appimage() {
// This is a workaround for the app, when package inside an AppImage, crashing when using opener::reveal.
sd_desktop_linux::open_file_path(
&(if path.is_file() {
path.parent().unwrap_or(&path)
} else {
&path
}),
)
.map_err(|err| {
error!("Failed to open logs dir: {err}");
})
.ok()
} else {
opener::reveal(path)
.map_err(|err| {
error!("Failed to open logs dir: {err}");
})
.ok()
};
#[cfg(not(target_os = "linux"))]
opener::reveal(path)
.map_err(|err| {
error!("Failed to open logs dir: {err}");
})
.ok();
}
Ok(())

View file

@ -9,7 +9,7 @@ use sd_core::{custom_uri::create_custom_uri_endpoint, Node, NodeError};
use tauri::{
api::path, async_runtime::block_on, ipc::RemoteDomainAccessScope, plugin::TauriPlugin,
AppHandle, Manager, RunEvent, Runtime, WindowEvent,
AppHandle, Manager, RunEvent, Runtime,
};
use tokio::{task::block_in_place, time::sleep};
use tracing::{debug, error};
@ -51,8 +51,17 @@ async fn reset_spacedrive(app_handle: AppHandle) {
#[tauri::command(async)]
#[specta::specta]
async fn open_logs_dir(node: tauri::State<'_, Arc<Node>>) -> Result<(), ()> {
opener::open(node.data_dir.join("logs")).ok();
Ok(())
let logs_path = node.data_dir.join("logs");
#[cfg(target_os = "linux")]
let open_result = sd_desktop_linux::open_file_path(&logs_path);
#[cfg(not(target_os = "linux"))]
let open_result = opener::open(logs_path);
open_result.map_err(|err| {
error!("Failed to open logs dir: {err}");
})
}
pub fn tauri_error_plugin<R: Runtime>(err: NodeError) -> TauriPlugin<R> {
@ -75,6 +84,9 @@ macro_rules! tauri_handlers {
#[tokio::main]
async fn main() -> tauri::Result<()> {
#[cfg(target_os = "linux")]
sd_desktop_linux::normalize_environment();
#[cfg(target_os = "linux")]
let (tx, rx) = tokio::sync::mpsc::channel(1);
@ -122,7 +134,7 @@ async fn main() -> tauri::Result<()> {
// Instead, the window is hidden and the dock icon remains so that on user click it should show the window again.
#[cfg(target_os = "macos")]
let app = app.on_window_event(|event| {
if let WindowEvent::CloseRequested { api, .. } = event.event() {
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
if event.window().label() == "main" {
AppHandle::hide(&event.window().app_handle()).expect("Window should hide on macOS");
api.prevent_close();

View file

@ -42,7 +42,7 @@ export function lockAppTheme(themeType: AppThemeType) {
return invoke()<null>("lock_app_theme", { themeType })
}
export type AppThemeType = "Auto" | "Light" | "Dark"
export type OpenWithApplication = { url: string; name: string }
export type OpenFilePathResult = { t: "NoLibrary" } | { t: "NoFile"; c: number } | { t: "OpenError"; c: [number, string] } | { t: "AllGood"; c: number } | { t: "Internal"; c: string }
export type RevealItem = { Location: { id: number } } | { FilePath: { id: number } }
export type OpenWithApplication = { id: number; name: string; url: string }
export type AppThemeType = "Auto" | "Light" | "Dark"

View file

@ -9,8 +9,6 @@ license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ffmpeg-sys-next = "6.0.1"
tracing = "0.1.37"

View file

@ -8,7 +8,6 @@ authors = [
license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0.163", features = ["derive"] }

View file

@ -5,10 +5,8 @@ license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[target.'cfg(target_os = "macos")'.dependencies]
swift-rs = { workspace = true }
[build-dependencies]
[target.'cfg(target_os = "macos")'.build-dependencies]
swift-rs = { workspace = true, features = ["build"] }

View file

@ -3,8 +3,6 @@ name = "sd-prisma"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
prisma-client-rust = { workspace = true }
serde = "1.0"

View file

@ -5,8 +5,6 @@ license = { workspace = true }
repository = { workspace = true }
edition = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nom = "7.1.3"
once_cell = "1.17.2"