use cosmic::{ iced::{futures::SinkExt, subscription}, widget, Command, }; use gio::{glib, prelude::*}; use std::{any::TypeId, future::pending, path::PathBuf, sync::Arc}; use tokio::sync::{mpsc, Mutex}; use super::{Mounter, MounterAuth, MounterItem, MounterItems, MounterMessage}; fn gio_icon_to_path(icon: &gio::Icon, size: u16) -> Option { if let Some(themed_icon) = icon.downcast_ref::() { for name in themed_icon.names() { let named = widget::icon::from_name(name.as_str()).size(size); if let Some(path) = named.path() { return Some(path); } } } //TODO: handle more gio icon types None } enum Cmd { Rescan, Mount(MounterItem), NetworkDrive(String), Unmount(MounterItem), } enum Event { Changed, Items(MounterItems), NetworkAuth(String, MounterAuth, mpsc::Sender), NetworkResult(String, Result), } #[derive(Clone, Debug)] enum ItemKind { Mount, Volume, } //TODO: better method of matching items #[derive(Clone, Debug)] pub struct Item { kind: ItemKind, index: usize, name: String, is_mounted: bool, icon_opt: Option, path_opt: Option, } impl Item { pub fn name(&self) -> String { self.name.clone() } pub fn is_mounted(&self) -> bool { self.is_mounted } pub fn icon(&self) -> Option { self.icon_opt .as_ref() .map(|icon| widget::icon::from_path(icon.clone())) } pub fn path(&self) -> Option { self.path_opt.clone() } } pub struct Gvfs { command_tx: mpsc::UnboundedSender, event_rx: Arc>>, } impl Gvfs { pub fn new() -> Self { //TODO: switch to using gvfs-zbus which will better integrate with async rust let (command_tx, mut command_rx) = mpsc::unbounded_channel(); let (event_tx, event_rx) = mpsc::unbounded_channel(); std::thread::spawn(move || { let main_loop = glib::MainLoop::new(None, false); main_loop.context().spawn_local(async move { let monitor = gio::VolumeMonitor::get(); { let event_tx = event_tx.clone(); monitor.connect_mount_changed(move |_monitor, mount| { log::info!("mount changed {}", MountExt::name(mount)); event_tx.send(Event::Changed).unwrap(); }); } { let event_tx = event_tx.clone(); monitor.connect_mount_added(move |_monitor, mount| { log::info!("mount added {}", MountExt::name(mount)); event_tx.send(Event::Changed).unwrap(); }); } { let event_tx = event_tx.clone(); monitor.connect_mount_removed(move |_monitor, mount| { log::info!("mount removed {}", MountExt::name(mount)); event_tx.send(Event::Changed).unwrap(); }); } { let event_tx = event_tx.clone(); monitor.connect_volume_changed(move |_monitor, volume| { log::info!("volume changed {}", VolumeExt::name(volume)); event_tx.send(Event::Changed).unwrap(); }); } { let event_tx = event_tx.clone(); monitor.connect_volume_added(move |_monitor, volume| { log::info!("volume added {}", VolumeExt::name(volume)); event_tx.send(Event::Changed).unwrap(); }); } { let event_tx = event_tx.clone(); monitor.connect_volume_removed(move |_monitor, volume| { log::info!("volume removed {}", VolumeExt::name(volume)); event_tx.send(Event::Changed).unwrap(); }); } while let Some(command) = command_rx.recv().await { match command { Cmd::Rescan => { let mut items = MounterItems::new(); for (i, mount) in monitor.mounts().into_iter().enumerate() { items.push(MounterItem::Gvfs(Item { kind: ItemKind::Mount, index: i, name: MountExt::name(&mount).to_string(), is_mounted: true, icon_opt: gio_icon_to_path( &MountExt::symbolic_icon(&mount), 16, ), path_opt: MountExt::root(&mount).path(), })); } for (i, volume) in monitor.volumes().into_iter().enumerate() { if volume.get_mount().is_some() { // Volumes with mounts are already listed by mount continue; } items.push(MounterItem::Gvfs(Item { kind: ItemKind::Volume, index: i, name: VolumeExt::name(&volume).to_string(), is_mounted: false, icon_opt: gio_icon_to_path( &VolumeExt::symbolic_icon(&volume), 16, ), path_opt: None, })); } event_tx.send(Event::Items(items)).unwrap(); } Cmd::Mount(mounter_item) => { let MounterItem::Gvfs(item) = mounter_item else { continue }; let ItemKind::Volume = item.kind else { continue }; for (i, volume) in monitor.volumes().into_iter().enumerate() { if i != item.index { continue; } let name = VolumeExt::name(&volume); if item.name != name { log::warn!("trying to mount volume {} failed: name is {:?} when {:?} was expected", i, name, item.name); continue; } log::info!("mount {}", name); VolumeExt::mount( &volume, gio::MountMountFlags::NONE, //TODO: gio::MountOperation needed for network shares with auth gio::MountOperation::NONE, gio::Cancellable::NONE, move |result| { log::info!("mount {}: result {:?}", name, result); }, ); } } Cmd::NetworkDrive(uri) => { let mount_op = gio::MountOperation::new(); { let event_tx = event_tx.clone(); let uri = uri.clone(); mount_op.connect_ask_password(move |mount_op, message, default_user, default_domain, flags| { let auth = MounterAuth { message: message.to_string(), username_opt: if flags.contains(gio::AskPasswordFlags::NEED_USERNAME) { Some(default_user.to_string()) } else { None }, domain_opt: if flags.contains(gio::AskPasswordFlags::NEED_DOMAIN) { Some(default_domain.to_string()) } else { None }, password_opt: if flags.contains(gio::AskPasswordFlags::NEED_PASSWORD) { Some(String::new()) } else { None }, remember_opt: if flags.contains(gio::AskPasswordFlags::SAVING_SUPPORTED) { Some(false) } else { None }, anonymous_opt: if flags.contains(gio::AskPasswordFlags::ANONYMOUS_SUPPORTED) { Some(false) } else { None } }; let (auth_tx, mut auth_rx) = mpsc::channel(1); event_tx.send(Event::NetworkAuth(uri.clone(), auth, auth_tx)).unwrap(); //TODO: async recv? if let Some(auth) = auth_rx.blocking_recv() { if auth.anonymous_opt == Some(true) { mount_op.set_anonymous(true); } else { mount_op.set_username(auth.username_opt.as_deref()); mount_op.set_domain(auth.domain_opt.as_deref()); mount_op.set_password(auth.password_opt.as_deref()); if auth.remember_opt == Some(true) { mount_op.set_password_save(gio::PasswordSave::Permanently); } } mount_op.reply(gio::MountOperationResult::Handled); } else { mount_op.reply(gio::MountOperationResult::Aborted); } }); } let file = gio::File::for_uri(&uri); let event_tx = event_tx.clone(); file.mount_enclosing_volume( gio::MountMountFlags::empty(), Some(&mount_op), gio::Cancellable::NONE, move |res| { log::info!("network drive {}: result {:?}", uri, res); event_tx.send(Event::NetworkResult(uri, match res { Ok(()) => Ok(true), Err(err) => match err.kind::() { Some(gio::IOErrorEnum::FailedHandled) => Ok(false), _ => Err(format!("{}", err)) } })).unwrap(); } ); } Cmd::Unmount(mounter_item) => { let MounterItem::Gvfs(item) = mounter_item else { continue }; let ItemKind::Mount = item.kind else { continue }; for (i, mount) in monitor.mounts().into_iter().enumerate() { if i != item.index { continue; } let name = MountExt::name(&mount); if item.name != name { log::warn!("trying to unmount mount {} failed: name is {:?} when {:?} was expected", i, name, item.name); continue; } //TODO: do eject instead of unmount? log::info!("unmount {}", name); MountExt::unmount_with_operation( &mount, gio::MountUnmountFlags::NONE, //TODO: gio::MountOperation needed for network shares with auth gio::MountOperation::NONE, gio::Cancellable::NONE, move |result| { log::info!("unmount {}: result {:?}", name, result); }, ); } } } } }); main_loop.run() }); Self { command_tx, event_rx: Arc::new(Mutex::new(event_rx)), } } } impl Mounter for Gvfs { fn mount(&self, item: MounterItem) -> Command<()> { let command_tx = self.command_tx.clone(); Command::perform( async move { command_tx.send(Cmd::Mount(item)).unwrap(); () }, |x| x, ) } fn network_drive(&self, uri: String) -> Command<()> { let command_tx = self.command_tx.clone(); Command::perform( async move { command_tx.send(Cmd::NetworkDrive(uri)).unwrap(); () }, |x| x, ) } fn unmount(&self, item: MounterItem) -> Command<()> { let command_tx = self.command_tx.clone(); Command::perform( async move { command_tx.send(Cmd::Unmount(item)).unwrap(); () }, |x| x, ) } fn subscription(&self) -> subscription::Subscription { let command_tx = self.command_tx.clone(); let event_rx = self.event_rx.clone(); subscription::channel(TypeId::of::(), 1, |mut output| async move { command_tx.send(Cmd::Rescan).unwrap(); while let Some(event) = event_rx.lock().await.recv().await { match event { Event::Changed => command_tx.send(Cmd::Rescan).unwrap(), Event::Items(items) => output.send(MounterMessage::Items(items)).await.unwrap(), Event::NetworkAuth(uri, auth, auth_tx) => output .send(MounterMessage::NetworkAuth(uri, auth, auth_tx)) .await .unwrap(), Event::NetworkResult(uri, res) => output .send(MounterMessage::NetworkResult(uri, res)) .await .unwrap(), } } pending().await }) } }