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

@ -2,6 +2,11 @@
## Unreleased
- Minimal rust version was bumped to 1.40.0
- Add support for `UTF8_STRING` mime type
- Clipboard now works only with extern display
- Clipboard now works only with last observed seats, instead of optionally accepting seat names
## 0.4.0 -- 2020-03-09
- Fix crash when receiving non-utf8 data

View file

@ -6,4 +6,4 @@ Smithay Clipboard is open to contributions from anyone.
There is a Matrix room dedicated to the Smithay project:
[#smithay:matrix.org](https://matrix.to/#/#smithay:matrix.org). If you don't want to use matrix, this room is
also bridged to gitter: https://gitter.im/smithay/Lobby.
also bridged to gitter: https://gitter.im/smithay/Lobby and freenode: `#smithay`.

View file

@ -1,7 +1,7 @@
[package]
name = "smithay-clipboard"
version = "0.4.0"
authors = ["Lucas Timmins <timmins.s.lucas@gmail.com>", "Victor Berger <victor.berger@m4x.org>"]
authors = ["Kirill Chibisov <contact@kchibisov.com>", "Lucas Timmins <timmins.s.lucas@gmail.com>", "Victor Berger <victor.berger@m4x.org>"]
edition = "2018"
description = "Provides access to the wayland clipboard for client applications."
repository = "https://github.com/smithay/smithay-clipboard"
@ -10,8 +10,8 @@ license = "MIT"
keywords = ["clipboard", "wayland"]
[dependencies]
sctk = { package = "smithay-client-toolkit", version = "0.6.1" }
nix = "0.14.1"
sctk = { package = "smithay-client-toolkit", version = "0.9.1", default-features = false }
wayland-client = { version = "0.26.3", features = ["dlopen"] }
[dev-dependencies]
andrew = "0.2"
sctk = { package = "smithay-client-toolkit", version = "0.9.1"}

View file

@ -4,7 +4,13 @@
# Smithay Clipboard
This crate provides access to the wayland clipboard.
This crate provides access to the Wayland clipboard for applications
already using some sort of GUI toolkit or a windowing library, like
[winit](https://github.com/rust-windowing/winit), since you should
have some surface around to receive keyboard/pointer events.
If you want to access clipboard from the CLI or to write clipboard manager,
this is not what you're looking for.
## Documentation

View file

@ -1,234 +1,290 @@
use std::io::{Read, Seek, SeekFrom, Write};
use std::sync::{atomic, Arc, Mutex};
use std::io::{BufWriter, Seek, SeekFrom, Write};
use sctk::keyboard::{map_keyboard_auto, Event as KbEvent, KeyState};
use sctk::utils::{DoubleMemPool, MemPool};
use sctk::window::{ConceptFrame, Event as WEvent, Window};
use sctk::Environment;
use sctk::seat;
use sctk::seat::keyboard::{self, Event as KeyboardEvent, KeyState, RepeatKind};
use sctk::shm::MemPool;
use sctk::window::{ConceptFrame, Event as WindowEvent};
use sctk::reexports::client::protocol::{wl_shm, wl_surface};
use sctk::reexports::client::{Display, NewProxy};
use sctk::reexports::calloop::Source as EventLoopSource;
use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_shm;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use andrew::shapes::rectangle;
use andrew::text;
use andrew::text::fontconfig;
use smithay_clipboard::Clipboard;
fn main() {
let (display, mut event_queue) =
Display::connect_to_env().expect("Failed to connect to the wayland server.");
let env = Environment::from_display(&*display, &mut event_queue).unwrap();
sctk::default_environment!(ClipboardExample, desktop);
let mut clipboard = smithay_clipboard::WaylandClipboard::new(&display);
let cb_contents = Arc::new(Mutex::new(String::new()));
/// Our dispatch data for simple clipboard access and processing frame events.
struct DispatchData {
/// Pending event from SCTK to update window.
pub pending_frame_event: Option<WindowEvent>,
/// Clipboard handler.
pub clipboard: Clipboard,
}
let seat = env
.manager
.instantiate_range(2, 6, NewProxy::implement_dummy)
.unwrap();
let need_redraw = Arc::new(atomic::AtomicBool::new(false));
let need_redraw_clone = need_redraw.clone();
let cb_contents_clone = cb_contents.clone();
map_keyboard_auto(&seat, move |event: KbEvent, _| {
if let KbEvent::Key {
state: KeyState::Pressed,
utf8: Some(text),
..
} = event
{
if text == " " {
*cb_contents_clone.lock().unwrap() = dbg!(clipboard.load(None).unwrap());
need_redraw_clone.store(true, atomic::Ordering::Relaxed)
} else if text == "s" {
clipboard.store(
None,
"This is an example text thats been copied to the wayland clipboard :)"
.to_string(),
);
}
impl DispatchData {
fn new(clipboard: Clipboard) -> Self {
Self {
pending_frame_event: None,
clipboard,
}
})
.unwrap();
let mut dimensions = (320u32, 240u32);
let surface = env
.compositor
.create_surface(NewProxy::implement_dummy)
.unwrap();
let next_action = Arc::new(Mutex::new(None::<WEvent>));
let waction = next_action.clone();
let mut window = Window::<ConceptFrame>::init_from_env(&env, surface, dimensions, move |evt| {
let mut next_action = waction.lock().unwrap();
// Keep last event in priority order : Close > Configure > Refresh
let replace = match (&evt, &*next_action) {
(_, &None)
| (_, &Some(WEvent::Refresh))
| (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. }))
| (&WEvent::Close, _) => true,
_ => false,
};
if replace {
*next_action = Some(evt);
}
})
.expect("Failed to create a window !");
window.new_seat(&seat);
window.set_title("Clipboard".to_string());
let mut pools = DoubleMemPool::new(&env.shm, || {}).expect("Failed to create a memory pool !");
let mut font_data = Vec::new();
std::fs::File::open(
&fontconfig::FontConfig::new()
.unwrap()
.get_regular_family_fonts("sans")
.unwrap()[0],
)
.unwrap()
.read_to_end(&mut font_data)
.unwrap();
if !env.shell.needs_configure() {
// initial draw to bootstrap on wl_shell
if let Some(pool) = pools.pool() {
redraw(
pool,
window.surface(),
dimensions,
&font_data,
"".to_string(),
);
}
window.refresh();
}
loop {
match next_action.lock().unwrap().take() {
Some(WEvent::Close) => break,
Some(WEvent::Refresh) => {
window.refresh();
window.surface().commit();
}
Some(WEvent::Configure { new_size, .. }) => {
if let Some((w, h)) = new_size {
window.resize(w, h);
dimensions = (w, h)
}
window.refresh();
if let Some(pool) = pools.pool() {
redraw(
pool,
window.surface(),
dimensions,
&font_data,
cb_contents.lock().unwrap().clone(),
);
}
}
None => {}
}
if need_redraw.swap(false, atomic::Ordering::Relaxed) {
if let Some(pool) = pools.pool() {
redraw(
pool,
window.surface(),
dimensions,
&font_data,
cb_contents.lock().unwrap().clone(),
);
}
window
.surface()
.damage_buffer(0, 0, dimensions.0 as i32, dimensions.1 as i32);
window.surface().commit();
}
event_queue.dispatch().unwrap();
}
}
fn redraw(
pool: &mut MemPool,
surface: &wl_surface::WlSurface,
dimensions: (u32, u32),
font_data: &[u8],
cb_contents: String,
) {
let (buf_x, buf_y) = (dimensions.0 as usize, dimensions.1 as usize);
fn main() {
// Setup default desktop environment
let (env, display, queue) = sctk::init_default_environment!(ClipboardExample, desktop)
.expect("unable to connect to a Wayland compositor.");
pool.resize(4 * buf_x * buf_y)
.expect("Failed to resize the memory pool.");
// Create event loop
let mut event_loop = sctk::reexports::calloop::EventLoop::<DispatchData>::new().unwrap();
let mut buf = vec![0; 4 * buf_x * buf_y];
let mut canvas =
andrew::Canvas::new(&mut buf, buf_x, buf_y, 4 * buf_x, andrew::Endian::native());
// Initial window dimentions
let mut dimentions = (320u32, 240u32);
let bg = rectangle::Rectangle::new((0, 0), (buf_x, buf_y), None, Some([255, 170, 20, 45]));
canvas.draw(&bg);
// Create surface
let surface = env.create_surface();
let text_box = rectangle::Rectangle::new(
(buf_x / 30, buf_y / 35),
(buf_x - 2 * (buf_x / 30), (buf_x as f32 / 14.) as usize),
Some((3, [255, 255, 255, 255], rectangle::Sides::ALL, Some(4))),
None,
);
canvas.draw(&text_box);
// Create window
let mut window = env
.create_window::<ConceptFrame, _>(surface, dimentions, move |event, mut dispatch_data| {
// Get our dispath data
let dispatch_data = dispatch_data.get::<DispatchData>().unwrap();
let helper_text = text::Text::new(
(buf_x / 25, buf_y / 30),
[255, 255, 255, 255],
font_data,
buf_x as f32 / 40.,
2.0,
"Press space to draw clipboard contents",
);
canvas.draw(&helper_text);
// Keep last event in priority order : Close > Configure > Refresh
let should_replace_event = match (&event, &dispatch_data.pending_frame_event) {
(_, &None)
| (_, &Some(WindowEvent::Refresh))
| (&WindowEvent::Configure { .. }, &Some(WindowEvent::Configure { .. }))
| (&WindowEvent::Close, _) => true,
_ => false,
};
let helper_text = text::Text::new(
(buf_x / 25, buf_y / 15),
[255, 255, 255, 255],
font_data,
buf_x as f32 / 40.,
2.0,
"Press 's' to store example text to clipboard",
);
canvas.draw(&helper_text);
if should_replace_event {
dispatch_data.pending_frame_event = Some(event);
}
})
.expect("failed to create a window.");
for i in (0..cb_contents.len()).step_by(36) {
let content = if cb_contents.len() < i + 36 {
cb_contents[i..].to_string()
} else {
cb_contents[i..i + 36].to_string()
// Set title and app id
window.set_title(String::from(
"smithay-clipboard example. Press C/P to copy/paste",
));
window.set_app_id(String::from("smithay-clipboard-example"));
// Create memory pool
let mut pools = env
.create_double_pool(|_| {})
.expect("failed to create a memory pool.");
// Structure to track seats
let mut seats = Vec::<(WlSeat, Option<(WlKeyboard, EventLoopSource<_>)>)>::new();
// Process existing seats
for seat in env.get_all_seats() {
let seat_data = match seat::with_seat_data(&seat, |seat_data| seat_data.clone()) {
Some(seat_data) => seat_data,
_ => continue,
};
let text = text::Text::new(
(
buf_x / 10,
buf_y / 8 + (i as f32 * buf_y as f32 / 1000.) as usize,
),
[255, 255, 255, 255],
font_data,
buf_x as f32 / 40.,
2.0,
content,
);
canvas.draw(&text);
if seat_data.has_keyboard && !seat_data.defunct {
// Suply event_loop's handle to handle key repeat
let event_loop_handle = event_loop.handle();
// Map keyboard for exising seats
let keyboard_mapping_result = keyboard::map_keyboard_repeat(
event_loop_handle,
&seat,
None,
RepeatKind::System,
move |event, _, mut dispatch_data| {
let dispatch_data = dispatch_data.get::<DispatchData>().unwrap();
process_keyboard_event(event, dispatch_data);
},
);
// Insert repeat rate handling source
match keyboard_mapping_result {
Ok((keyboard, repeat_source)) => {
seats.push((seat.detach(), Some((keyboard, repeat_source))));
}
Err(err) => {
eprintln!(
"Failed to map keyboard on seat {:?} : {:?}",
seat_data.name, err
);
seats.push((seat.detach(), None));
}
}
} else {
// Handle seats without keyboard, since they can gain keyboard later
seats.push((seat.detach(), None));
}
}
pool.seek(SeekFrom::Start(0)).unwrap();
pool.write_all(canvas.buffer).unwrap();
pool.flush().unwrap();
// Implement event listener for seats to handle capability change, etc
let event_loop_handle = event_loop.handle();
let _listener = env.listen_for_seats(move |seat, seat_data, _| {
// find the seat in the vec of seats or insert it
let idx = seats.iter().position(|(st, _)| st == &seat.detach());
let idx = idx.unwrap_or_else(|| {
seats.push((seat.detach(), None));
seats.len() - 1
});
let (_, mapped_keyboard) = &mut seats[idx];
if seat_data.has_keyboard && !seat_data.defunct {
// Map keyboard if it's not mapped already
if mapped_keyboard.is_none() {
let keyboard_mapping_result = keyboard::map_keyboard_repeat(
event_loop_handle.clone(),
&seat,
None,
RepeatKind::System,
move |event, _, mut dispatch_data| {
let dispatch_data = dispatch_data.get::<DispatchData>().unwrap();
process_keyboard_event(event, dispatch_data);
},
);
// Insert repeat rate source
match keyboard_mapping_result {
Ok((keyboard, repeat_source)) => {
*mapped_keyboard = Some((keyboard, repeat_source));
}
Err(err) => {
eprintln!(
"Failed to map keyboard on seat {} : {:?}",
seat_data.name, err
);
}
}
}
} else if let Some((keyboard, repeat_source)) = mapped_keyboard.take() {
if keyboard.as_ref().version() >= 3 {
keyboard.release();
}
event_loop_handle.remove(repeat_source);
}
});
if !env.get_shell().unwrap().needs_configure() {
if let Some(pool) = pools.pool() {
draw(pool, window.surface().clone(), dimentions).expect("failed to draw.")
}
// Refresh our frame
window.refresh();
}
sctk::WaylandSource::new(queue)
.quick_insert(event_loop.handle())
.unwrap();
let clipboard = Clipboard::new(display.get_display_ptr() as *mut _);
let mut dispatch_data = DispatchData::new(clipboard);
loop {
if let Some(frame_event) = dispatch_data.pending_frame_event.take() {
match frame_event {
WindowEvent::Close => break,
WindowEvent::Refresh => {
window.refresh();
window.surface().commit();
}
WindowEvent::Configure { new_size, .. } => {
if let Some((w, h)) = new_size {
window.resize(w, h);
dimentions = (w, h)
}
window.refresh();
if let Some(pool) = pools.pool() {
draw(pool, window.surface().clone(), dimentions).expect("failed to draw.")
}
}
}
}
display.flush().unwrap();
event_loop.dispatch(None, &mut dispatch_data).unwrap();
}
}
fn process_keyboard_event(event: KeyboardEvent, dispatch_data: &mut DispatchData) {
let text = match event {
KeyboardEvent::Key {
state,
utf8: Some(text),
..
} if state == KeyState::Pressed => text,
KeyboardEvent::Repeat {
utf8: Some(text), ..
} => text,
_ => return,
};
match text.as_str() {
// Paste primary.
"P" => {
let contents = dispatch_data
.clipboard
.load_primary()
.unwrap_or_else(|_| String::from("Failed to load primary selection"));
println!("Paste from primary clipboard: {}", contents);
}
// Paste.
"p" => {
let contents = dispatch_data
.clipboard
.load()
.unwrap_or_else(|_| String::from("Failed to load primary selection"));
println!("Paste: {}", contents);
}
// Copy primary.
"C" => {
let text = String::from("Copy primary");
dispatch_data.clipboard.store_primary(text.clone());
println!("Copied string into primary selection buffer: {}", text);
}
// Copy.
"c" => {
let text = String::from("Copy");
dispatch_data.clipboard.store(text.clone());
println!("Copied string: {}", text);
}
_ => (),
}
}
fn draw(
pool: &mut MemPool,
surface: WlSurface,
dimensions: (u32, u32),
) -> Result<(), std::io::Error> {
pool.resize((4 * dimensions.0 * dimensions.1) as usize)
.expect("failed to resize memory pool");
{
pool.seek(SeekFrom::Start(0))?;
let mut writer = BufWriter::new(&mut *pool);
for _ in 0..dimensions.0 * dimensions.1 {
// ARGB color written in LE, so it's #FF1C1C1C
writer.write_all(&[0x1c, 0x1c, 0x1c, 0xff])?;
}
writer.flush()?;
}
let new_buffer = pool.buffer(
0,
buf_x as i32,
buf_y as i32,
4 * buf_x as i32,
dimensions.0 as i32,
dimensions.1 as i32,
4 * dimensions.0 as i32,
wl_shm::Format::Argb8888,
);
surface.attach(Some(&new_buffer), 0, 0);
surface.commit();
Ok(())
}

80
src/env.rs Normal file
View 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,
]
);

View file

@ -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
View 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")
}

View file

@ -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,
&reg,
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(),
&reg,
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")
}

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