use crate::{ error::{AppError, Result}, model::ProviderIdentity, }; use glib::variant::ToVariant; use std::{collections::HashSet, path::PathBuf}; pub const ALIASES: &[(&str, &str)] = &[ ("KDE KWallet", "org.kde.secretservicecompat"), ("KDE KWallet", "org.kde.kwalletd6"), ("KDE KWallet", "org.kde.kwalletd5"), ("GNOME Keyring", "org.gnome.keyring"), ("System Secret Service", "org.freedesktop.secrets"), ]; #[derive(Clone)] pub struct ProviderCandidate { pub label: String, pub identity: Option, pub recommended: bool, } pub(crate) fn bus() -> Result { gio::bus_get_sync(gio::BusType::Session, gio::Cancellable::NONE).map_err(|_| { AppError::new( "WALLET_UNAVAILABLE", "No desktop session bus. Run setup from your desktop session.", ) }) } pub(crate) fn call( bus: &gio::DBusConnection, method: &str, params: Option<&glib::Variant>, ) -> Result { bus.call_sync( Some("org.freedesktop.DBus"), "/org/freedesktop/DBus", "org.freedesktop.DBus", method, params, None, gio::DBusCallFlags::NO_AUTO_START, 5000, gio::Cancellable::NONE, ) .map_err(|_| { AppError::new( "WALLET_UNAVAILABLE", "The selected wallet is not running. Open its native manager and retry.", ) }) } pub(crate) fn owner_identity(bus: &gio::DBusConnection, name: &str) -> Result<(String, PathBuf)> { let owner = call(bus, "GetNameOwner", Some(&(name,).to_variant()))? .get::<(String,)>() .ok_or_else(AppError::input)? .0; let pid = call( bus, "GetConnectionUnixProcessID", Some(&(owner.as_str(),).to_variant()), )? .get::<(u32,)>() .ok_or_else(AppError::input)? .0; let exe = std::fs::read_link(format!("/proc/{pid}/exe")).map_err(|_| { AppError::new( "WALLET_UNAVAILABLE", "Cannot identify the selected wallet process.", ) })?; Ok((owner, exe)) } pub(crate) fn bind_selected(identity: &ProviderIdentity) -> Result { let bus = bus()?; let (owner, exe) = owner_identity(&bus, &identity.bus_name)?; if exe != identity.executable { return Err(AppError::new( "WALLET_PROVIDER_CHANGED", "The wallet provider changed. Run setup to review its selection.", )); } Ok(owner) } pub fn discover_providers() -> Result> { let bus = bus()?; let names = call(&bus, "ListNames", None)? .get::<(Vec,)>() .ok_or_else(AppError::input)? .0; let inactive = call(&bus, "ListActivatableNames", None)? .get::<(Vec,)>() .ok_or_else(AppError::input)? .0; let desktop = std::env::var("XDG_CURRENT_DESKTOP") .unwrap_or_default() .to_lowercase(); let mut seen = HashSet::new(); let mut output = Vec::new(); for (label, name) in ALIASES { if names.iter().any(|n| n == name) { if let Ok((owner, exe)) = owner_identity(&bus, name) { if seen.insert(owner) { let recommended = (desktop.contains("kde") && exe.to_string_lossy().contains("kwallet")) || (desktop.contains("gnome") && exe.to_string_lossy().contains("gnome-keyring")); output.push(ProviderCandidate { label: label.to_string(), identity: Some(ProviderIdentity { bus_name: name.to_string(), executable: exe, }), recommended, }); } } } else if inactive.iter().any(|n| n == name) && !output.iter().any(|p| p.label == *label) { output.push(ProviderCandidate { label: format!("{label} (not running; open its manager)"), identity: None, recommended: false, }) } } output.sort_by_key(|p| (!p.recommended, p.identity.is_none())); Ok(output) }