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:
Kirill Chibisov 2020-05-10 16:40:03 +03:00 committed by GitHub
parent a4240ad835
commit bb652c775b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1040 additions and 1055 deletions

View 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
View 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
View 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
View 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,
}
}
}

View 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
}
}