diff --git a/Cargo.lock b/Cargo.lock index 8a47b9e5f..b085f708d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1857,6 +1857,7 @@ dependencies = [ "duckdb", "font-kit", "futures", + "libloading 0.8.9", "log", "mongodb", "objc2", diff --git a/flake.lock b/flake.lock index e955dedea..7fd8c488f 100644 --- a/flake.lock +++ b/flake.lock @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1783488441, - "narHash": "sha256-jmWf+H3iC/0z7/mmvTovaJx1E/LME1QyHkyk+AFfJPk=", + "lastModified": 1783614422, + "narHash": "sha256-zSX553aXiIKJHWzUF6nrDKgeNRlfNK/SdyNYSbs2nkM=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "7dc3a177a239ed879c5581a80f6dc246c7e102f1", + "rev": "e8e9d70b96113fd49ccde6c3e08fb260b6ef5dd6", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 32b4c202d..53c166496 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,7 @@ webkitgtk_4_1 gtk3 libappindicator-gtk3 + libayatana-appindicator # provides libayatana-appindicator3.so.1 (dlopen'd at runtime) librsvg patchelf openssl @@ -205,6 +206,8 @@ pkgs.rustPlatform.cargoSetupHook # sets CARGO_HOME to cargoDeps pkgs.pnpmConfigHook # sets up pnpm offline store pkgs.desktop-file-utils # for `desktop-file-validate` + pkgs.copyDesktopItems # installs desktopItem into share/applications + pkgs.imagemagick # generates correctly sized hicolor icons ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux ( with pkgs; @@ -272,6 +275,21 @@ # Tauri reads the version from this env var during build TAURI_SKIP_DEVSERVER_CHECK = "true"; + # ── Runtime library path injection ───────────────────────────────── # + # libappindicator-sys uses dlopen() at runtime to load the appindicator + # shared library. dlopen() does NOT honour the binary's RPATH — it only + # searches LD_LIBRARY_PATH and system paths. In a Nix derivation the + # library lives in the store, not in /usr/lib, so we must inject the + # path explicitly into the wrapGAppsHook3 C-wrapper. + # + # IMPORTANT: wrapGAppsHook3 uses its own `gappsWrapperArgs` bash array + # (NOT `makeWrapperArgs`) — inject via preFixup before the hook runs. + preFixup = pkgs.lib.optionalString pkgs.stdenv.isLinux '' + gappsWrapperArgs+=( + --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath linuxTauriDeps}" + ) + ''; + # ── Build phases ─────────────────────────────────────────────────── # preConfigure = '' export HOME=$TMPDIR @@ -312,9 +330,10 @@ # tauri build --no-bundle puts the binary at target/release/dbx cp target/release/dbx $out/bin/dbx - # Copy desktop integration files if present. - # Install every PNG size Tauri ships so the hicolor theme lookup - # (e.g. panels @ 48px, launchers @ 128px) always succeeds. + # Install icon files into the hicolor theme tree so that all + # desktop environments (GNOME Shell, KDE Plasma, XFCE, etc.) can + # find the right size: task-switcher (32px), panel (48px), + # launcher (64px), app-menu (128px), HiDPI launcher (256px). if [ -d src-tauri/icons ]; then for size in 32 128; do if [ -f "src-tauri/icons/''${size}x''${size}.png" ]; then @@ -323,12 +342,36 @@ "$out/share/icons/hicolor/''${size}x''${size}/apps/dbx.png" fi done - # @2x retina variant for 128px + + # @2x retina variant (128x128@2x) → install as 256x256 if [ -f "src-tauri/icons/128x128@2x.png" ]; then mkdir -p "$out/share/icons/hicolor/256x256/apps" cp "src-tauri/icons/128x128@2x.png" \ "$out/share/icons/hicolor/256x256/apps/dbx.png" fi + + # Generate missing common sizes so hicolor directory metadata + # always matches the actual PNG dimensions. + for size in 16 48 64; do + mkdir -p "$out/share/icons/hicolor/''${size}x''${size}/apps" + if [ "$size" -le 32 ] && [ -f "src-tauri/icons/32x32.png" ]; then + src="src-tauri/icons/32x32.png" + elif [ -f "src-tauri/icons/128x128.png" ]; then + src="src-tauri/icons/128x128.png" + else + continue + fi + magick "$src" -resize "''${size}x''${size}" \ + "$out/share/icons/hicolor/''${size}x''${size}/apps/dbx.png" + done + + # Install the full-size icon.png as the scalable fallback so that + # Tauri's default_window_icon() and the taskbar always have an image. + if [ -f "src-tauri/icons/icon.png" ]; then + mkdir -p "$out/share/icons/hicolor/512x512/apps" + cp "src-tauri/icons/icon.png" \ + "$out/share/icons/hicolor/512x512/apps/dbx.png" + fi fi # Register the freedesktop .desktop file so app launchers (GNOME diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cb12b21d0..488604a48 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -66,3 +66,6 @@ tauri-plugin-clipboard-manager = "2.3.2" objc2 = { version = "0.6.4", default-features = false, features = ["std"] } objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSApplication", "NSImage"] } objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSData"] } + +[target.'cfg(target_os = "linux")'.dependencies] +libloading = "0.8" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index da55a291a..7cb454eb4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -69,8 +69,25 @@ fn should_hide_window_on_close(target_os: &str) -> bool { matches!(target_os, "macos" | "windows") } -fn should_setup_desktop_tray(target_os: &str, show_tray_icon: bool) -> bool { - show_tray_icon && matches!(target_os, "macos" | "windows") +fn should_setup_desktop_tray(target_os: &str, show_tray_icon: bool, linux_appindicator_available: bool) -> bool { + show_tray_icon + && (matches!(target_os, "macos" | "windows") || (target_os == "linux" && linux_appindicator_available)) +} + +#[cfg(target_os = "linux")] +fn linux_appindicator_available() -> bool { + const APPINDICATOR_LIBRARIES: &[&str] = &["libayatana-appindicator3.so.1", "libappindicator3.so.1"]; + + APPINDICATOR_LIBRARIES.iter().any(|library| { + // tray-icon loads AppIndicator dynamically and panics when neither ABI is + // installed, so probe the same libraries before entering that code path. + unsafe { libloading::Library::new(library).is_ok() } + }) +} + +#[cfg(not(target_os = "linux"))] +fn linux_appindicator_available() -> bool { + false } #[cfg(test)] @@ -449,7 +466,15 @@ fn apply_desktop_tray_icon_theme(app: &tauri::AppHandle, _icon_theme: DesktopIco }; _tray.set_icon(icon)?; } - #[cfg(not(any(target_os = "macos", target_os = "windows")))] + #[cfg(target_os = "linux")] + { + let icon = match _icon_theme { + DesktopIconTheme::Default => app.default_window_icon().cloned(), + DesktopIconTheme::Black => Some(BLACK_APP_ICON), + }; + _tray.set_icon(icon)?; + } + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] { let _ = (_tray, _icon_theme); } @@ -460,7 +485,8 @@ fn apply_desktop_tray_icon_theme(app: &tauri::AppHandle, _icon_theme: DesktopIco pub(crate) fn apply_desktop_settings(app: &tauri::AppHandle, desktop_settings: &DesktopSettings) -> tauri::Result<()> { apply_debug_log_level(desktop_settings.debug_logging_enabled); apply_desktop_icon_theme(app, desktop_settings.icon_theme)?; - if matches!(std::env::consts::OS, "macos" | "windows") { + if should_setup_desktop_tray(std::env::consts::OS, desktop_settings.show_tray_icon, linux_appindicator_available()) + { if let Some(tray) = app.tray_by_id(DESKTOP_TRAY_ID) { tray.set_visible(desktop_settings.show_tray_icon)?; apply_desktop_tray_icon_theme(app, desktop_settings.icon_theme)?; @@ -496,12 +522,14 @@ mod tests { } #[test] - fn sets_up_desktop_tray_for_windows_and_macos() { - assert!(should_setup_desktop_tray("windows", true)); - assert!(should_setup_desktop_tray("macos", true)); - assert!(!should_setup_desktop_tray("windows", false)); - assert!(!should_setup_desktop_tray("macos", false)); - assert!(!should_setup_desktop_tray("linux", true)); + fn sets_up_desktop_tray_for_windows_macos_and_linux() { + assert!(should_setup_desktop_tray("windows", true, false)); + assert!(should_setup_desktop_tray("macos", true, false)); + assert!(should_setup_desktop_tray("linux", true, true)); + assert!(!should_setup_desktop_tray("linux", true, false)); + assert!(!should_setup_desktop_tray("windows", false, true)); + assert!(!should_setup_desktop_tray("macos", false, true)); + assert!(!should_setup_desktop_tray("linux", false, true)); } #[cfg(target_os = "macos")] @@ -800,7 +828,11 @@ pub fn run() { let _ = window.set_decorations(decorations); } } - if should_setup_desktop_tray(std::env::consts::OS, desktop_settings.show_tray_icon) { + if should_setup_desktop_tray( + std::env::consts::OS, + desktop_settings.show_tray_icon, + linux_appindicator_available(), + ) { setup_desktop_tray(app, desktop_settings.icon_theme)?; } apply_desktop_icon_theme(app.handle(), desktop_settings.icon_theme)?;