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
|
## 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
|
## 0.4.0 -- 2020-03-09
|
||||||
|
|
||||||
- Fix crash when receiving non-utf8 data
|
- 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:
|
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
|
[#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]
|
[package]
|
||||||
name = "smithay-clipboard"
|
name = "smithay-clipboard"
|
||||||
version = "0.4.0"
|
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"
|
edition = "2018"
|
||||||
description = "Provides access to the wayland clipboard for client applications."
|
description = "Provides access to the wayland clipboard for client applications."
|
||||||
repository = "https://github.com/smithay/smithay-clipboard"
|
repository = "https://github.com/smithay/smithay-clipboard"
|
||||||
|
|
@ -10,8 +10,8 @@ license = "MIT"
|
||||||
keywords = ["clipboard", "wayland"]
|
keywords = ["clipboard", "wayland"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sctk = { package = "smithay-client-toolkit", version = "0.6.1" }
|
sctk = { package = "smithay-client-toolkit", version = "0.9.1", default-features = false }
|
||||||
nix = "0.14.1"
|
wayland-client = { version = "0.26.3", features = ["dlopen"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
andrew = "0.2"
|
sctk = { package = "smithay-client-toolkit", version = "0.9.1"}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@
|
||||||
|
|
||||||
# Smithay Clipboard
|
# 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
|
## Documentation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,234 +1,290 @@
|
||||||
use std::io::{Read, Seek, SeekFrom, Write};
|
use std::io::{BufWriter, Seek, SeekFrom, Write};
|
||||||
use std::sync::{atomic, Arc, Mutex};
|
|
||||||
|
|
||||||
use sctk::keyboard::{map_keyboard_auto, Event as KbEvent, KeyState};
|
use sctk::seat;
|
||||||
use sctk::utils::{DoubleMemPool, MemPool};
|
use sctk::seat::keyboard::{self, Event as KeyboardEvent, KeyState, RepeatKind};
|
||||||
use sctk::window::{ConceptFrame, Event as WEvent, Window};
|
use sctk::shm::MemPool;
|
||||||
use sctk::Environment;
|
use sctk::window::{ConceptFrame, Event as WindowEvent};
|
||||||
|
|
||||||
use sctk::reexports::client::protocol::{wl_shm, wl_surface};
|
use sctk::reexports::calloop::Source as EventLoopSource;
|
||||||
use sctk::reexports::client::{Display, NewProxy};
|
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 smithay_clipboard::Clipboard;
|
||||||
use andrew::text;
|
|
||||||
use andrew::text::fontconfig;
|
|
||||||
|
|
||||||
fn main() {
|
sctk::default_environment!(ClipboardExample, desktop);
|
||||||
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();
|
|
||||||
|
|
||||||
let mut clipboard = smithay_clipboard::WaylandClipboard::new(&display);
|
/// Our dispatch data for simple clipboard access and processing frame events.
|
||||||
let cb_contents = Arc::new(Mutex::new(String::new()));
|
struct DispatchData {
|
||||||
|
/// Pending event from SCTK to update window.
|
||||||
|
pub pending_frame_event: Option<WindowEvent>,
|
||||||
|
/// Clipboard handler.
|
||||||
|
pub clipboard: Clipboard,
|
||||||
|
}
|
||||||
|
|
||||||
let seat = env
|
impl DispatchData {
|
||||||
.manager
|
fn new(clipboard: Clipboard) -> Self {
|
||||||
.instantiate_range(2, 6, NewProxy::implement_dummy)
|
Self {
|
||||||
.unwrap();
|
pending_frame_event: None,
|
||||||
|
clipboard,
|
||||||
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();
|
|
||||||
|
|
||||||
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(
|
fn main() {
|
||||||
pool: &mut MemPool,
|
// Setup default desktop environment
|
||||||
surface: &wl_surface::WlSurface,
|
let (env, display, queue) = sctk::init_default_environment!(ClipboardExample, desktop)
|
||||||
dimensions: (u32, u32),
|
.expect("unable to connect to a Wayland compositor.");
|
||||||
font_data: &[u8],
|
|
||||||
cb_contents: String,
|
|
||||||
) {
|
|
||||||
let (buf_x, buf_y) = (dimensions.0 as usize, dimensions.1 as usize);
|
|
||||||
|
|
||||||
pool.resize(4 * buf_x * buf_y)
|
// Create event loop
|
||||||
.expect("Failed to resize the memory pool.");
|
let mut event_loop = sctk::reexports::calloop::EventLoop::<DispatchData>::new().unwrap();
|
||||||
|
|
||||||
let mut buf = vec![0; 4 * buf_x * buf_y];
|
// Initial window dimentions
|
||||||
let mut canvas =
|
let mut dimentions = (320u32, 240u32);
|
||||||
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]));
|
// Create surface
|
||||||
canvas.draw(&bg);
|
let surface = env.create_surface();
|
||||||
|
|
||||||
let text_box = rectangle::Rectangle::new(
|
// Create window
|
||||||
(buf_x / 30, buf_y / 35),
|
let mut window = env
|
||||||
(buf_x - 2 * (buf_x / 30), (buf_x as f32 / 14.) as usize),
|
.create_window::<ConceptFrame, _>(surface, dimentions, move |event, mut dispatch_data| {
|
||||||
Some((3, [255, 255, 255, 255], rectangle::Sides::ALL, Some(4))),
|
// Get our dispath data
|
||||||
None,
|
let dispatch_data = dispatch_data.get::<DispatchData>().unwrap();
|
||||||
);
|
|
||||||
canvas.draw(&text_box);
|
|
||||||
|
|
||||||
let helper_text = text::Text::new(
|
// Keep last event in priority order : Close > Configure > Refresh
|
||||||
(buf_x / 25, buf_y / 30),
|
let should_replace_event = match (&event, &dispatch_data.pending_frame_event) {
|
||||||
[255, 255, 255, 255],
|
(_, &None)
|
||||||
font_data,
|
| (_, &Some(WindowEvent::Refresh))
|
||||||
buf_x as f32 / 40.,
|
| (&WindowEvent::Configure { .. }, &Some(WindowEvent::Configure { .. }))
|
||||||
2.0,
|
| (&WindowEvent::Close, _) => true,
|
||||||
"Press space to draw clipboard contents",
|
_ => false,
|
||||||
);
|
};
|
||||||
canvas.draw(&helper_text);
|
|
||||||
|
|
||||||
let helper_text = text::Text::new(
|
if should_replace_event {
|
||||||
(buf_x / 25, buf_y / 15),
|
dispatch_data.pending_frame_event = Some(event);
|
||||||
[255, 255, 255, 255],
|
}
|
||||||
font_data,
|
})
|
||||||
buf_x as f32 / 40.,
|
.expect("failed to create a window.");
|
||||||
2.0,
|
|
||||||
"Press 's' to store example text to clipboard",
|
|
||||||
);
|
|
||||||
canvas.draw(&helper_text);
|
|
||||||
|
|
||||||
for i in (0..cb_contents.len()).step_by(36) {
|
// Set title and app id
|
||||||
let content = if cb_contents.len() < i + 36 {
|
window.set_title(String::from(
|
||||||
cb_contents[i..].to_string()
|
"smithay-clipboard example. Press C/P to copy/paste",
|
||||||
} else {
|
));
|
||||||
cb_contents[i..i + 36].to_string()
|
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(
|
|
||||||
(
|
if seat_data.has_keyboard && !seat_data.defunct {
|
||||||
buf_x / 10,
|
// Suply event_loop's handle to handle key repeat
|
||||||
buf_y / 8 + (i as f32 * buf_y as f32 / 1000.) as usize,
|
let event_loop_handle = event_loop.handle();
|
||||||
),
|
|
||||||
[255, 255, 255, 255],
|
// Map keyboard for exising seats
|
||||||
font_data,
|
let keyboard_mapping_result = keyboard::map_keyboard_repeat(
|
||||||
buf_x as f32 / 40.,
|
event_loop_handle,
|
||||||
2.0,
|
&seat,
|
||||||
content,
|
None,
|
||||||
);
|
RepeatKind::System,
|
||||||
canvas.draw(&text);
|
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();
|
// Implement event listener for seats to handle capability change, etc
|
||||||
pool.write_all(canvas.buffer).unwrap();
|
let event_loop_handle = event_loop.handle();
|
||||||
pool.flush().unwrap();
|
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(
|
let new_buffer = pool.buffer(
|
||||||
0,
|
0,
|
||||||
buf_x as i32,
|
dimensions.0 as i32,
|
||||||
buf_y as i32,
|
dimensions.1 as i32,
|
||||||
4 * buf_x as i32,
|
4 * dimensions.0 as i32,
|
||||||
wl_shm::Format::Argb8888,
|
wl_shm::Format::Argb8888,
|
||||||
);
|
);
|
||||||
surface.attach(Some(&new_buffer), 0, 0);
|
surface.attach(Some(&new_buffer), 0, 0);
|
||||||
surface.commit();
|
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
|
//! Smithay Clipboard
|
||||||
//!
|
//!
|
||||||
//! Provides access to the wayland clipboard with only requirement being a WlDisplay
|
//! Provides access to the Wayland clipboard for gui applications. The user should have surface
|
||||||
//! object
|
//! around.
|
||||||
//!
|
|
||||||
//! ```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));
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
use std::ffi::c_void;
|
||||||
|
use std::io::Result;
|
||||||
|
use std::sync::mpsc::{self, Receiver, Sender};
|
||||||
|
|
||||||
mod threaded;
|
use sctk::reexports::client::Display;
|
||||||
pub use crate::threaded::ThreadedClipboard;
|
|
||||||
pub use crate::threaded::ThreadedClipboard as WaylandClipboard;
|
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