Update smithay-clipboard to SCTK 0.9.1 (#16)
This commit entirely reworks the internal structure of the entire crate, as well as some of its APIs. This crate only accepts a C pointer to a Wayland display object, since the target audience of this crate are libraries without a wayland-client types. Also since seat information is not presented in such clients most of the time, the clipboard entirely relies on its seat tracking.
This commit is contained in:
parent
a4240ad835
commit
bb652c775b
14 changed files with 1040 additions and 1055 deletions
80
src/env.rs
Normal file
80
src/env.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use sctk::data_device::{DataDevice, DataDeviceHandler, DataDeviceHandling, DndEvent};
|
||||
use sctk::primary_selection::{
|
||||
PrimarySelectionDevice, PrimarySelectionDeviceManager, PrimarySelectionHandler,
|
||||
PrimarySelectionHandling,
|
||||
};
|
||||
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
||||
use sctk::reexports::client::{Attached, DispatchData};
|
||||
use sctk::seat::{SeatData, SeatHandler, SeatHandling, SeatListener};
|
||||
|
||||
/// Environemt setup for smithay-clipboard.
|
||||
pub struct SmithayClipboard {
|
||||
seats: SeatHandler,
|
||||
primary_selection_manager: PrimarySelectionHandler,
|
||||
data_device_manager: DataDeviceHandler,
|
||||
}
|
||||
|
||||
impl SmithayClipboard {
|
||||
/// Create new environment.
|
||||
pub fn new() -> Self {
|
||||
let mut seats = SeatHandler::new();
|
||||
let data_device_manager = DataDeviceHandler::init(&mut seats);
|
||||
let primary_selection_manager = PrimarySelectionHandler::init(&mut seats);
|
||||
Self {
|
||||
seats,
|
||||
primary_selection_manager,
|
||||
data_device_manager,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seat handling for data device manager and primary selection.
|
||||
impl SeatHandling for SmithayClipboard {
|
||||
fn listen<F: FnMut(Attached<WlSeat>, &SeatData, DispatchData) + 'static>(
|
||||
&mut self,
|
||||
f: F,
|
||||
) -> SeatListener {
|
||||
self.seats.listen(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimarySelectionHandling for SmithayClipboard {
|
||||
fn with_primary_selection<F: FnOnce(&PrimarySelectionDevice)>(
|
||||
&self,
|
||||
seat: &WlSeat,
|
||||
f: F,
|
||||
) -> Result<(), ()> {
|
||||
self.primary_selection_manager
|
||||
.with_primary_selection(seat, f)
|
||||
}
|
||||
|
||||
fn get_primary_selection_manager(&self) -> Option<PrimarySelectionDeviceManager> {
|
||||
self.primary_selection_manager
|
||||
.get_primary_selection_manager()
|
||||
}
|
||||
}
|
||||
|
||||
impl DataDeviceHandling for SmithayClipboard {
|
||||
fn set_callback<F>(&mut self, callback: F) -> Result<(), ()>
|
||||
where
|
||||
F: FnMut(WlSeat, DndEvent, DispatchData) + 'static,
|
||||
{
|
||||
self.data_device_manager.set_callback(callback)
|
||||
}
|
||||
|
||||
fn with_device<F: FnOnce(&DataDevice)>(&self, seat: &WlSeat, f: F) -> Result<(), ()> {
|
||||
self.data_device_manager.with_device(seat, f)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup globals.
|
||||
sctk::environment!(SmithayClipboard,
|
||||
singles = [
|
||||
sctk::reexports::protocols::unstable::primary_selection::v1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1 => primary_selection_manager,
|
||||
sctk::reexports::protocols::misc::gtk_primary_selection::client::gtk_primary_selection_device_manager::GtkPrimarySelectionDeviceManager => primary_selection_manager,
|
||||
sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager => data_device_manager,
|
||||
],
|
||||
multis = [
|
||||
WlSeat => seats,
|
||||
]
|
||||
);
|
||||
100
src/lib.rs
100
src/lib.rs
|
|
@ -1,18 +1,90 @@
|
|||
//! Smithay Clipboard
|
||||
//!
|
||||
//! Provides access to the wayland clipboard with only requirement being a WlDisplay
|
||||
//! object
|
||||
//!
|
||||
//! ```norun
|
||||
//! let (display, _) =
|
||||
//! Display::connect_to_env().expect("Failed to connect to the wayland server.");
|
||||
//! let mut clipboard = smithay_clipboard::WaylandClipboard::new(&display);
|
||||
//! clipboard.store(None, "Test data");
|
||||
//! println!("{}", clipboard.load(None));
|
||||
//! ```
|
||||
//! Provides access to the Wayland clipboard for gui applications. The user should have surface
|
||||
//! around.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
use std::ffi::c_void;
|
||||
use std::io::Result;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
mod threaded;
|
||||
pub use crate::threaded::ThreadedClipboard;
|
||||
pub use crate::threaded::ThreadedClipboard as WaylandClipboard;
|
||||
use sctk::reexports::client::Display;
|
||||
|
||||
mod env;
|
||||
mod mime;
|
||||
mod worker;
|
||||
|
||||
/// Access to a Wayland clipboard.
|
||||
pub struct Clipboard {
|
||||
request_sender: Sender<worker::Command>,
|
||||
request_receiver: Receiver<Result<String>>,
|
||||
clipboard_thread: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// Creates new clipboard which will be running on its own thread with its own event queue to
|
||||
/// handle clipboard requests.
|
||||
pub fn new(display: *mut c_void) -> Self {
|
||||
let display = unsafe { Display::from_external_display(display as *mut _) };
|
||||
|
||||
// Create channel to send data to clipboard thread.
|
||||
let (request_sender, clipboard_request_receiver) = mpsc::channel();
|
||||
// Create channel to get data from the clipboard thread.
|
||||
let (clipboard_reply_sender, request_receiver) = mpsc::channel();
|
||||
|
||||
let name = String::from("smithay-clipboard");
|
||||
let clipboard_thread = worker::spawn(
|
||||
name,
|
||||
display,
|
||||
clipboard_request_receiver,
|
||||
clipboard_reply_sender,
|
||||
);
|
||||
|
||||
Self {
|
||||
request_receiver,
|
||||
request_sender,
|
||||
clipboard_thread,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load clipboard data.
|
||||
///
|
||||
/// Loads content from a clipboard on a last observed seat.
|
||||
pub fn load(&self) -> Result<String> {
|
||||
let _ = self.request_sender.send(worker::Command::Load);
|
||||
self.request_receiver.recv().unwrap()
|
||||
}
|
||||
|
||||
/// Store to a clipboard
|
||||
///
|
||||
/// Stores to a clipboard on a last observed seat.
|
||||
pub fn store<T: Into<String>>(&self, text: T) {
|
||||
let request = worker::Command::Store(text.into());
|
||||
let _ = self.request_sender.send(request);
|
||||
}
|
||||
|
||||
/// Load primary clipboard data.
|
||||
///
|
||||
/// Loads content from a primary clipboard on a last observed seat.
|
||||
pub fn load_primary(&self) -> Result<String> {
|
||||
let _ = self.request_sender.send(worker::Command::LoadPrimary);
|
||||
self.request_receiver.recv().unwrap()
|
||||
}
|
||||
|
||||
/// Store to a primary clipboard.
|
||||
///
|
||||
/// Stores to a primary clipboard on a last observed seat.
|
||||
pub fn store_primary<T: Into<String>>(&self, text: T) {
|
||||
let request = worker::Command::StorePrimary(text.into());
|
||||
let _ = self.request_sender.send(request);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Clipboard {
|
||||
fn drop(&mut self) {
|
||||
// Shutdown smithay-clipboard.
|
||||
self.request_sender.send(worker::Command::Exit).unwrap();
|
||||
if let Some(clipboard_thread) = self.clipboard_thread.take() {
|
||||
let _ = clipboard_thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
src/mime.rs
Normal file
49
src/mime.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/// List of allowed mimes.
|
||||
static ALLOWED_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
|
||||
|
||||
/// Mime type supported by clipboard.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub enum MimeType {
|
||||
/// text/plain;charset=utf-8 mime type.
|
||||
///
|
||||
/// The primary mime type used by most clients
|
||||
TextPlainUtf8 = 0,
|
||||
/// UTF8_STRING mime type.
|
||||
///
|
||||
/// Some X11 clients are using only this mime type, so we
|
||||
/// should have it as a fallback just in case.
|
||||
Utf8String = 1,
|
||||
}
|
||||
|
||||
impl MimeType {
|
||||
/// Find first allowed mime type among the `offered_mime_types`.
|
||||
///
|
||||
/// `find_allowed()` searches for mime type clipboard supports, if we have a match,
|
||||
/// returns `Some(MimeType)`, otherwise `None`.
|
||||
pub fn find_allowed(offered_mime_types: &[String]) -> Option<Self> {
|
||||
for offered_mime_type in offered_mime_types.iter() {
|
||||
if offered_mime_type == ALLOWED_MIME_TYPES[Self::TextPlainUtf8 as usize] {
|
||||
return Some(Self::TextPlainUtf8);
|
||||
} else if offered_mime_type == ALLOWED_MIME_TYPES[Self::Utf8String as usize] {
|
||||
return Some(Self::Utf8String);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for MimeType {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(ALLOWED_MIME_TYPES[*self as usize])
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize \r and \r\n into \n.
|
||||
///
|
||||
/// Gtk does this for text/plain;charset=utf-8, so following them here, otherwise there is
|
||||
/// a chance of getting extra new lines on load, since they're converting \r and \n into
|
||||
/// \r\n on every store.
|
||||
pub fn normilize_to_lf(text: String) -> String {
|
||||
text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
}
|
||||
829
src/threaded.rs
829
src/threaded.rs
|
|
@ -1,829 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use std::io::{Read, Result, Write};
|
||||
use std::ops::Deref;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use nix::fcntl::OFlag;
|
||||
use nix::unistd::{close, pipe2};
|
||||
|
||||
use sctk::data_device::{DataDevice, DataSource, DataSourceEvent};
|
||||
use sctk::keyboard::{map_keyboard_auto, Event as KbEvent};
|
||||
use sctk::reexports::client::protocol::{
|
||||
wl_data_device_manager,
|
||||
wl_display::WlDisplay,
|
||||
wl_pointer::Event as PtrEvent,
|
||||
wl_registry,
|
||||
wl_seat::{self, Capability},
|
||||
};
|
||||
|
||||
use sctk::reexports::client::{Display, EventQueue, GlobalEvent, GlobalManager, NewProxy};
|
||||
use sctk::reexports::protocols::misc::gtk_primary_selection::client::{
|
||||
gtk_primary_selection_device::Event as GtkPrimarySelectionDeviceEvent,
|
||||
gtk_primary_selection_device::GtkPrimarySelectionDevice,
|
||||
gtk_primary_selection_device_manager::GtkPrimarySelectionDeviceManager,
|
||||
gtk_primary_selection_offer::GtkPrimarySelectionOffer, gtk_primary_selection_source,
|
||||
};
|
||||
use sctk::reexports::protocols::unstable::primary_selection::v1::client::{
|
||||
zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1 as PrimarySelectionDeviceMgr,
|
||||
zwp_primary_selection_device_v1::{
|
||||
Event as ZwpPrimarySelectionDeviceEvent,
|
||||
ZwpPrimarySelectionDeviceV1 as PrimarySelectionDevice,
|
||||
},
|
||||
zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1 as PrimarySelectionOffer,
|
||||
zwp_primary_selection_source_v1,
|
||||
};
|
||||
use sctk::wayland_client::sys::client::wl_display;
|
||||
|
||||
/// Used to store registered seats and their last event serial
|
||||
type SeatMap = HashMap<
|
||||
String,
|
||||
(
|
||||
Arc<Mutex<Option<DataDevice>>>,
|
||||
u32,
|
||||
Arc<Mutex<Option<PrimarySelectionDevice>>>,
|
||||
Arc<Mutex<Option<PrimarySelectionOffer>>>,
|
||||
Arc<Mutex<Option<GtkPrimarySelectionDevice>>>,
|
||||
Arc<Mutex<Option<GtkPrimarySelectionOffer>>>,
|
||||
),
|
||||
>;
|
||||
|
||||
/// Object representing the Wayland clipboard
|
||||
pub struct ThreadedClipboard {
|
||||
request_send: mpsc::Sender<ThreadRequest>,
|
||||
load_recv: mpsc::Receiver<Result<String>>,
|
||||
}
|
||||
|
||||
// Kill thread when clipboard object is dropped
|
||||
impl Drop for ThreadedClipboard {
|
||||
fn drop(&mut self) {
|
||||
self.request_send.send(ThreadRequest::Kill).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadedClipboard {
|
||||
/// Creates a new wayland clipboard object
|
||||
///
|
||||
/// Spawns a new thread to dispatch messages to the wayland server every
|
||||
/// 50ms to ensure the server can read stored data
|
||||
pub fn new(display: &Display) -> Self {
|
||||
let (request_send, request_recv) = mpsc::channel();
|
||||
let (load_send, load_recv) = mpsc::channel();
|
||||
let display = display.clone();
|
||||
|
||||
// Spawn a thread to handle the clipboard as regular dispatching of the wayland thread is needed
|
||||
std::thread::spawn(move || {
|
||||
let mut event_queue = display.create_event_queue();
|
||||
let display = (*display)
|
||||
.as_ref()
|
||||
.make_wrapper(&event_queue.get_token())
|
||||
.unwrap();
|
||||
clipboard_thread(&display, &mut event_queue, request_recv, load_send);
|
||||
});
|
||||
|
||||
ThreadedClipboard {
|
||||
request_send,
|
||||
load_recv,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new wayland clipboard object from a mutable `wl_display` ptr
|
||||
///
|
||||
/// Spawns a new thread to dispatch messages to the wayland server every
|
||||
/// 50ms to ensure the server can read stored data
|
||||
pub unsafe fn new_from_external(display_ptr: *mut wl_display) -> Self {
|
||||
let (request_send, request_recv) = mpsc::channel();
|
||||
let (load_send, load_recv) = mpsc::channel();
|
||||
let display = display_ptr.as_mut().unwrap();
|
||||
|
||||
// Spawn a thread to handle the clipboard as regular dispatching of the wayland thread is needed
|
||||
std::thread::spawn(move || {
|
||||
let (display, mut event_queue) = Display::from_external_display(display);
|
||||
clipboard_thread(&display, &mut event_queue, request_recv, load_send);
|
||||
});
|
||||
|
||||
ThreadedClipboard {
|
||||
request_send,
|
||||
load_recv,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns text from the wayland clipboard
|
||||
///
|
||||
/// If provided with a seat name that seat must be in
|
||||
/// focus to work. Otherwise if no seat name is provided
|
||||
/// the name of the seat to last generate a key or pointer event
|
||||
/// is used
|
||||
pub fn load(&mut self, seat_name: Option<String>) -> Result<String> {
|
||||
self.request_send
|
||||
.send(ThreadRequest::Load(seat_name))
|
||||
.unwrap();
|
||||
self.load_recv.recv().unwrap()
|
||||
}
|
||||
|
||||
/// Stores text in the wayland clipboard
|
||||
///
|
||||
/// If provided with a seat name that seat must be in
|
||||
/// focus to work. Otherwise if no seat name is provided
|
||||
/// the name of the seat to last generate a key or pointer event
|
||||
/// is used
|
||||
pub fn store<T>(&mut self, seat_name: Option<String>, text: T)
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.request_send
|
||||
.send(ThreadRequest::Store(seat_name, text.into()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Returns text from the primary selection of the wayland clipboard
|
||||
///
|
||||
/// If provided with a seat name that seat must be in
|
||||
/// focus to work. Otherwise if no seat name is provided
|
||||
/// the name of the seat to last generate a key or pointer event
|
||||
/// is used
|
||||
pub fn load_primary(&mut self, seat_name: Option<String>) -> Result<String> {
|
||||
self.request_send
|
||||
.send(ThreadRequest::LoadPrimary(seat_name))
|
||||
.unwrap();
|
||||
self.load_recv.recv().unwrap()
|
||||
}
|
||||
|
||||
/// Stores text in the primary selection of the wayland clipboard
|
||||
///
|
||||
/// If provided with a seat name that seat must be in
|
||||
/// focus to work. Otherwise if no seat name is provided
|
||||
/// the name of the seat to last generate a key or pointer event
|
||||
/// is used
|
||||
pub fn store_primary(&mut self, seat_name: Option<String>, text: String) {
|
||||
self.request_send
|
||||
.send(ThreadRequest::StorePrimary(seat_name, text))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests sent to the clipboard thread
|
||||
enum ThreadRequest {
|
||||
/// Store text in a specific seats clipboard
|
||||
Store(Option<String>, String),
|
||||
/// Load text from a specific seats clipboard
|
||||
Load(Option<String>),
|
||||
/// Store text in a specific seats primary clipboard
|
||||
StorePrimary(Option<String>, String),
|
||||
/// Load text in a specific seats primary clipboard
|
||||
LoadPrimary(Option<String>),
|
||||
/// Kill the thread
|
||||
Kill,
|
||||
}
|
||||
|
||||
/// Handles the setup and running of the clipboard thread
|
||||
fn clipboard_thread(
|
||||
display: &WlDisplay,
|
||||
event_queue: &mut EventQueue,
|
||||
request_recv: mpsc::Receiver<ThreadRequest>,
|
||||
load_send: mpsc::Sender<Result<String>>,
|
||||
) {
|
||||
// Create a seat map to register seats
|
||||
let seat_map = Arc::new(Mutex::new(SeatMap::new()));
|
||||
|
||||
// Store unimplemented seats so we can implement them when the data device manager is implemented
|
||||
let data_device_manager = Arc::new(Mutex::new(None));
|
||||
let mut unimplemented_seats = Vec::new();
|
||||
|
||||
let primary_selection_device_manager = Arc::new(Mutex::new(None));
|
||||
let gtk_primary_selection_device_manager = Arc::new(Mutex::new(None));
|
||||
|
||||
// Store the name of the seat that last sends an event for use as the default seat
|
||||
let last_seat_name = Arc::new(Mutex::new(String::new()));
|
||||
|
||||
let data_device_manager_clone = data_device_manager.clone();
|
||||
let primary_selection_device_manager_clone = primary_selection_device_manager.clone();
|
||||
let gtk_primary_selection_device_manager_clone = gtk_primary_selection_device_manager.clone();
|
||||
let seat_map_clone = seat_map.clone();
|
||||
let last_seat_name_clone = last_seat_name.clone();
|
||||
|
||||
// Register wl_seat objects and wl_data_device_manager
|
||||
GlobalManager::new_with_cb(&display, move |event, reg| {
|
||||
if let GlobalEvent::New {
|
||||
id,
|
||||
ref interface,
|
||||
version,
|
||||
} = event
|
||||
{
|
||||
if "wl_seat" == interface.as_str() && version >= 2 {
|
||||
if let Some(ref data_device_manager) =
|
||||
data_device_manager_clone.lock().unwrap().deref()
|
||||
{
|
||||
// Implement the seat
|
||||
implement_seat(
|
||||
id,
|
||||
std::cmp::min(version, 6), // only seat up to version 6 is supported
|
||||
seat_map_clone.clone(),
|
||||
last_seat_name_clone.clone(),
|
||||
data_device_manager,
|
||||
®,
|
||||
primary_selection_device_manager_clone.clone(),
|
||||
gtk_primary_selection_device_manager_clone.clone(),
|
||||
);
|
||||
} else {
|
||||
// Store the seat for implementation once wl_data_device_manager is registered
|
||||
unimplemented_seats.push((id, version));
|
||||
}
|
||||
} else if "wl_data_device_manager" == interface.as_str() {
|
||||
// Register the wl_data_device_manager
|
||||
*data_device_manager_clone.lock().unwrap() = Some(
|
||||
reg.bind::<wl_data_device_manager::WlDataDeviceManager, _>(
|
||||
version,
|
||||
id,
|
||||
NewProxy::implement_dummy,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
// Implement the unimplemented seats
|
||||
for (id, version) in &unimplemented_seats {
|
||||
implement_seat(
|
||||
*id,
|
||||
std::cmp::min(*version, 6), // only seat up to version 6 is supported
|
||||
seat_map_clone.clone(),
|
||||
last_seat_name_clone.clone(),
|
||||
data_device_manager_clone.lock().unwrap().as_ref().unwrap(),
|
||||
®,
|
||||
primary_selection_device_manager_clone.clone(),
|
||||
gtk_primary_selection_device_manager_clone.clone(),
|
||||
);
|
||||
}
|
||||
} else if "zwp_primary_selection_device_manager_v1" == interface.as_str() {
|
||||
// Register the zwp_primary_selection_device_manager
|
||||
*primary_selection_device_manager_clone.lock().unwrap() = Some(
|
||||
reg.bind::<PrimarySelectionDeviceMgr, _>(
|
||||
version,
|
||||
id,
|
||||
NewProxy::implement_dummy,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
} else if "gtk_primary_selection_device_manager" == interface.as_str() {
|
||||
*gtk_primary_selection_device_manager_clone.lock().unwrap() = Some(
|
||||
reg.bind::<GtkPrimarySelectionDeviceManager, _>(
|
||||
version,
|
||||
id,
|
||||
NewProxy::implement_dummy,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
|
||||
// We should provide lower sleep amounts in a moments of spaming our clipboard
|
||||
let mut sleep_amount = 50;
|
||||
// Provide our clipboard a warm start, so 16 initial cycles will be at 1ms and other will go
|
||||
// like 1 2 4 8 16 32 50 50 and so on
|
||||
let mut warm_start_amount = 0;
|
||||
|
||||
// Thread loop to handle requests and dispatch the event queue
|
||||
loop {
|
||||
if let Ok(request) = request_recv.try_recv() {
|
||||
// Lower sleep amount to zero, so the next recv will be instant
|
||||
sleep_amount = 0;
|
||||
|
||||
match request {
|
||||
// Load text from clipboard
|
||||
ThreadRequest::Load(seat_name) => {
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
let seat_map = seat_map.lock().unwrap().clone();
|
||||
|
||||
// Get the clipboard contents of the requested seat from the seat map
|
||||
let contents = seat_map
|
||||
.get(&seat_name.unwrap_or_else(|| last_seat_name.lock().unwrap().clone()))
|
||||
.map_or(Ok(String::new()), |seat| {
|
||||
let mut reader = None;
|
||||
if let Some(device) = seat.0.lock().unwrap().as_ref() {
|
||||
device.with_selection(|offer| {
|
||||
if let Some(offer) = offer {
|
||||
offer.with_mime_types(|types| {
|
||||
if types
|
||||
.contains(&"text/plain;charset=utf-8".to_string())
|
||||
{
|
||||
reader = Some(
|
||||
offer
|
||||
.receive("text/plain;charset=utf-8".into())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
}
|
||||
reader.map_or(Ok(String::new()), |mut reader| {
|
||||
let mut contents = String::new();
|
||||
if let Err(err) = reader.read_to_string(&mut contents) {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(contents)
|
||||
}
|
||||
})
|
||||
});
|
||||
// Normalization should happen only on `text/plain;charset=utf-8`, in case we
|
||||
// add other mime types consult gtk for normalization.
|
||||
let contents = contents.and_then(|contents| Ok(normilize_to_lf(contents)));
|
||||
load_send.send(contents).unwrap();
|
||||
}
|
||||
// Store text in the clipboard
|
||||
ThreadRequest::Store(seat_name, contents) => {
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
let seat_map = seat_map.lock().unwrap().clone();
|
||||
|
||||
// Get the requested seat from the seat map
|
||||
if let Some((device, enter_serial, _, _, _, _)) = seat_map
|
||||
.get(&seat_name.unwrap_or_else(|| last_seat_name.lock().unwrap().clone()))
|
||||
{
|
||||
if let Some(device) = device.lock().unwrap().as_ref() {
|
||||
let data_source = DataSource::new(
|
||||
data_device_manager.lock().unwrap().as_ref().unwrap(),
|
||||
&["text/plain;charset=utf-8"],
|
||||
move |source_event| {
|
||||
if let DataSourceEvent::Send { mut pipe, .. } = source_event {
|
||||
write!(pipe, "{}", contents).unwrap();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
device.set_selection(&Some(data_source), *enter_serial);
|
||||
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load text from primary clipboard
|
||||
ThreadRequest::LoadPrimary(seat_name) => {
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
let seat_map = seat_map.lock().unwrap().clone();
|
||||
|
||||
// Get the primary clipboard contents of the requested seat from the seat map
|
||||
let contents = if primary_selection_device_manager.lock().unwrap().is_some() {
|
||||
seat_map
|
||||
.get(
|
||||
&seat_name
|
||||
.unwrap_or_else(|| last_seat_name.lock().unwrap().clone()),
|
||||
)
|
||||
.map_or(Ok(String::new()), |seat| {
|
||||
seat.3.lock().unwrap().as_ref().map_or(
|
||||
Ok(String::new()),
|
||||
|primary_offer| {
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC).unwrap();
|
||||
let mut file =
|
||||
unsafe { std::fs::File::from_raw_fd(readfd) };
|
||||
primary_offer.receive(
|
||||
"text/plain;charset=utf-8".to_string(),
|
||||
writefd,
|
||||
);
|
||||
close(writefd).unwrap();
|
||||
let mut contents = String::new();
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
if let Err(err) = file.read_to_string(&mut contents) {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(contents)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
} else if gtk_primary_selection_device_manager
|
||||
.lock()
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
seat_map
|
||||
.get(
|
||||
&seat_name
|
||||
.unwrap_or_else(|| last_seat_name.lock().unwrap().clone()),
|
||||
)
|
||||
.map_or(Ok(String::new()), |seat| {
|
||||
seat.5.lock().unwrap().as_ref().map_or(
|
||||
Ok(String::new()),
|
||||
|primary_offer| {
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC).unwrap();
|
||||
let mut file =
|
||||
unsafe { std::fs::File::from_raw_fd(readfd) };
|
||||
primary_offer.receive(
|
||||
"text/plain;charset=utf-8".to_string(),
|
||||
writefd,
|
||||
);
|
||||
close(writefd).unwrap();
|
||||
let mut contents = String::new();
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
if let Err(err) = file.read_to_string(&mut contents) {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(contents)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Ok(String::new())
|
||||
};
|
||||
// Normalization should happen only on `text/plain;charset=utf-8`, in case we
|
||||
// add other mime types consult gtk for normalization.
|
||||
let contents = contents.and_then(|contents| Ok(normilize_to_lf(contents)));
|
||||
load_send.send(contents).unwrap();
|
||||
}
|
||||
// Store text in the primary clipboard
|
||||
ThreadRequest::StorePrimary(seat_name, contents) => {
|
||||
event_queue.sync_roundtrip().unwrap();
|
||||
let seat_map = seat_map.lock().unwrap().clone();
|
||||
|
||||
// Get the requested seat from the seat map
|
||||
if let Some((_, enter_serial, primary_device, _, gtk_primary_device, _)) =
|
||||
seat_map.get(
|
||||
&seat_name.unwrap_or_else(|| last_seat_name.lock().unwrap().clone()),
|
||||
)
|
||||
{
|
||||
if let Some(manager) = &*primary_selection_device_manager.lock().unwrap() {
|
||||
if let Some(primary_device) = &*primary_device.lock().unwrap() {
|
||||
let source = manager.create_source(|proxy| {
|
||||
proxy.implement_closure(
|
||||
move |event, _| {
|
||||
if let zwp_primary_selection_source_v1::Event::Send {
|
||||
mime_type,
|
||||
fd,
|
||||
} = event
|
||||
{
|
||||
if mime_type == "text/plain;charset=utf-8" {
|
||||
let mut file =
|
||||
unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
file.write_fmt(format_args!("{}", contents))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
},
|
||||
(),
|
||||
)
|
||||
});
|
||||
if let Ok(source) = &source {
|
||||
source.offer("text/plain;charset=utf-8".to_string());
|
||||
}
|
||||
primary_device.set_selection(source.ok().as_ref(), *enter_serial);
|
||||
}
|
||||
} else if let Some(manager) =
|
||||
&*gtk_primary_selection_device_manager.lock().unwrap()
|
||||
{
|
||||
if let Some(gtk_primary_device) = &*gtk_primary_device.lock().unwrap() {
|
||||
let source = manager.create_source(|proxy| {
|
||||
proxy.implement_closure(
|
||||
move |event, _| {
|
||||
if let gtk_primary_selection_source::Event::Send {
|
||||
mime_type,
|
||||
fd,
|
||||
} = event
|
||||
{
|
||||
if mime_type == "text/plain;charset=utf-8" {
|
||||
let mut file =
|
||||
unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
file.write_fmt(format_args!("{}", contents))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
},
|
||||
(),
|
||||
)
|
||||
});
|
||||
if let Ok(source) = &source {
|
||||
source.offer("text/plain;charset=utf-8".to_string());
|
||||
}
|
||||
gtk_primary_device
|
||||
.set_selection(source.ok().as_ref(), *enter_serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadRequest::Kill => break,
|
||||
}
|
||||
}
|
||||
// Dispatch the event queue and block for `sleep_amount`
|
||||
let pending_events = event_queue.dispatch_pending().unwrap();
|
||||
let num_seats = seat_map.lock().unwrap().len();
|
||||
|
||||
// If some app is trying to spam us when there no seats, it's likely that someone is
|
||||
// trying to paste from us
|
||||
if num_seats == 0 && pending_events != 0 {
|
||||
sleep_amount = 0;
|
||||
} else if sleep_amount > 0 {
|
||||
thread::sleep(Duration::from_millis(sleep_amount));
|
||||
|
||||
if warm_start_amount < 16 {
|
||||
warm_start_amount += 1;
|
||||
if warm_start_amount == 16 {
|
||||
sleep_amount = 1;
|
||||
}
|
||||
} else if sleep_amount < 50 {
|
||||
// The aim of this different sleep times is to provide a good performance under
|
||||
// high load and not waste system resources too much when idle
|
||||
sleep_amount = std::cmp::min(2 * sleep_amount, 50);
|
||||
}
|
||||
} else if sleep_amount == 0 {
|
||||
// Reset sleep amount from zero back to one, so sleep sequence could reach 50
|
||||
sleep_amount = 1;
|
||||
// Reset warm start to accelerate the initial clipboard requests
|
||||
warm_start_amount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement seats that we register
|
||||
fn implement_seat(
|
||||
id: u32,
|
||||
version: u32,
|
||||
seat_map: Arc<Mutex<SeatMap>>,
|
||||
last_seat_name: Arc<Mutex<String>>,
|
||||
data_device_manager: &wl_data_device_manager::WlDataDeviceManager,
|
||||
reg: &wl_registry::WlRegistry,
|
||||
primary_device_manager: Arc<Mutex<Option<PrimarySelectionDeviceMgr>>>,
|
||||
gtk_primary_device_manager: Arc<Mutex<Option<GtkPrimarySelectionDeviceManager>>>,
|
||||
) {
|
||||
let device = Arc::new(Mutex::new(None));
|
||||
let device_clone = device.clone();
|
||||
let seat_name = Arc::new(Mutex::new(String::new()));
|
||||
let seat_name_clone = seat_name.clone();
|
||||
let seat_map_clone = seat_map.clone();
|
||||
|
||||
let primary_device = Arc::new(Mutex::new(None));
|
||||
let primary_offer = Arc::new(Mutex::new(None));
|
||||
|
||||
let primary_device_clone = primary_device.clone();
|
||||
let primary_offer_clone = primary_offer.clone();
|
||||
|
||||
let gtk_primary_device = Arc::new(Mutex::new(None));
|
||||
let gtk_primary_offer = Arc::new(Mutex::new(None));
|
||||
|
||||
let gtk_primary_device_clone = gtk_primary_device.clone();
|
||||
let gtk_primary_offer_clone = gtk_primary_offer.clone();
|
||||
|
||||
let mut pointer = None;
|
||||
let mut keyboard = None;
|
||||
|
||||
// Register the seat
|
||||
let seat = reg
|
||||
.bind::<wl_seat::WlSeat, _>(version, id, move |proxy| {
|
||||
proxy.implement_closure(
|
||||
move |event, seat| match event {
|
||||
wl_seat::Event::Name { name } => *seat_name_clone.lock().unwrap() = name,
|
||||
wl_seat::Event::Capabilities { capabilities } => {
|
||||
if capabilities.contains(Capability::Pointer) {
|
||||
if pointer.is_none() {
|
||||
let device_clone = device_clone.clone();
|
||||
|
||||
let primary_device_clone = primary_device_clone.clone();
|
||||
let primary_offer_clone = primary_offer_clone.clone();
|
||||
|
||||
let gtk_primary_device_clone = gtk_primary_device_clone.clone();
|
||||
let gtk_primary_offer_clone = gtk_primary_offer_clone.clone();
|
||||
|
||||
let last_seat_name_clone = last_seat_name.clone();
|
||||
let seat_map_clone = seat_map_clone.clone();
|
||||
let seat_name_clone = seat_name_clone.clone();
|
||||
pointer = Some(
|
||||
seat.get_pointer(move |pointer| {
|
||||
pointer.implement_closure(
|
||||
move |evt, _| {
|
||||
// Set this seat as the last to send an event
|
||||
*last_seat_name_clone.lock().unwrap() =
|
||||
seat_name_clone.lock().unwrap().clone();
|
||||
|
||||
// Get serials from recieved events from the seat
|
||||
// pointer
|
||||
match evt {
|
||||
PtrEvent::Enter { serial, .. } => {
|
||||
if let Some(seat) =
|
||||
seat_map_clone.lock().unwrap().get_mut(
|
||||
&seat_name_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
)
|
||||
{
|
||||
// Update serial if "seat" is already
|
||||
// presented
|
||||
seat.1 = serial;
|
||||
return;
|
||||
}
|
||||
|
||||
seat_map_clone.lock().unwrap().insert(
|
||||
seat_name_clone.lock().unwrap().clone(),
|
||||
(
|
||||
device_clone.clone(),
|
||||
serial,
|
||||
primary_device_clone.clone(),
|
||||
primary_offer_clone.clone(),
|
||||
gtk_primary_device_clone.clone(),
|
||||
gtk_primary_offer_clone.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
PtrEvent::Button { serial, .. } => {
|
||||
if let Some(seat) =
|
||||
seat_map_clone.lock().unwrap().get_mut(
|
||||
&seat_name_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
)
|
||||
{
|
||||
// Update serial if seat is already
|
||||
// presented
|
||||
seat.1 = serial;
|
||||
return;
|
||||
}
|
||||
|
||||
// This is for consistency with
|
||||
// `PtrEvent::Enter`
|
||||
seat_map_clone.lock().unwrap().insert(
|
||||
seat_name_clone.lock().unwrap().clone(),
|
||||
(
|
||||
device_clone.clone(),
|
||||
serial,
|
||||
primary_device_clone.clone(),
|
||||
primary_offer_clone.clone(),
|
||||
gtk_primary_device_clone.clone(),
|
||||
gtk_primary_offer_clone.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
(),
|
||||
)
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
} else if let Some(pointer) = pointer.take() {
|
||||
// Release old pointer
|
||||
if pointer.as_ref().version() >= 3 {
|
||||
pointer.release();
|
||||
}
|
||||
}
|
||||
|
||||
if capabilities.contains(Capability::Keyboard) {
|
||||
if keyboard.is_none() {
|
||||
let device_clone = device_clone.clone();
|
||||
|
||||
let primary_device_clone = primary_device_clone.clone();
|
||||
let primary_offer_clone = primary_offer_clone.clone();
|
||||
|
||||
let gtk_primary_device_clone = gtk_primary_device_clone.clone();
|
||||
let gtk_primary_offer_clone = gtk_primary_offer_clone.clone();
|
||||
|
||||
let last_seat_name_clone = last_seat_name.clone();
|
||||
let seat_map_clone = seat_map_clone.clone();
|
||||
let seat_name_clone = seat_name_clone.clone();
|
||||
keyboard = Some(
|
||||
map_keyboard_auto(&seat, move |event, _| {
|
||||
// Set this seat as the last to send an event
|
||||
*last_seat_name_clone.lock().unwrap() =
|
||||
seat_name_clone.lock().unwrap().clone();
|
||||
|
||||
// Get serials from recieved events from the seat keyboard
|
||||
match event {
|
||||
KbEvent::Enter { serial, .. } => {
|
||||
seat_map_clone.lock().unwrap().insert(
|
||||
seat_name_clone.lock().unwrap().clone(),
|
||||
(
|
||||
device_clone.clone(),
|
||||
serial,
|
||||
primary_device_clone.clone(),
|
||||
primary_offer_clone.clone(),
|
||||
gtk_primary_device_clone.clone(),
|
||||
gtk_primary_offer_clone.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
KbEvent::Key { serial, .. } => {
|
||||
seat_map_clone.lock().unwrap().insert(
|
||||
seat_name_clone.lock().unwrap().clone(),
|
||||
(
|
||||
device_clone.clone(),
|
||||
serial,
|
||||
primary_device_clone.clone(),
|
||||
primary_offer_clone.clone(),
|
||||
gtk_primary_device_clone.clone(),
|
||||
gtk_primary_offer_clone.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
KbEvent::Leave { .. } => {
|
||||
seat_map_clone
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&*seat_name_clone.lock().unwrap());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
} else if let Some(keyboard) = keyboard.take() {
|
||||
// Release old keyboard
|
||||
if keyboard.as_ref().version() >= 3 {
|
||||
keyboard.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Create a device for the seat
|
||||
*device.lock().unwrap() = Some(DataDevice::init_for_seat(
|
||||
data_device_manager,
|
||||
&seat,
|
||||
|_| {},
|
||||
));
|
||||
|
||||
if let Some(manager) = &*primary_device_manager.lock().unwrap() {
|
||||
*primary_device.lock().unwrap() = manager
|
||||
.get_device(&seat, |proxy| {
|
||||
proxy.implement_closure(
|
||||
move |event, _| {
|
||||
if let ZwpPrimarySelectionDeviceEvent::DataOffer { offer } = event {
|
||||
*primary_offer.lock().unwrap() = Some(offer.implement_dummy());
|
||||
|
||||
let map_contents = seat_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&seat_name.lock().unwrap().clone())
|
||||
.cloned();
|
||||
if let Some(map_contents) = map_contents {
|
||||
seat_map.lock().unwrap().insert(
|
||||
seat_name.lock().unwrap().clone(),
|
||||
(
|
||||
map_contents.0.clone(),
|
||||
map_contents.1,
|
||||
map_contents.2,
|
||||
primary_offer.clone(),
|
||||
Arc::new(Mutex::new(None)),
|
||||
Arc::new(Mutex::new(None)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
(),
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
} else if let Some(manager) = &*gtk_primary_device_manager.lock().unwrap() {
|
||||
*gtk_primary_device.lock().unwrap() = manager
|
||||
.get_device(&seat, |proxy| {
|
||||
proxy.implement_closure(
|
||||
move |event, _| {
|
||||
if let GtkPrimarySelectionDeviceEvent::DataOffer { offer } = event {
|
||||
*gtk_primary_offer.lock().unwrap() = Some(offer.implement_dummy());
|
||||
|
||||
let map_contents = seat_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&seat_name.lock().unwrap().clone())
|
||||
.cloned();
|
||||
if let Some(map_contents) = map_contents {
|
||||
seat_map.lock().unwrap().insert(
|
||||
seat_name.lock().unwrap().clone(),
|
||||
(
|
||||
map_contents.0.clone(),
|
||||
map_contents.1,
|
||||
Arc::new(Mutex::new(None)),
|
||||
Arc::new(Mutex::new(None)),
|
||||
map_contents.4,
|
||||
gtk_primary_offer.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
(),
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize \r and \r\n into \n.
|
||||
//
|
||||
// Gtk does this for text/plain;charset=utf-8, so following them here, otherwise there is
|
||||
// a chance of getting extra new lines on load, since they're converting \r and \n into
|
||||
// \r\n on every store.
|
||||
fn normilize_to_lf(text: String) -> String {
|
||||
text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
}
|
||||
47
src/worker/dispatch_data.rs
Normal file
47
src/worker/dispatch_data.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
||||
|
||||
/// Data to track latest seat and serial for clipboard requests.
|
||||
#[derive(Default)]
|
||||
pub struct ClipboardDispatchData {
|
||||
observed_seats: Vec<(WlSeat, u32)>,
|
||||
last_pos: usize,
|
||||
}
|
||||
|
||||
impl ClipboardDispatchData {
|
||||
/// Builds new `ClipboardDispatchData` with all fields equal to `None`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the last observed seat.
|
||||
pub fn set_last_seat(&mut self, seat: WlSeat, serial: u32) {
|
||||
let pos = self.observed_seats.iter().position(|st| st.0 == seat);
|
||||
match pos {
|
||||
Some(pos) => {
|
||||
// Update serial and set the last data we've seen.
|
||||
self.observed_seats[pos].1 = serial;
|
||||
self.last_pos = pos;
|
||||
}
|
||||
None => {
|
||||
// Add new seat and mark it as last.
|
||||
self.last_pos = self.observed_seats.len();
|
||||
self.observed_seats.push((seat, serial));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the given seat from the observer seats.
|
||||
pub fn remove_seat(&mut self, seat: WlSeat) {
|
||||
let pos = self.observed_seats.iter().position(|st| st.0 == seat);
|
||||
|
||||
if let Some(pos) = pos {
|
||||
// Remove the seat data.
|
||||
self.observed_seats.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the last observed seat and the serial.
|
||||
pub fn last_seat(&self) -> Option<&(WlSeat, u32)> {
|
||||
self.observed_seats.get(self.last_pos)
|
||||
}
|
||||
}
|
||||
131
src/worker/handlers.rs
Normal file
131
src/worker/handlers.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#![macro_use]
|
||||
|
||||
use std::io::Result;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use sctk::reexports::client::protocol::wl_keyboard::Event as KeyboardEvent;
|
||||
use sctk::reexports::client::protocol::wl_pointer::Event as PointerEvent;
|
||||
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
||||
use sctk::reexports::client::DispatchData;
|
||||
|
||||
use super::dispatch_data::ClipboardDispatchData;
|
||||
|
||||
/// Macro to handle load for selection and primary clipboards.
|
||||
macro_rules! handle_load {
|
||||
($env:ident, $sel_ty:ident, $seat:ident, $queue:ident, $tx:ident ) => {
|
||||
let result = $env.$sel_ty(&$seat, |device| {
|
||||
let (mut reader, mime_type) = match device.with_selection(|offer| {
|
||||
// Check that we have an offer.
|
||||
let offer = match offer {
|
||||
Some(offer) => offer,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// Check that we can work with advertised mime type and pick the one
|
||||
// that suits us more.
|
||||
let mime_type = match offer.with_mime_types(MimeType::find_allowed) {
|
||||
Some(mime_type) => mime_type,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// Request given the mime type.
|
||||
let reader = offer.receive(mime_type.to_string()).ok()?;
|
||||
Some((reader, mime_type))
|
||||
}) {
|
||||
Some((reader, mime_type)) => (reader, mime_type),
|
||||
None => {
|
||||
handlers::reply_error(&$tx, "offer receive failed.");
|
||||
return ();
|
||||
}
|
||||
};
|
||||
|
||||
$queue
|
||||
.sync_roundtrip(&mut (), |_, _, _| unreachable!())
|
||||
.unwrap();
|
||||
|
||||
let mut contents = String::new();
|
||||
let result = reader.read_to_string(&mut contents).map(|_| {
|
||||
if mime_type == MimeType::Utf8String {
|
||||
mime::normilize_to_lf(contents)
|
||||
} else {
|
||||
contents
|
||||
}
|
||||
});
|
||||
|
||||
$tx.send(result).unwrap();
|
||||
});
|
||||
|
||||
// Send back that we've failed to load data from the clipboard.
|
||||
if result.is_err() {
|
||||
handlers::reply_error(&$tx, "failed to access clipboard.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro to handle store for selection and primary clipboards.
|
||||
macro_rules! handle_store {
|
||||
($env:ident,
|
||||
$sel_source:ident, $sel_device:ident, $event_ty:ident,
|
||||
$seat:ident, $serial:ident, $queue:ident, $contents:ident) => {
|
||||
let data_source = $env.$sel_source(
|
||||
vec![MimeType::TextPlainUtf8.to_string()],
|
||||
move |event, _| {
|
||||
if let $event_ty::Send { mut pipe, .. } = event {
|
||||
write!(pipe, "{}", $contents).unwrap();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let _ = $env.$sel_device(&$seat, |device| {
|
||||
device.set_selection(&Some(data_source), $serial);
|
||||
|
||||
let _ = $queue.sync_roundtrip(&mut (), |_, _, _| unreachable!());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// Reply an error to a clipboard master.
|
||||
pub fn reply_error(tx: &Sender<Result<String>>, description: &str) {
|
||||
tx.send(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
description,
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Update seat and serial on pointer events.
|
||||
pub fn pointer_handler(seat: WlSeat, event: PointerEvent, mut dispatch_data: DispatchData) {
|
||||
let dispatch_data = match dispatch_data.get::<ClipboardDispatchData>() {
|
||||
Some(dispatch_data) => dispatch_data,
|
||||
None => return,
|
||||
};
|
||||
match event {
|
||||
PointerEvent::Enter { serial, .. } => {
|
||||
dispatch_data.set_last_seat(seat, serial);
|
||||
}
|
||||
PointerEvent::Button { serial, .. } => {
|
||||
dispatch_data.set_last_seat(seat, serial);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update seat and serial on keyboard events.
|
||||
pub fn keyboard_handler(seat: WlSeat, event: KeyboardEvent, mut dispatch_data: DispatchData) {
|
||||
let dispatch_data = match dispatch_data.get::<ClipboardDispatchData>() {
|
||||
Some(dispatch_data) => dispatch_data,
|
||||
None => return,
|
||||
};
|
||||
match event {
|
||||
KeyboardEvent::Enter { serial, .. } => {
|
||||
dispatch_data.set_last_seat(seat, serial);
|
||||
}
|
||||
KeyboardEvent::Key { serial, .. } => {
|
||||
dispatch_data.set_last_seat(seat, serial);
|
||||
}
|
||||
KeyboardEvent::Leave { .. } => {
|
||||
dispatch_data.remove_seat(seat);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
288
src/worker/mod.rs
Normal file
288
src/worker/mod.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
use std::io::prelude::*;
|
||||
use std::io::Result;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::time::Duration;
|
||||
|
||||
use sctk::reexports::client::protocol::wl_data_device_manager::WlDataDeviceManager;
|
||||
use sctk::reexports::client::Display;
|
||||
|
||||
use sctk::data_device::DataSourceEvent;
|
||||
use sctk::primary_selection::PrimarySelectionSourceEvent;
|
||||
|
||||
use sctk::environment::Environment;
|
||||
use sctk::seat;
|
||||
|
||||
use crate::env::SmithayClipboard;
|
||||
use crate::mime::{self, MimeType};
|
||||
|
||||
mod dispatch_data;
|
||||
mod handlers;
|
||||
mod seat_data;
|
||||
mod sleep_amount_tracker;
|
||||
|
||||
use dispatch_data::ClipboardDispatchData;
|
||||
use seat_data::SeatData;
|
||||
use sleep_amount_tracker::SleepAmountTracker;
|
||||
|
||||
/// Max time clipboard thread can sleep.
|
||||
const MAX_TIME_TO_SLEEP: u8 = 50;
|
||||
|
||||
/// Max warm wakeups.
|
||||
const MAX_WARM_WAKEUPS: u8 = 16;
|
||||
|
||||
/// Spawn a clipboard worker, which dispatches it's own `EventQueue` each 50ms and handles
|
||||
/// clipboard requests.
|
||||
pub fn spawn(
|
||||
name: String,
|
||||
display: Display,
|
||||
worker_receiver: Receiver<Command>,
|
||||
worker_replier: Sender<Result<String>>,
|
||||
) -> Option<std::thread::JoinHandle<()>> {
|
||||
std::thread::Builder::new()
|
||||
.name(name)
|
||||
.spawn(move || {
|
||||
worker_impl(display, worker_receiver, worker_replier);
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Clipboard worker thread command.
|
||||
#[derive(Eq, PartialEq)]
|
||||
pub enum Command {
|
||||
/// Store data to a clipboard.
|
||||
Store(String),
|
||||
/// Store data to a primary selection.
|
||||
StorePrimary(String),
|
||||
/// Load data from a clipboard.
|
||||
Load,
|
||||
/// Load primary selection.
|
||||
LoadPrimary,
|
||||
/// Shutdown the worker.
|
||||
Exit,
|
||||
}
|
||||
|
||||
/// Handle clipboard requests.
|
||||
fn worker_impl(display: Display, request_rx: Receiver<Command>, reply_tx: Sender<Result<String>>) {
|
||||
let mut queue = display.create_event_queue();
|
||||
let display_proxy = display.attach(queue.token());
|
||||
|
||||
let env = Environment::init(&display_proxy, SmithayClipboard::new());
|
||||
let req = queue.sync_roundtrip(&mut (), |_, _, _| unreachable!());
|
||||
let _ = req
|
||||
.and_then(|_| queue.sync_roundtrip(&mut (), |_, _, _| unreachable!()))
|
||||
.unwrap();
|
||||
|
||||
// Get data device manager.
|
||||
let data_device_manager = env.get_global::<WlDataDeviceManager>();
|
||||
|
||||
// Get primary selection device manager.
|
||||
let primary_selection_manager = env.get_primary_selection_manager();
|
||||
|
||||
// Both clipboards are not available, spin the loop and reply to a clipboard master.
|
||||
if data_device_manager.is_none() && primary_selection_manager.is_none() {
|
||||
loop {
|
||||
if let Ok(event) = request_rx.recv() {
|
||||
match event {
|
||||
Command::Exit => {
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
// Reply with error
|
||||
handlers::reply_error(&reply_tx, "Clipboard are missing.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track seats.
|
||||
let mut seats = Vec::<SeatData>::new();
|
||||
|
||||
for seat in env.get_all_seats() {
|
||||
let seat_data = match seat::clone_seat_data(&seat) {
|
||||
Some(seat_data) => {
|
||||
// Handle defunct seats early on.
|
||||
if seat_data.defunct {
|
||||
seats.push(SeatData::new(seat.detach(), None, None));
|
||||
continue;
|
||||
}
|
||||
|
||||
seat_data
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let keyboard = if seat_data.has_keyboard {
|
||||
let keyboard = seat.get_keyboard();
|
||||
let seat_clone = seat.clone();
|
||||
|
||||
keyboard.quick_assign(move |_keyboard, event, dispatch_data| {
|
||||
handlers::keyboard_handler(seat_clone.detach(), event, dispatch_data);
|
||||
});
|
||||
|
||||
Some(keyboard.detach())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pointer = if seat_data.has_pointer {
|
||||
let pointer = seat.get_pointer();
|
||||
let seat_clone = seat.clone();
|
||||
|
||||
pointer.quick_assign(move |_pointer, event, dispatch_data| {
|
||||
handlers::pointer_handler(seat_clone.detach(), event, dispatch_data);
|
||||
});
|
||||
|
||||
Some(pointer.detach())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Track the seat.
|
||||
seats.push(SeatData::new(seat.detach(), keyboard, pointer));
|
||||
}
|
||||
|
||||
// Listen for seats.
|
||||
let _listener = env.listen_for_seats(move |seat, seat_data, _| {
|
||||
let detached_seat = seat.clone().detach();
|
||||
let pos = seats.iter().position(|st| st.seat == detached_seat);
|
||||
let index = pos.unwrap_or_else(|| {
|
||||
seats.push(SeatData::new(detached_seat, None, None));
|
||||
seats.len() - 1
|
||||
});
|
||||
|
||||
let seat_resources = &mut seats[index];
|
||||
|
||||
if seat_data.has_keyboard && !seat_data.defunct {
|
||||
if seat_resources.keyboard.is_none() {
|
||||
let keyboard = seat.get_keyboard();
|
||||
let seat_clone = seat.clone();
|
||||
|
||||
keyboard.quick_assign(move |_keyboard, event, dispatch_data| {
|
||||
handlers::keyboard_handler(seat_clone.detach(), event, dispatch_data);
|
||||
});
|
||||
|
||||
seat_resources.keyboard = Some(keyboard.detach());
|
||||
}
|
||||
} else {
|
||||
// Clean up.
|
||||
if let Some(keyboard) = seat_resources.keyboard.take() {
|
||||
if keyboard.as_ref().version() >= 3 {
|
||||
keyboard.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if seat_data.has_pointer && !seat_data.defunct {
|
||||
if seat_resources.pointer.is_none() {
|
||||
let pointer = seat.get_pointer();
|
||||
|
||||
pointer.quick_assign(move |_pointer, event, dispatch_data| {
|
||||
handlers::pointer_handler(seat.detach(), event, dispatch_data);
|
||||
});
|
||||
|
||||
seat_resources.pointer = Some(pointer.detach());
|
||||
}
|
||||
} else if let Some(pointer) = seat_resources.pointer.take() {
|
||||
// Clean up.
|
||||
if pointer.as_ref().version() >= 3 {
|
||||
pointer.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Flush the display.
|
||||
let _ = queue.display().flush();
|
||||
|
||||
let mut dispatch_data = ClipboardDispatchData::new();
|
||||
|
||||
// Setup sleep amount tracker.
|
||||
let mut sa_tracker = SleepAmountTracker::new(MAX_TIME_TO_SLEEP, MAX_WARM_WAKEUPS);
|
||||
|
||||
loop {
|
||||
// Try to get event from the user.
|
||||
if let Ok(request) = request_rx.try_recv() {
|
||||
// Break early on to handle shutdown gracefully, otherwise we can crash on
|
||||
// `sync_roundtrip`, if client closed connection to a server before releasing the
|
||||
// clipboard.
|
||||
if request == Command::Exit {
|
||||
break;
|
||||
}
|
||||
// Reset the time we're sleeping.
|
||||
sa_tracker.reset_sleep();
|
||||
|
||||
queue
|
||||
.sync_roundtrip(&mut dispatch_data, |_, _, _| unimplemented!())
|
||||
.unwrap();
|
||||
|
||||
// Get latest observed seat and serial.
|
||||
let (seat, serial) = dispatch_data.last_seat().unwrap();
|
||||
let serial = *serial;
|
||||
|
||||
// Handle requests.
|
||||
match request {
|
||||
Command::Load => {
|
||||
if data_device_manager.is_some() {
|
||||
handle_load!(env, with_data_device, seat, queue, reply_tx);
|
||||
} else {
|
||||
handlers::reply_error(&reply_tx, "Clipboard is not available");
|
||||
}
|
||||
}
|
||||
Command::Store(contents) => {
|
||||
if data_device_manager.is_some() {
|
||||
handle_store!(
|
||||
env,
|
||||
new_data_source,
|
||||
with_data_device,
|
||||
DataSourceEvent,
|
||||
seat,
|
||||
serial,
|
||||
queue,
|
||||
contents
|
||||
);
|
||||
}
|
||||
}
|
||||
Command::LoadPrimary => {
|
||||
if primary_selection_manager.is_some() {
|
||||
handle_load!(env, with_primary_selection, seat, queue, reply_tx);
|
||||
} else {
|
||||
handlers::reply_error(&reply_tx, "Primary clipboard is not available");
|
||||
}
|
||||
}
|
||||
Command::StorePrimary(contents) => {
|
||||
if primary_selection_manager.is_some() {
|
||||
handle_store!(
|
||||
env,
|
||||
new_primary_selection_source,
|
||||
with_primary_selection,
|
||||
PrimarySelectionSourceEvent,
|
||||
seat,
|
||||
serial,
|
||||
queue,
|
||||
contents
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
let pending_events = queue
|
||||
.dispatch_pending(&mut dispatch_data, |_, _, _| {})
|
||||
.unwrap();
|
||||
|
||||
// If some application is trying to spam us when there're no seats, it's likely that
|
||||
// someone is trying to paste from us.
|
||||
if dispatch_data.last_seat().is_none() && pending_events != 0 {
|
||||
sa_tracker.reset_sleep();
|
||||
} else {
|
||||
// Time for thread to sleep.
|
||||
let tts = sa_tracker.sleep_amount();
|
||||
if tts > 0 {
|
||||
std::thread::sleep(Duration::from_millis(tts as _));
|
||||
}
|
||||
|
||||
sa_tracker.increase_sleep();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/worker/seat_data.rs
Normal file
20
src/worker/seat_data.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard;
|
||||
use sctk::reexports::client::protocol::wl_pointer::WlPointer;
|
||||
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
||||
|
||||
/// Data to track seat capability changes and handle release of the objects.
|
||||
pub struct SeatData {
|
||||
pub seat: WlSeat,
|
||||
pub keyboard: Option<WlKeyboard>,
|
||||
pub pointer: Option<WlPointer>,
|
||||
}
|
||||
|
||||
impl SeatData {
|
||||
pub fn new(seat: WlSeat, keyboard: Option<WlKeyboard>, pointer: Option<WlPointer>) -> Self {
|
||||
SeatData {
|
||||
seat,
|
||||
keyboard,
|
||||
pointer,
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/worker/sleep_amount_tracker.rs
Normal file
60
src/worker/sleep_amount_tracker.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/// Handles dynamic thread sleep time.
|
||||
pub struct SleepAmountTracker {
|
||||
/// The maximum amount of time for thread to sleep.
|
||||
max_time_to_sleep: u8,
|
||||
/// Current time to block the thread.
|
||||
time_to_sleep: u8,
|
||||
/// The current warm wakeup number.
|
||||
warm_wakeup: u8,
|
||||
/// The maximum amount of clipboard wakeups in a row with low sleep amount.
|
||||
max_warm_wakeups: u8,
|
||||
}
|
||||
|
||||
impl SleepAmountTracker {
|
||||
/// Build new tracker for sleep amount.
|
||||
///
|
||||
/// `max_time_to_sleep` - maximum sleep value for a thread.
|
||||
/// ``
|
||||
pub fn new(max_time_to_sleep: u8, max_warm_wakeups: u8) -> Self {
|
||||
Self {
|
||||
max_time_to_sleep,
|
||||
max_warm_wakeups,
|
||||
warm_wakeup: 0,
|
||||
time_to_sleep: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the current sleep amount to 0ms.
|
||||
#[inline]
|
||||
pub fn reset_sleep(&mut self) {
|
||||
self.time_to_sleep = 0;
|
||||
}
|
||||
|
||||
/// Adjust the sleep amount.
|
||||
#[inline]
|
||||
pub fn increase_sleep(&mut self) {
|
||||
if self.time_to_sleep == 0 {
|
||||
// Reset `time_to_sleep` to one, so we can reach `max_time_to_sleep`.
|
||||
self.time_to_sleep = 1;
|
||||
// Reset `warm_wakeup` count.
|
||||
self.warm_wakeup = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if self.warm_wakeup < self.max_warm_wakeups {
|
||||
// Handled warm wake up.
|
||||
self.warm_wakeup += 1;
|
||||
} else if self.time_to_sleep < self.max_warm_wakeups {
|
||||
// The aim of this different sleep times is to provide a good performance under
|
||||
// high the load and not waste system resources too much when idle.
|
||||
self.time_to_sleep = std::cmp::min(2 * self.time_to_sleep, self.max_time_to_sleep);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current time to sleep in ms.
|
||||
#[inline]
|
||||
pub fn sleep_amount(&self) -> u8 {
|
||||
self.time_to_sleep
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue