mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
[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:
parent
447069388b
commit
7e4ed2b547
|
@ -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
|
||||
|
|
6
.github/scripts/setup-system.sh
vendored
6
.github/scripts/setup-system.sh
vendored
|
@ -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"
|
||||
|
|
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
|
@ -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: |
|
||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -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
111
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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"
|
||||
|
|
140
apps/desktop/crates/linux/src/app_info.rs
Normal file
140
apps/desktop/crates/linux/src/app_info.rs
Normal 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)))
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
226
apps/desktop/crates/linux/src/env.rs
Normal file
226
apps/desktop/crates/linux/src/env.rs
Normal 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 there’s 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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"] }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue