From 2b1abc7c23acae9708d0a3d04113230f3399a7a7 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 22 Apr 2024 13:14:25 -0600 Subject: [PATCH] Refresh gvfs volumes as needed --- examples/gvfs.rs | 14 +++ src/app.rs | 113 ++++++++++++-------- src/mounter/gvfs.rs | 251 +++++++++++++++++++++++++++++++++++++------- src/mounter/mod.rs | 52 +++++++-- 4 files changed, 342 insertions(+), 88 deletions(-) diff --git a/examples/gvfs.rs b/examples/gvfs.rs index 5ba3f4c..d0a2bab 100644 --- a/examples/gvfs.rs +++ b/examples/gvfs.rs @@ -4,22 +4,36 @@ fn main() { let monitor = gio::VolumeMonitor::get(); for drive in monitor.connected_drives() { println!("Drive: {}", drive.name()); + for id in drive.enumerate_identifiers() { + println!(" ID: {}={:?}", id, drive.identifier(&id)); + } for volume in drive.volumes() { println!(" Volume: {}", volume.name()); + println!(" UUID: {:?}", volume.uuid()); + for id in volume.enumerate_identifiers() { + println!(" ID: {}={:?}", id, volume.identifier(&id)); + } if let Some(mount) = volume.get_mount() { println!(" Mount: {}", mount.name()); + println!(" UUID: {:?}", mount.uuid()); } } } for mount in monitor.mounts() { println!("Mount: {}", mount.name()); + println!(" UUID: {:?}", mount.uuid()); } for volume in monitor.volumes() { println!("Volume: {}", volume.name()); + println!(" UUID: {:?}", volume.uuid()); + for id in volume.enumerate_identifiers() { + println!(" ID: {}={:?}", id, volume.identifier(&id)); + } if let Some(mount) = volume.get_mount() { println!(" Mount: {}", mount.name()); + println!(" UUID: {:?}", mount.uuid()); } } } diff --git a/src/app.rs b/src/app.rs index 4f99010..c6960e3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,6 @@ use notify_debouncer_full::{ notify::{self, RecommendedWatcher, Watcher}, DebouncedEvent, Debouncer, FileIdMap, }; -use std::time::Instant; use std::{ any::TypeId, collections::{BTreeMap, HashMap, HashSet, VecDeque}, @@ -38,7 +37,7 @@ use std::{ path::PathBuf, process, sync::Arc, - time, + time::{self, Instant}, }; use crate::tab::HOVER_DURATION; @@ -48,7 +47,7 @@ use crate::{ fl, home_dir, key_bind::key_binds, menu, mime_app, - mounter::{mounters, Mounters}, + mounter::{mounters, MounterItem, MounterItems, MounterKey, Mounters}, operation::Operation, spawn_detached::spawn_detached, tab::{self, HeadingOptions, ItemMetadata, Location, Tab}, @@ -160,6 +159,7 @@ pub enum Message { LaunchUrl(String), Modifiers(Modifiers), MoveToTrash(Option), + MounterItems(MounterKey, MounterItems), NewItem(Option, bool), NotifyEvents(Vec), NotifyWatcher(WatcherWrapper), @@ -232,6 +232,8 @@ pub enum DialogPage { }, } +pub struct MounterData(MounterKey, MounterItem); + pub struct WatcherWrapper { watcher_opt: Option>, } @@ -270,6 +272,7 @@ pub struct App { key_binds: HashMap, modifiers: Modifiers, mounters: Mounters, + mounter_items: HashMap, pending_operation_id: u64, pending_operations: BTreeMap, complete_operations: BTreeMap, @@ -729,34 +732,6 @@ impl Application for App { .data(Location::Trash) }); - //TODO: dynamic mount list - let mounters = mounters(); - for (mounter_name, mounter) in mounters.iter() { - println!("Mounter {}", mounter_name); - match mounter.items() { - Ok(items) => { - for item in items { - let name = item.name(); - println!(" - {}", name); - let icon = item.icon(16); - let dir_opt = item.path(); - nav_model = nav_model.insert(move |mut b| { - b = b - .text(name.clone()) - .icon(widget::icon::icon(icon.clone()).size(16)); - match &dir_opt { - Some(dir) => b.data(Location::Path(dir.clone())), - None => b, - } - }); - } - } - Err(err) => { - log::warn!("failed to get items: {}", err); - } - } - } - let mut app = App { core, nav_model: nav_model.build(), @@ -771,7 +746,8 @@ impl Application for App { dialog_text_input: widget::Id::unique(), key_binds: key_binds(), modifiers: Modifiers::empty(), - mounters, + mounters: mounters(), + mounter_items: HashMap::new(), pending_operation_id: 0, pending_operations: BTreeMap::new(), complete_operations: BTreeMap::new(), @@ -816,13 +792,17 @@ impl Application for App { } fn on_nav_select(&mut self, entity: Entity) -> Command { - let location_opt = self.nav_model.data::(entity).clone(); - - if let Some(location) = location_opt { + if let Some(location) = self.nav_model.data::(entity) { let message = Message::TabMessage(None, tab::Message::Location(location.clone())); return self.update(message); } + if let Some(data) = self.nav_model.data::(entity).clone() { + if let Some(mounter) = self.mounters.get(&data.0) { + return mounter.mount(data.1.clone()).map(|_| message::none()); + } + } + Command::none() } @@ -979,6 +959,51 @@ impl Application for App { self.operation(Operation::Delete { paths }); } } + Message::MounterItems(mounter_key, mounter_items) => { + // Insert new items + self.mounter_items.insert(mounter_key, mounter_items); + + // Remove any items with mounter data from nav model + let entities: Vec = self.nav_model.iter().collect(); + for entity in entities { + if self.nav_model.data::(entity).is_some() { + self.nav_model.remove(entity); + } + } + + // Collect all mounter items + let mut nav_items = Vec::new(); + for (key, items) in self.mounter_items.iter() { + for item in items.iter() { + nav_items.push((*key, item)); + } + } + // Sort by name lexically + nav_items + .sort_by(|a, b| lexical_sort::natural_lexical_cmp(&a.1.name(), &b.1.name())); + // Add items to nav model + for (key, item) in nav_items { + let mut entity = self + .nav_model + .insert() + .text(format!( + "{} ({})", + item.name(), + if item.is_mounted() { + "mounted" + } else { + "not mounted" + } + )) + .data(MounterData(key, item.clone())); + if let Some(path) = item.path() { + entity = entity.data(Location::Path(path.clone())); + } + if let Some(icon) = item.icon() { + entity = entity.icon(widget::icon::icon(icon).size(16)); + } + } + } Message::NewItem(entity_opt, dir) => { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { @@ -1907,10 +1932,7 @@ impl Application for App { } } - //TODO: how to properly kill this task? - loop { - tokio::time::sleep(time::Duration::new(1, 0)).await; - } + std::future::pending().await }, ), subscription::channel( @@ -1981,6 +2003,15 @@ impl Application for App { ), ]; + for (key, mounter) in self.mounters.iter() { + let key = *key; + subscriptions.push( + mounter + .subscription() + .map(move |items| Message::MounterItems(key, items)), + ); + } + for (id, (pending_operation, _)) in self.pending_operations.iter() { //TODO: use recipe? let id = *id; @@ -2000,9 +2031,7 @@ impl Application for App { } } - loop { - tokio::time::sleep(time::Duration::new(1, 0)).await; - } + std::future::pending().await })); } diff --git a/src/mounter/gvfs.rs b/src/mounter/gvfs.rs index 423eb7d..ff9fad1 100644 --- a/src/mounter/gvfs.rs +++ b/src/mounter/gvfs.rs @@ -1,54 +1,233 @@ -use cosmic::widget; -use gio::prelude::*; -use gio::{Mount, ThemedIcon, Volume, VolumeMonitor}; -use std::{error::Error, path::PathBuf}; +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, MounterItem, MounterItems}; +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), +} + +enum Event { + Changed, + Items(MounterItems), +} + +#[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 { - monitor: VolumeMonitor, + command_tx: mpsc::UnboundedSender, + event_rx: Arc>>, } impl Gvfs { pub fn new() -> Self { - let monitor = VolumeMonitor::get(); - Self { monitor } + //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| { + eprintln!("mount changed {}", MountExt::name(mount)); + event_tx.send(Event::Changed).unwrap(); + }); + } + { + let event_tx = event_tx.clone(); + monitor.connect_mount_added(move |_monitor, mount| { + eprintln!("mount added {}", MountExt::name(mount)); + event_tx.send(Event::Changed).unwrap(); + }); + } + { + let event_tx = event_tx.clone(); + monitor.connect_mount_removed(move |_monitor, mount| { + eprintln!("mount removed {}", MountExt::name(mount)); + event_tx.send(Event::Changed).unwrap(); + }); + } + + { + let event_tx = event_tx.clone(); + monitor.connect_volume_changed(move |_monitor, volume| { + eprintln!("volume changed {}", VolumeExt::name(volume)); + event_tx.send(Event::Changed).unwrap(); + }); + } + { + let event_tx = event_tx.clone(); + monitor.connect_volume_added(move |_monitor, volume| { + eprintln!("volume added {}", VolumeExt::name(volume)); + event_tx.send(Event::Changed).unwrap(); + }); + } + { + let event_tx = event_tx.clone(); + monitor.connect_volume_removed(move |_monitor, volume| { + eprintln!("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) => { + #[allow(irrefutable_let_patterns)] + 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); + }, + ); + } + } + } + } + }); + main_loop.run() + }); + Self { + command_tx, + event_rx: Arc::new(Mutex::new(event_rx)), + } } } impl Mounter for Gvfs { - fn items(&self) -> Result> { - let mut items = MounterItems::new(); - for mount in self.monitor.mounts() { - items.push(Box::new(mount)); - } - Ok(items) - } -} - -impl MounterItem for Mount { - fn name(&self) -> String { - MountExt::name(self).to_string() + 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 icon(&self, size: u16) -> widget::icon::Handle { - let icon = MountExt::symbolic_icon(self); - 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 widget::icon::from_path(path); + 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(items).await.unwrap(), } } - } - - //TODO: handle more gio icon types - widget::icon::from_name("folder-symbolic") - .size(size) - .handle() - } - - fn path(&self) -> Option { - MountExt::root(self).path() + pending().await + }) } } diff --git a/src/mounter/mod.rs b/src/mounter/mod.rs index 3029783..3c4b106 100644 --- a/src/mounter/mod.rs +++ b/src/mounter/mod.rs @@ -1,23 +1,55 @@ -use cosmic::widget; -use std::error::Error; +use cosmic::{iced::subscription, widget, Command}; use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; #[cfg(feature = "gvfs")] mod gvfs; -pub trait MounterItem { - fn name(&self) -> String; - fn icon(&self, size: u16) -> widget::icon::Handle; - fn path(&self) -> Option; +#[derive(Clone, Debug)] +pub enum MounterItem { + #[cfg(feature = "gvfs")] + Gvfs(gvfs::Item), } -pub type MounterItems = Vec>; +impl MounterItem { + pub fn name(&self) -> String { + match self { + #[cfg(feature = "gvfs")] + Self::Gvfs(item) => item.name(), + } + } + + pub fn is_mounted(&self) -> bool { + match self { + #[cfg(feature = "gvfs")] + Self::Gvfs(item) => item.is_mounted(), + } + } + + pub fn icon(&self) -> Option { + match self { + #[cfg(feature = "gvfs")] + Self::Gvfs(item) => item.icon(), + } + } + + pub fn path(&self) -> Option { + match self { + #[cfg(feature = "gvfs")] + Self::Gvfs(item) => item.path(), + } + } +} + +pub type MounterItems = Vec; pub trait Mounter { - fn items(&self) -> Result>; + //TODO: send result + fn mount(&self, item: MounterItem) -> Command<()>; + fn subscription(&self) -> subscription::Subscription; } -pub type MounterKey = &'static str; +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct MounterKey(pub &'static str); pub type MounterMap = BTreeMap>; pub type Mounters = Arc; @@ -26,7 +58,7 @@ pub fn mounters() -> Mounters { #[cfg(feature = "gvfs")] { - mounters.insert("gvfs", Box::new(gvfs::Gvfs::new())); + mounters.insert(MounterKey("gvfs"), Box::new(gvfs::Gvfs::new())); } Mounters::new(mounters)