diff --git a/.editorconfig b/.editorconfig index 8549c42f9..462a3da63 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.github/scripts/setup-system.sh b/.github/scripts/setup-system.sh index d3e5aea51..e93c1f50d 100755 --- a/.github/scripts/setup-system.sh +++ b/.github/scripts/setup-system.sh @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4cace3e44..9fd6e5e41 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: | diff --git a/.vscode/settings.json b/.vscode/settings.json index bc82ff68a..41ac7fed5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -65,7 +65,6 @@ "packages/config", "packages/ui" ], - "eslint.packageManager": "pnpm", "eslint.lintTask.enable": true, "explorer.fileNesting.enabled": true, "explorer.fileNesting.patterns": { diff --git a/Cargo.lock b/Cargo.lock index 2e8e3e341..46e83631d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index ebdcb92b5..b60a717b2 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -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"] } diff --git a/apps/desktop/crates/linux/Cargo.toml b/apps/desktop/crates/linux/Cargo.toml index febbc8a91..ed82b3a3b 100644 --- a/apps/desktop/crates/linux/Cargo.toml +++ b/apps/desktop/crates/linux/Cargo.toml @@ -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" diff --git a/apps/desktop/crates/linux/src/app_info.rs b/apps/desktop/crates/linux/src/app_info.rs new file mode 100644 index 000000000..4729196bd --- /dev/null +++ b/apps/desktop/crates/linux/src/app_info.rs @@ -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, + env_name: &str, + prefix: &impl AsRef, +) { + 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::().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>, +} + +async fn recommended_for_type(file_path: impl AsRef) -> Vec { + 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) -> Vec { + 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], 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::>(), + Some(ctx), + ) + }) +} + +pub fn open_file_path(file_path: &impl AsRef) -> 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))) +} diff --git a/apps/desktop/crates/linux/src/desktop_entry.rs b/apps/desktop/crates/linux/src/desktop_entry.rs deleted file mode 100644 index 98b583932..000000000 --- a/apps/desktop/crates/linux/src/desktop_entry.rs +++ /dev/null @@ -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, - pub categories: HashMap, -} - -#[derive(PartialEq, Eq, Copy, Clone)] -pub enum Mode { - Launch, - Open, -} - -fn terminal() -> Result { - // 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)> { - 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 { - 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::>(); - } - "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 { - 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"); - } -} diff --git a/apps/desktop/crates/linux/src/env.rs b/apps/desktop/crates/linux/src/env.rs new file mode 100644 index 000000000..d280877de --- /dev/null +++ b/apps/desktop/crates/linux/src/env.rs @@ -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 { + 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::() }; + let mut result = ptr::null_mut::(); + + 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, 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::>(); + + 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::>(); + + 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, +) -> Option { + 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 +} diff --git a/apps/desktop/crates/linux/src/handler.rs b/apps/desktop/crates/linux/src/handler.rs deleted file mode 100644 index be0932d2d..000000000 --- a/apps/desktop/crates/linux/src/handler.rs +++ /dev/null @@ -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 { - 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 { - 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::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) - } -} diff --git a/apps/desktop/crates/linux/src/lib.rs b/apps/desktop/crates/linux/src/lib.rs index 617d0fc5a..14ddcb43e 100644 --- a/apps/desktop/crates/linux/src/lib.rs +++ b/apps/desktop/crates/linux/src/lib.rs @@ -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 = std::result::Result; - -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}; diff --git a/apps/desktop/crates/linux/src/system.rs b/apps/desktop/crates/linux/src/system.rs deleted file mode 100644 index 7ea6fd07d..000000000 --- a/apps/desktop/crates/linux/src/system.rs +++ /dev/null @@ -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>); - -impl SystemApps { - pub fn get_handlers(&self, handler_type: HandlerType) -> impl Iterator { - 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> { - 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 { - let mut map = HashMap::>::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)) - } -} diff --git a/apps/desktop/crates/linux/tests/cmus.desktop b/apps/desktop/crates/linux/tests/cmus.desktop deleted file mode 100644 index a26121d85..000000000 --- a/apps/desktop/crates/linux/tests/cmus.desktop +++ /dev/null @@ -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; diff --git a/apps/desktop/crates/macos/Cargo.toml b/apps/desktop/crates/macos/Cargo.toml index dcff8a575..e6d612df9 100644 --- a/apps/desktop/crates/macos/Cargo.toml +++ b/apps/desktop/crates/macos/Cargo.toml @@ -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"] } diff --git a/apps/desktop/crates/macos/src-swift/files.swift b/apps/desktop/crates/macos/src-swift/files.swift index 8a421201c..4af38e41f 100644 --- a/apps/desktop/crates/macos/src-swift/files.swift +++ b/apps/desktop/crates/macos/src-swift/files.swift @@ -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 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) } diff --git a/apps/desktop/crates/macos/src/lib.rs b/apps/desktop/crates/macos/src/lib.rs index 4aceb3043..7cbb1317e 100644 --- a/apps/desktop/crates/macos/src/lib.rs +++ b/apps/desktop/crates/macos/src/lib.rs @@ -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); -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()) } +} diff --git a/apps/desktop/crates/windows/Cargo.toml b/apps/desktop/crates/windows/Cargo.toml index b09ecacb7..8ca878561 100644 --- a/apps/desktop/crates/windows/Cargo.toml +++ b/apps/desktop/crates/windows/Cargo.toml @@ -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"] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 9ecffc668..5cbdf0e27 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -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" } diff --git a/apps/desktop/src-tauri/src/file.rs b/apps/desktop/src-tauri/src/file.rs index 91528d59e..27f42490d 100644 --- a/apps/desktop/src-tauri/src/file.rs +++ b/apps/desktop/src-tauri/src/file.rs @@ -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(&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::>() - }) - .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::>(), + ) + }) + .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::, _>>() - .unwrap_or(vec![]) - }) - .collect()); + .collect::>(), + ) + })) + .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::>() - }) - .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::>() + }) + .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(()) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 58be59f65..0b8238e60 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -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>) -> 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(err: NodeError) -> TauriPlugin { @@ -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(); diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index 3d136f3ef..696e03ad6 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -42,7 +42,7 @@ export function lockAppTheme(themeType: AppThemeType) { return invoke()("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" diff --git a/crates/ffmpeg/Cargo.toml b/crates/ffmpeg/Cargo.toml index e1efe2159..0099ae093 100644 --- a/crates/ffmpeg/Cargo.toml +++ b/crates/ffmpeg/Cargo.toml @@ -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" diff --git a/crates/file-ext/Cargo.toml b/crates/file-ext/Cargo.toml index 4023b1a28..633a62ca5 100644 --- a/crates/file-ext/Cargo.toml +++ b/crates/file-ext/Cargo.toml @@ -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"] } diff --git a/crates/macos/Cargo.toml b/crates/macos/Cargo.toml index 157d4f6c8..ac866d9f4 100644 --- a/crates/macos/Cargo.toml +++ b/crates/macos/Cargo.toml @@ -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"] } diff --git a/crates/prisma/Cargo.toml b/crates/prisma/Cargo.toml index fca3f28af..5a6e7ab73 100644 --- a/crates/prisma/Cargo.toml +++ b/crates/prisma/Cargo.toml @@ -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" diff --git a/crates/sync-generator/Cargo.toml b/crates/sync-generator/Cargo.toml index 9fcc97031..f68b0d13f 100644 --- a/crates/sync-generator/Cargo.toml +++ b/crates/sync-generator/Cargo.toml @@ -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"