use super::wayland::{self, ClipboardItem}; use crate::channels::AsyncSenderExt; use crate::{arc_mut, lock, register_client, spawn}; use indexmap::IndexMap; use indexmap::map::Iter; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; use tracing::{debug, trace}; #[derive(Debug)] pub enum ClipboardEvent { Add(ClipboardItem), Remove(usize), Activate(usize), } type EventSender = mpsc::Sender; /// Clipboard client singleton, /// to ensure bars don't duplicate requests to the compositor. #[derive(Debug)] pub struct Client { wayland: Arc, senders: Arc>>, cache: Arc>, } impl Client { pub(crate) fn new(wl: Arc) -> Self { trace!("Initializing clipboard client"); let senders = arc_mut!(Vec::<(EventSender, usize)>::new()); let cache = arc_mut!(ClipboardCache::new()); { let senders = senders.clone(); let cache = cache.clone(); let wl = wl.clone(); spawn(async move { let item = wl.clipboard_item(); let mut rx = wl.subscribe_clipboard(); if let Some(item) = item { let senders = lock!(senders); let iter = senders.iter(); for (tx, _) in iter { tx.send_spawn(ClipboardEvent::Add(item.clone())); } lock!(cache).insert(item, senders.len()); } while let Ok(item) = rx.recv().await { debug!("Received clipboard item (ID: {})", item.id); let (existing_id, cache_size) = { let cache = lock!(cache); (cache.contains(&item), cache.len()) }; existing_id.map_or_else( || { { let mut cache = lock!(cache); let senders = lock!(senders); cache.insert(item.clone(), senders.len()); } let senders = lock!(senders); let iter = senders.iter(); for (tx, sender_cache_size) in iter { if cache_size == *sender_cache_size { let removed_id = lock!(cache) .remove_ref_first() .expect("Clipboard cache unexpectedly empty"); tx.send_spawn(ClipboardEvent::Remove(removed_id)); } tx.send_spawn(ClipboardEvent::Add(item.clone())); } }, |existing_id| { let senders = lock!(senders); let iter = senders.iter(); for (tx, _) in iter { tx.send_spawn(ClipboardEvent::Activate(existing_id)); } }, ); } }); } Self { wayland: wl, senders, cache, } } pub fn subscribe(&self, cache_size: usize) -> mpsc::Receiver { let (tx, rx) = mpsc::channel(16); { let cache = lock!(self.cache); let iter = cache.iter(); for (_, (item, _)) in iter { tx.send_spawn(ClipboardEvent::Add(item.clone())); } } lock!(self.senders).push((tx, cache_size)); rx } pub fn copy(&self, id: usize) { debug!("Copying item with id {id}"); let item = { let cache = lock!(self.cache); cache.get(id) }; if let Some(item) = item { self.wayland.copy_to_clipboard(item); } let senders = lock!(self.senders); let iter = senders.iter(); for (tx, _) in iter { tx.send_spawn(ClipboardEvent::Activate(id)); } } pub fn remove(&self, id: usize) { lock!(self.cache).remove(id); let senders = lock!(self.senders); let iter = senders.iter(); for (tx, _) in iter { tx.send_spawn(ClipboardEvent::Remove(id)); } } } /// Shared clipboard item cache. /// /// Items are stored with a number of references, /// allowing different consumers to 'remove' cached items /// at different times. #[derive(Debug)] struct ClipboardCache { cache: IndexMap, } impl ClipboardCache { /// Creates a new empty cache. fn new() -> Self { Self { cache: IndexMap::new(), } } /// Gets the entry with key `id` from the cache. fn get(&self, id: usize) -> Option { self.cache.get(&id).map(|(item, _)| item).cloned() } /// Inserts an entry with `ref_count` initial references. fn insert(&mut self, item: ClipboardItem, ref_count: usize) -> Option { self.cache .insert(item.id, (item, ref_count)) .map(|(item, _)| item) } /// Removes the entry with key `id`. /// This ignores references. fn remove(&mut self, id: usize) -> Option { self.cache.shift_remove(&id).map(|(item, _)| item) } /// Removes a reference to the entry with key `id`. /// /// If the reference count reaches zero, the entry /// is removed from the cache. fn remove_ref(&mut self, id: usize) { if let Some(entry) = self.cache.get_mut(&id) { entry.1 -= 1; if entry.1 == 0 { self.cache.shift_remove(&id); } } } /// Removes a reference to the first entry. /// /// If the reference count reaches zero, the entry /// is removed from the cache. fn remove_ref_first(&mut self) -> Option { if let Some((id, _)) = self.cache.first() { let id = *id; self.remove_ref(id); Some(id) } else { None } } /// Checks if an item with matching mime type and value /// already exists in the cache. fn contains(&self, item: &ClipboardItem) -> Option { self.cache.values().find_map(|(it, _)| { if it.mime_type == item.mime_type && it.value == item.value { Some(it.id) } else { None } }) } /// Gets the current number of items in the cache. fn len(&self) -> usize { self.cache.len() } fn iter(&self) -> Iter<'_, usize, (ClipboardItem, usize)> { self.cache.iter() } } register_client!(Client, clipboard);