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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
sctk::default_environment!(ClipboardExample, desktop);
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl DispatchData {
|
||||
fn new(clipboard: Clipboard) -> Self {
|
||||
Self {
|
||||
pending_frame_event: None,
|
||||
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();
|
||||
// Setup default desktop environment
|
||||
let (env, display, queue) = sctk::init_default_environment!(ClipboardExample, desktop)
|
||||
.expect("unable to connect to a Wayland compositor.");
|
||||
|
||||
let mut clipboard = smithay_clipboard::WaylandClipboard::new(&display);
|
||||
let cb_contents = Arc::new(Mutex::new(String::new()));
|
||||
// Create event loop
|
||||
let mut event_loop = sctk::reexports::calloop::EventLoop::<DispatchData>::new().unwrap();
|
||||
|
||||
let seat = env
|
||||
.manager
|
||||
.instantiate_range(2, 6, NewProxy::implement_dummy)
|
||||
.unwrap();
|
||||
// Initial window dimentions
|
||||
let mut dimentions = (320u32, 240u32);
|
||||
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
// Create surface
|
||||
let surface = env.create_surface();
|
||||
|
||||
let mut dimensions = (320u32, 240u32);
|
||||
let surface = env
|
||||
.compositor
|
||||
.create_surface(NewProxy::implement_dummy)
|
||||
.unwrap();
|
||||
// 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 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) {
|
||||
let should_replace_event = match (&event, &dispatch_data.pending_frame_event) {
|
||||
(_, &None)
|
||||
| (_, &Some(WEvent::Refresh))
|
||||
| (&WEvent::Configure { .. }, &Some(WEvent::Configure { .. }))
|
||||
| (&WEvent::Close, _) => true,
|
||||
| (_, &Some(WindowEvent::Refresh))
|
||||
| (&WindowEvent::Configure { .. }, &Some(WindowEvent::Configure { .. }))
|
||||
| (&WindowEvent::Close, _) => true,
|
||||
_ => false,
|
||||
};
|
||||
if replace {
|
||||
*next_action = Some(evt);
|
||||
|
||||
if should_replace_event {
|
||||
dispatch_data.pending_frame_event = Some(event);
|
||||
}
|
||||
})
|
||||
.expect("Failed to create a window !");
|
||||
.expect("failed to create a window.");
|
||||
|
||||
window.new_seat(&seat);
|
||||
window.set_title("Clipboard".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"));
|
||||
|
||||
let mut pools = DoubleMemPool::new(&env.shm, || {}).expect("Failed to create a memory pool !");
|
||||
// Create memory pool
|
||||
let mut pools = env
|
||||
.create_double_pool(|_| {})
|
||||
.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)
|
||||
// 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,
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
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();
|
||||
}
|
||||
let clipboard = Clipboard::new(display.get_display_ptr() as *mut _);
|
||||
let mut dispatch_data = DispatchData::new(clipboard);
|
||||
|
||||
loop {
|
||||
match next_action.lock().unwrap().take() {
|
||||
Some(WEvent::Close) => break,
|
||||
Some(WEvent::Refresh) => {
|
||||
if let Some(frame_event) = dispatch_data.pending_frame_event.take() {
|
||||
match frame_event {
|
||||
WindowEvent::Close => break,
|
||||
WindowEvent::Refresh => {
|
||||
window.refresh();
|
||||
window.surface().commit();
|
||||
}
|
||||
Some(WEvent::Configure { new_size, .. }) => {
|
||||
WindowEvent::Configure { new_size, .. } => {
|
||||
if let Some((w, h)) = new_size {
|
||||
window.resize(w, h);
|
||||
dimensions = (w, h)
|
||||
dimentions = (w, h)
|
||||
}
|
||||
window.refresh();
|
||||
if let Some(pool) = pools.pool() {
|
||||
redraw(
|
||||
pool,
|
||||
window.surface(),
|
||||
dimensions,
|
||||
&font_data,
|
||||
cb_contents.lock().unwrap().clone(),
|
||||
);
|
||||
draw(pool, window.surface().clone(), dimentions).expect("failed to draw.")
|
||||
}
|
||||
}
|
||||
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);
|
||||
display.flush().unwrap();
|
||||
|
||||
pool.resize(4 * buf_x * buf_y)
|
||||
.expect("Failed to resize the memory pool.");
|
||||
event_loop.dispatch(None, &mut dispatch_data).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());
|
||||
|
||||
let bg = rectangle::Rectangle::new((0, 0), (buf_x, buf_y), None, Some([255, 170, 20, 45]));
|
||||
canvas.draw(&bg);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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()
|
||||
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,
|
||||
};
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pool.seek(SeekFrom::Start(0)).unwrap();
|
||||
pool.write_all(canvas.buffer).unwrap();
|
||||
pool.flush().unwrap();
|
||||
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
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