Update to SCTK 0.18.0
The main highlight is the move to calloop to handle all the requests which makes it possible to remove all the active polling and clean the data reading/writing, as well as accepting arbitrary long payloads. This update also fixes the CI for the repository by moving it to github actions, sets the minimum rust version, and edition to 2021. Fixes #46. Fixes #44. Fixes #34.
This commit is contained in:
parent
e2ffc4f183
commit
31392200dc
15 changed files with 1124 additions and 937 deletions
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
|
|
@ -37,9 +37,16 @@ jobs:
|
|||
- uses: hecrj/setup-rust-action@v1
|
||||
with:
|
||||
rust-version: ${{ matrix.rust_version }}
|
||||
components: clippy
|
||||
|
||||
- name: Check documentation
|
||||
run: cargo doc --no-deps --document-private-items
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get install libxkbcommon-dev libwayland-dev
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
- name: Check documentation
|
||||
run: cargo doc --no-deps --document-private-items
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Update SCTK to 0.18
|
||||
- Fix active polling of the clipboard each 50ms
|
||||
- Fix freeze when copying data larger than the pipe buffer size
|
||||
|
||||
## 0.6.6 -- 2022-06-20
|
||||
|
||||
- Update SCTK to 0.16
|
||||
|
|
|
|||
13
Cargo.toml
13
Cargo.toml
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "smithay-clipboard"
|
||||
version = "0.6.6"
|
||||
authors = ["Kirill Chibisov <contact@kchibisov.com>", "Victor Berger <victor.berger@m4x.org>", "Lucas Timmins <timmins.s.lucas@gmail.com>"]
|
||||
edition = "2018"
|
||||
authors = ["Kirill Chibisov <contact@kchibisov.com>", "Victor Berger <victor.berger@m4x.org>"]
|
||||
edition = "2021"
|
||||
description = "Provides access to the wayland clipboard for client applications."
|
||||
repository = "https://github.com/smithay/smithay-clipboard"
|
||||
documentation = "https://smithay.github.io/smithay-clipboard"
|
||||
|
|
@ -11,12 +11,13 @@ keywords = ["clipboard", "wayland"]
|
|||
rust-version = "1.65.0"
|
||||
|
||||
[dependencies]
|
||||
sctk = { package = "smithay-client-toolkit", version = "0.16", default-features = false }
|
||||
wayland-client = { version = "0.29", features = ["use_system_lib"] }
|
||||
libc = "0.2.149"
|
||||
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop"] }
|
||||
wayland-backend = { version = "0.3.0", default_features = false, features = ["client_system"] }
|
||||
|
||||
[dev-dependencies]
|
||||
sctk = { package = "smithay-client-toolkit", version = "0.16", default-features = false, features = ["calloop"] }
|
||||
sctk = { package = "smithay-client-toolkit", version = "0.18.0", default-features = false, features = ["calloop", "xkbcommon"] }
|
||||
|
||||
[features]
|
||||
default = ["dlopen"]
|
||||
dlopen = ["sctk/dlopen", "wayland-client/dlopen"]
|
||||
dlopen = ["wayland-backend/dlopen" ]
|
||||
|
|
|
|||
|
|
@ -1,269 +1,405 @@
|
|||
use std::io::{BufWriter, Seek, SeekFrom, Write};
|
||||
// The example just demonstrates how to integrate the smithay-clipboard into the
|
||||
// application. For more details on what is going on, consult the
|
||||
// `smithay-client-toolkit` examples.
|
||||
|
||||
use sctk::seat;
|
||||
use sctk::seat::keyboard::{self, Event as KeyboardEvent, KeyState, RepeatKind};
|
||||
use sctk::shm::MemPool;
|
||||
use sctk::window::{Event as WindowEvent, FallbackFrame};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use sctk::reexports::client::protocol::wl_shm;
|
||||
use sctk::reexports::client::protocol::wl_surface::WlSurface;
|
||||
use sctk::compositor::{CompositorHandler, CompositorState};
|
||||
use sctk::output::{OutputHandler, OutputState};
|
||||
use sctk::reexports::calloop::{EventLoop, LoopHandle};
|
||||
use sctk::reexports::calloop_wayland_source::WaylandSource;
|
||||
use sctk::reexports::client::globals::registry_queue_init;
|
||||
use sctk::reexports::client::protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface};
|
||||
use sctk::reexports::client::{Connection, Proxy, QueueHandle};
|
||||
use sctk::registry::{ProvidesRegistryState, RegistryState};
|
||||
use sctk::seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers};
|
||||
use sctk::seat::{Capability, SeatHandler, SeatState};
|
||||
use sctk::shell::xdg::window::{Window, WindowConfigure, WindowDecorations, WindowHandler};
|
||||
use sctk::shell::xdg::XdgShell;
|
||||
use sctk::shell::WaylandSurface;
|
||||
use sctk::shm::slot::{Buffer, SlotPool};
|
||||
use sctk::shm::{Shm, ShmHandler};
|
||||
use sctk::{
|
||||
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
|
||||
delegate_shm, delegate_xdg_shell, delegate_xdg_window, registry_handlers,
|
||||
};
|
||||
use smithay_clipboard::{self, Clipboard};
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
const MIN_DIM_SIZE: usize = 256;
|
||||
|
||||
fn main() {
|
||||
// Setup default desktop environment
|
||||
let (env, display, queue) = sctk::new_default_environment!(ClipboardExample, desktop)
|
||||
.expect("unable to connect to a Wayland compositor.");
|
||||
let connection = Connection::connect_to_env().unwrap();
|
||||
let (globals, event_queue) = registry_queue_init(&connection).unwrap();
|
||||
let queue_handle = event_queue.handle();
|
||||
let mut event_loop: EventLoop<SimpleWindow> =
|
||||
EventLoop::try_new().expect("Failed to initialize the event loop!");
|
||||
let loop_handle = event_loop.handle();
|
||||
WaylandSource::new(connection.clone(), event_queue).insert(loop_handle).unwrap();
|
||||
|
||||
// Create event loop
|
||||
let mut event_loop = sctk::reexports::calloop::EventLoop::<DispatchData>::try_new().unwrap();
|
||||
let compositor =
|
||||
CompositorState::bind(&globals, &queue_handle).expect("wl_compositor not available");
|
||||
let xdg_shell = XdgShell::bind(&globals, &queue_handle).expect("xdg shell is not available");
|
||||
|
||||
// Initial window dimentions
|
||||
let mut dimentions = (320u32, 240u32);
|
||||
let shm = Shm::bind(&globals, &queue_handle).expect("wl shm is not available.");
|
||||
let surface = compositor.create_surface(&queue_handle);
|
||||
let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &queue_handle);
|
||||
|
||||
// Create surface
|
||||
let surface = env.create_surface().detach();
|
||||
window.set_title(String::from("smithay-clipboard example. Press C/c/P/p to copy/paste"));
|
||||
window.set_min_size(Some((MIN_DIM_SIZE as u32, MIN_DIM_SIZE as u32)));
|
||||
window.commit();
|
||||
|
||||
// Create window
|
||||
let mut window = env
|
||||
.create_window::<FallbackFrame, _>(
|
||||
surface,
|
||||
None,
|
||||
dimentions,
|
||||
move |event, mut dispatch_data| {
|
||||
// Get our dispath data
|
||||
let dispatch_data = dispatch_data.get::<DispatchData>().unwrap();
|
||||
let clipboard = unsafe { Clipboard::new(connection.display().id().as_ptr() as *mut _) };
|
||||
|
||||
// Keep last event in priority order : Close > Configure > Refresh
|
||||
let should_replace_event = match (&event, &dispatch_data.pending_frame_event) {
|
||||
(_, &None)
|
||||
| (_, &Some(WindowEvent::Refresh))
|
||||
| (&WindowEvent::Configure { .. }, &Some(WindowEvent::Configure { .. }))
|
||||
| (&WindowEvent::Close, _) => true,
|
||||
_ => false,
|
||||
let pool = SlotPool::new(MIN_DIM_SIZE * MIN_DIM_SIZE * 4, &shm).expect("Failed to create pool");
|
||||
|
||||
let mut simple_window = SimpleWindow {
|
||||
registry_state: RegistryState::new(&globals),
|
||||
seat_state: SeatState::new(&globals, &queue_handle),
|
||||
output_state: OutputState::new(&globals, &queue_handle),
|
||||
shm,
|
||||
clipboard,
|
||||
|
||||
exit: false,
|
||||
first_configure: true,
|
||||
pool,
|
||||
width: 256,
|
||||
height: 256,
|
||||
buffer: None,
|
||||
window,
|
||||
keyboard: None,
|
||||
keyboard_focus: false,
|
||||
loop_handle: event_loop.handle(),
|
||||
};
|
||||
|
||||
if should_replace_event {
|
||||
dispatch_data.pending_frame_event = Some(event);
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("failed to create a window.");
|
||||
|
||||
// Set title and app id
|
||||
window.set_title(String::from("smithay-clipboard example. Press C/P to copy/paste"));
|
||||
window.set_app_id(String::from("smithay-clipboard-example"));
|
||||
|
||||
// Create memory pool
|
||||
let mut pools = env.create_double_pool(|_| {}).expect("failed to create a memory pool.");
|
||||
|
||||
// Structure to track seats
|
||||
let mut seats = Vec::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) => {
|
||||
seats.push((seat.detach(), Some(keyboard)));
|
||||
}
|
||||
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) => {
|
||||
*mapped_keyboard = Some(keyboard);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to map keyboard on seat {} : {:?}", seat_data.name, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(keyboard) = mapped_keyboard.take() {
|
||||
if keyboard.as_ref().version() >= 3 {
|
||||
keyboard.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 = unsafe { Clipboard::new(display.get_display_ptr() as *mut _) };
|
||||
let mut dispatch_data = DispatchData::new(clipboard);
|
||||
|
||||
// We don't draw immediately, the configure will notify us when to first draw.
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
event_loop.dispatch(None, &mut simple_window).unwrap();
|
||||
|
||||
display.flush().unwrap();
|
||||
|
||||
event_loop.dispatch(None, &mut dispatch_data).unwrap();
|
||||
if simple_window.exit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
struct SimpleWindow {
|
||||
registry_state: RegistryState,
|
||||
seat_state: SeatState,
|
||||
output_state: OutputState,
|
||||
shm: Shm,
|
||||
clipboard: Clipboard,
|
||||
|
||||
match text.as_str() {
|
||||
exit: bool,
|
||||
first_configure: bool,
|
||||
pool: SlotPool,
|
||||
width: u32,
|
||||
height: u32,
|
||||
buffer: Option<Buffer>,
|
||||
window: Window,
|
||||
keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||
keyboard_focus: bool,
|
||||
loop_handle: LoopHandle<'static, SimpleWindow>,
|
||||
}
|
||||
|
||||
impl CompositorHandler for SimpleWindow {
|
||||
fn scale_factor_changed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_new_factor: i32,
|
||||
) {
|
||||
// Not needed for this example.
|
||||
}
|
||||
|
||||
fn transform_changed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_new_transform: wl_output::Transform,
|
||||
) {
|
||||
// Not needed for this example.
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
_surface: &wl_surface::WlSurface,
|
||||
_time: u32,
|
||||
) {
|
||||
self.draw(conn, qh);
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputHandler for SimpleWindow {
|
||||
fn output_state(&mut self) -> &mut OutputState {
|
||||
&mut self.output_state
|
||||
}
|
||||
|
||||
fn new_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
|
||||
fn update_output(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
|
||||
fn output_destroyed(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_output: wl_output::WlOutput,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowHandler for SimpleWindow {
|
||||
fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &Window) {
|
||||
self.exit = true;
|
||||
}
|
||||
|
||||
fn configure(
|
||||
&mut self,
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
_window: &Window,
|
||||
configure: WindowConfigure,
|
||||
_serial: u32,
|
||||
) {
|
||||
println!("Window configured to: {:?}", configure);
|
||||
|
||||
self.buffer = None;
|
||||
self.width = configure.new_size.0.map(|v| v.get()).unwrap_or(256);
|
||||
self.height = configure.new_size.1.map(|v| v.get()).unwrap_or(256);
|
||||
|
||||
// Initiate the first draw.
|
||||
if self.first_configure {
|
||||
self.first_configure = false;
|
||||
self.draw(conn, qh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SeatHandler for SimpleWindow {
|
||||
fn seat_state(&mut self) -> &mut SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
seat: wl_seat::WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
if capability == Capability::Keyboard && self.keyboard.is_none() {
|
||||
println!("Set keyboard capability");
|
||||
let keyboard = self
|
||||
.seat_state
|
||||
.get_keyboard_with_repeat(
|
||||
qh,
|
||||
&seat,
|
||||
None,
|
||||
self.loop_handle.clone(),
|
||||
Box::new(|_state, _wl_kbd, event| {
|
||||
println!("Repeat: {:?} ", event);
|
||||
}),
|
||||
)
|
||||
.expect("Failed to create keyboard");
|
||||
|
||||
self.keyboard = Some(keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: wl_seat::WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
if capability == Capability::Keyboard && self.keyboard.is_some() {
|
||||
println!("Unset keyboard capability");
|
||||
self.keyboard.take().unwrap().release();
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
|
||||
}
|
||||
|
||||
impl KeyboardHandler for SimpleWindow {
|
||||
fn enter(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
surface: &wl_surface::WlSurface,
|
||||
_: u32,
|
||||
_: &[u32],
|
||||
keysyms: &[Keysym],
|
||||
) {
|
||||
if self.window.wl_surface() == surface {
|
||||
println!("Keyboard focus on window with pressed syms: {keysyms:?}");
|
||||
self.keyboard_focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn leave(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
surface: &wl_surface::WlSurface,
|
||||
_: u32,
|
||||
) {
|
||||
if self.window.wl_surface() == surface {
|
||||
println!("Release keyboard focus on window");
|
||||
self.keyboard_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn press_key(
|
||||
&mut self,
|
||||
_conn: &Connection,
|
||||
_qh: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: u32,
|
||||
event: KeyEvent,
|
||||
) {
|
||||
match event.utf8.as_deref() {
|
||||
// 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 selection"));
|
||||
println!("Paste: {}", contents);
|
||||
}
|
||||
Some("P") => match self.clipboard.load_primary() {
|
||||
Ok(contents) => println!("Paste from primary clipboard: {contents}"),
|
||||
Err(err) => eprintln!("Error loading from primary clipboard: {err}"),
|
||||
},
|
||||
// Paste clipboard.
|
||||
Some("p") => match self.clipboard.load() {
|
||||
Ok(contents) => println!("Paste from clipboard: {contents}"),
|
||||
Err(err) => eprintln!("Error loading from clipboard: {err}"),
|
||||
},
|
||||
// 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);
|
||||
}
|
||||
Some("C") => {
|
||||
let to_store = "Copy primary";
|
||||
self.clipboard.store_primary(to_store);
|
||||
println!("Copied string into primary clipboard: {}", to_store);
|
||||
},
|
||||
// Copy clipboard.
|
||||
Some("c") => {
|
||||
let to_store = "Copy";
|
||||
self.clipboard.store(to_store);
|
||||
println!("Copied string into clipboard: {}", to_store);
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn release_key(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_: u32,
|
||||
_event: KeyEvent,
|
||||
) {
|
||||
}
|
||||
|
||||
fn update_modifiers(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
_serial: u32,
|
||||
_modifiers: Modifiers,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
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()?;
|
||||
impl ShmHandler for SimpleWindow {
|
||||
fn shm_state(&mut self) -> &mut Shm {
|
||||
&mut self.shm
|
||||
}
|
||||
}
|
||||
|
||||
let new_buffer = pool.buffer(
|
||||
0,
|
||||
dimensions.0 as i32,
|
||||
dimensions.1 as i32,
|
||||
4 * dimensions.0 as i32,
|
||||
impl SimpleWindow {
|
||||
pub fn draw(&mut self, _conn: &Connection, qh: &QueueHandle<Self>) {
|
||||
let width = self.width;
|
||||
let height = self.height;
|
||||
let stride = self.width as i32 * 4;
|
||||
|
||||
let buffer = self.buffer.get_or_insert_with(|| {
|
||||
self.pool
|
||||
.create_buffer(width as i32, height as i32, stride, wl_shm::Format::Argb8888)
|
||||
.expect("create buffer")
|
||||
.0
|
||||
});
|
||||
|
||||
let canvas = match self.pool.canvas(buffer) {
|
||||
Some(canvas) => canvas,
|
||||
None => {
|
||||
// This should be rare, but if the compositor has not released the previous
|
||||
// buffer, we need double-buffering.
|
||||
let (second_buffer, canvas) = self
|
||||
.pool
|
||||
.create_buffer(
|
||||
self.width as i32,
|
||||
self.height as i32,
|
||||
stride,
|
||||
wl_shm::Format::Argb8888,
|
||||
);
|
||||
surface.attach(Some(&new_buffer), 0, 0);
|
||||
surface.commit();
|
||||
)
|
||||
.expect("create buffer");
|
||||
*buffer = second_buffer;
|
||||
canvas
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
// Draw to the window:
|
||||
canvas.chunks_exact_mut(4).enumerate().for_each(|(_, chunk)| {
|
||||
// ARGB color.
|
||||
let color = 0xFF181818u32;
|
||||
|
||||
let array: &mut [u8; 4] = chunk.try_into().unwrap();
|
||||
*array = color.to_le_bytes();
|
||||
});
|
||||
|
||||
// Damage the entire window
|
||||
self.window.wl_surface().damage_buffer(0, 0, self.width as i32, self.height as i32);
|
||||
|
||||
// Request our next frame
|
||||
self.window.wl_surface().frame(qh, self.window.wl_surface().clone());
|
||||
|
||||
// Attach and commit to present.
|
||||
buffer.attach_to(self.window.wl_surface()).expect("buffer attach");
|
||||
self.window.commit();
|
||||
}
|
||||
}
|
||||
|
||||
delegate_compositor!(SimpleWindow);
|
||||
delegate_output!(SimpleWindow);
|
||||
delegate_shm!(SimpleWindow);
|
||||
|
||||
delegate_seat!(SimpleWindow);
|
||||
delegate_keyboard!(SimpleWindow);
|
||||
|
||||
delegate_xdg_shell!(SimpleWindow);
|
||||
delegate_xdg_window!(SimpleWindow);
|
||||
|
||||
delegate_registry!(SimpleWindow);
|
||||
|
||||
impl ProvidesRegistryState for SimpleWindow {
|
||||
registry_handlers![OutputState, SeatState,];
|
||||
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
rustfmt.toml
16
rustfmt.toml
|
|
@ -1,4 +1,16 @@
|
|||
use_small_heuristics = "Max"
|
||||
format_code_in_doc_comments = true
|
||||
match_block_trailing_comma = true
|
||||
condense_wildcard_suffixes = true
|
||||
use_field_init_shorthand = true
|
||||
normalize_doc_attributes = true
|
||||
overflow_delimited_expr = true
|
||||
imports_granularity = "Module"
|
||||
use_small_heuristics = "Max"
|
||||
normalize_comments = true
|
||||
reorder_impl_items = true
|
||||
use_try_shorthand = true
|
||||
newline_style = "Unix"
|
||||
edition = "2018"
|
||||
format_strings = true
|
||||
wrap_comments = true
|
||||
comment_width = 80
|
||||
edition = "2021"
|
||||
|
|
|
|||
79
src/env.rs
79
src/env.rs
|
|
@ -1,79 +0,0 @@
|
|||
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};
|
||||
use sctk::MissingGlobal;
|
||||
|
||||
/// 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<(), MissingGlobal> {
|
||||
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<(), MissingGlobal>
|
||||
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<(), MissingGlobal> {
|
||||
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,
|
||||
]
|
||||
);
|
||||
24
src/lib.rs
24
src/lib.rs
|
|
@ -1,17 +1,19 @@
|
|||
//! Smithay Clipboard
|
||||
//!
|
||||
//! Provides access to the Wayland clipboard for gui applications. The user should have surface
|
||||
//! around.
|
||||
//! Provides access to the Wayland clipboard for gui applications. The user
|
||||
//! should have surface around.
|
||||
|
||||
#![deny(clippy::all, clippy::if_not_else, clippy::enum_glob_use)]
|
||||
use std::ffi::c_void;
|
||||
use std::io::Result;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::sync::mpsc::{self, Receiver};
|
||||
|
||||
use sctk::reexports::client::Display;
|
||||
use sctk::reexports::calloop::channel::{self, Sender};
|
||||
use sctk::reexports::client::backend::Backend;
|
||||
use sctk::reexports::client::Connection;
|
||||
|
||||
mod env;
|
||||
mod mime;
|
||||
mod state;
|
||||
mod worker;
|
||||
|
||||
/// Access to a Wayland clipboard.
|
||||
|
|
@ -22,24 +24,24 @@ pub struct Clipboard {
|
|||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// Creates new clipboard which will be running on its own thread with its own event queue to
|
||||
/// handle clipboard requests.
|
||||
/// Creates new clipboard which will be running on its own thread with its
|
||||
/// own event queue to handle clipboard requests.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `display` must be a valid `*mut wl_display` pointer, and it must remain
|
||||
/// valid for as long as `Clipboard` object is alive.
|
||||
pub unsafe fn new(display: *mut c_void) -> Self {
|
||||
let display = Display::from_external_display(display as *mut _);
|
||||
let backend = unsafe { Backend::from_foreign_display(display.cast()) };
|
||||
let connection = Connection::from_backend(backend);
|
||||
|
||||
// Create channel to send data to clipboard thread.
|
||||
let (request_sender, clipboard_request_receiver) = mpsc::channel();
|
||||
let (request_sender, rx_chan) = channel::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);
|
||||
let clipboard_thread = worker::spawn(name, connection, rx_chan, clipboard_reply_sender);
|
||||
|
||||
Self { request_receiver, request_sender, clipboard_thread }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/// List of allowed mimes.
|
||||
static ALLOWED_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
|
||||
pub static ALLOWED_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
|
||||
|
||||
/// Mime type supported by clipboard.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
|
|
@ -18,8 +18,8 @@ pub enum MimeType {
|
|||
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`.
|
||||
/// `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] {
|
||||
|
|
|
|||
588
src/state.rs
Normal file
588
src/state.rs
Normal file
|
|
@ -0,0 +1,588 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Error, ErrorKind, Read, Result, Write};
|
||||
use std::mem;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use sctk::data_device_manager::data_device::{DataDevice, DataDeviceHandler};
|
||||
use sctk::data_device_manager::data_offer::{DataOfferError, DataOfferHandler, DragOffer};
|
||||
use sctk::data_device_manager::data_source::{CopyPasteSource, DataSourceHandler};
|
||||
use sctk::data_device_manager::{DataDeviceManagerState, WritePipe};
|
||||
use sctk::primary_selection::device::{PrimarySelectionDevice, PrimarySelectionDeviceHandler};
|
||||
use sctk::primary_selection::selection::{PrimarySelectionSource, PrimarySelectionSourceHandler};
|
||||
use sctk::primary_selection::PrimarySelectionManagerState;
|
||||
use sctk::registry::{ProvidesRegistryState, RegistryState};
|
||||
use sctk::seat::pointer::{PointerData, PointerEvent, PointerEventKind, PointerHandler};
|
||||
use sctk::seat::{Capability, SeatHandler, SeatState};
|
||||
use sctk::{
|
||||
delegate_data_device, delegate_pointer, delegate_primary_selection, delegate_registry,
|
||||
delegate_seat, registry_handlers,
|
||||
};
|
||||
|
||||
use sctk::reexports::calloop::{LoopHandle, PostAction};
|
||||
use sctk::reexports::client::globals::GlobalList;
|
||||
use sctk::reexports::client::protocol::wl_data_device::WlDataDevice;
|
||||
use sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
|
||||
use sctk::reexports::client::protocol::wl_data_source::WlDataSource;
|
||||
use sctk::reexports::client::protocol::wl_keyboard::WlKeyboard;
|
||||
use sctk::reexports::client::protocol::wl_pointer::WlPointer;
|
||||
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
||||
use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use sctk::reexports::protocols::wp::primary_selection::zv1::client::{
|
||||
zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1,
|
||||
zwp_primary_selection_source_v1::ZwpPrimarySelectionSourceV1,
|
||||
};
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
||||
use crate::mime::{normalize_to_lf, MimeType, ALLOWED_MIME_TYPES};
|
||||
|
||||
pub struct State {
|
||||
pub primary_selection_manager_state: Option<PrimarySelectionManagerState>,
|
||||
pub data_device_manager_state: Option<DataDeviceManagerState>,
|
||||
pub reply_tx: Sender<Result<String>>,
|
||||
pub exit: bool,
|
||||
|
||||
registry_state: RegistryState,
|
||||
seat_state: SeatState,
|
||||
|
||||
seats: HashMap<ObjectId, ClipboardSeatState>,
|
||||
/// The latest seat which got an event.
|
||||
latest_seat: Option<ObjectId>,
|
||||
|
||||
loop_handle: LoopHandle<'static, Self>,
|
||||
queue_handle: QueueHandle<Self>,
|
||||
|
||||
primary_sources: Vec<PrimarySelectionSource>,
|
||||
primary_selection_content: Rc<[u8]>,
|
||||
|
||||
data_sources: Vec<CopyPasteSource>,
|
||||
data_selection_content: Rc<[u8]>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
globals: &GlobalList,
|
||||
queue_handle: &QueueHandle<Self>,
|
||||
loop_handle: LoopHandle<'static, Self>,
|
||||
reply_tx: Sender<Result<String>>,
|
||||
) -> Option<Self> {
|
||||
let mut seats = HashMap::new();
|
||||
|
||||
let data_device_manager_state = DataDeviceManagerState::bind(globals, queue_handle).ok();
|
||||
let primary_selection_manager_state =
|
||||
PrimarySelectionManagerState::bind(globals, queue_handle).ok();
|
||||
|
||||
// When both globals are not available nothing could be done.
|
||||
if data_device_manager_state.is_none() && primary_selection_manager_state.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let seat_state = SeatState::new(globals, queue_handle);
|
||||
for seat in seat_state.seats() {
|
||||
seats.insert(seat.id(), Default::default());
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
registry_state: RegistryState::new(globals),
|
||||
primary_selection_content: Rc::from([]),
|
||||
data_selection_content: Rc::from([]),
|
||||
queue_handle: queue_handle.clone(),
|
||||
primary_selection_manager_state,
|
||||
primary_sources: Vec::new(),
|
||||
data_device_manager_state,
|
||||
data_sources: Vec::new(),
|
||||
latest_seat: None,
|
||||
loop_handle,
|
||||
exit: false,
|
||||
seat_state,
|
||||
reply_tx,
|
||||
seats,
|
||||
})
|
||||
}
|
||||
|
||||
/// Store selection for the given target.
|
||||
///
|
||||
/// Selection source is only created when `Some(())` is returned.
|
||||
pub fn store_selection(&mut self, ty: SelectionTarget, contents: String) -> Option<()> {
|
||||
let latest = self.latest_seat.as_ref()?;
|
||||
let seat = self.seats.get_mut(latest)?;
|
||||
|
||||
if !seat.has_focus {
|
||||
return None;
|
||||
}
|
||||
|
||||
let contents = Rc::from(contents.into_bytes());
|
||||
|
||||
match ty {
|
||||
SelectionTarget::Clipboard => {
|
||||
let mgr = self.data_device_manager_state.as_ref()?;
|
||||
self.data_selection_content = contents;
|
||||
let source =
|
||||
mgr.create_copy_paste_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter());
|
||||
source.set_selection(seat.data_device.as_ref().unwrap(), seat.latest_serial);
|
||||
self.data_sources.push(source);
|
||||
},
|
||||
SelectionTarget::Primary => {
|
||||
let mgr = self.primary_selection_manager_state.as_ref()?;
|
||||
self.primary_selection_content = contents;
|
||||
let source =
|
||||
mgr.create_selection_source(&self.queue_handle, ALLOWED_MIME_TYPES.iter());
|
||||
source.set_selection(seat.primary_device.as_ref().unwrap(), seat.latest_serial);
|
||||
self.primary_sources.push(source);
|
||||
},
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Load selection for the given target.
|
||||
pub fn load_selection(&mut self, ty: SelectionTarget) -> Result<()> {
|
||||
let latest = self
|
||||
.latest_seat
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "no events received on any seat"))?;
|
||||
let seat = self
|
||||
.seats
|
||||
.get_mut(latest)
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "active seat lost"))?;
|
||||
|
||||
if !seat.has_focus {
|
||||
return Err(Error::new(ErrorKind::Other, "client doesn't have focus"));
|
||||
}
|
||||
|
||||
let (read_pipe, mime_type) = match ty {
|
||||
SelectionTarget::Clipboard => {
|
||||
let selection = seat
|
||||
.data_device
|
||||
.as_ref()
|
||||
.and_then(|data| data.data().selection_offer())
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
||||
|
||||
let mime_type =
|
||||
selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| {
|
||||
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
||||
})?;
|
||||
|
||||
(
|
||||
selection.receive(mime_type.to_string()).map_err(|err| match err {
|
||||
DataOfferError::InvalidReceive => {
|
||||
Error::new(ErrorKind::Other, "offer is not ready yet")
|
||||
},
|
||||
DataOfferError::Io(err) => err,
|
||||
})?,
|
||||
mime_type,
|
||||
)
|
||||
},
|
||||
SelectionTarget::Primary => {
|
||||
let selection = seat
|
||||
.primary_device
|
||||
.as_ref()
|
||||
.and_then(|data| data.data().selection_offer())
|
||||
.ok_or_else(|| Error::new(ErrorKind::Other, "selection is empty"))?;
|
||||
|
||||
let mime_type =
|
||||
selection.with_mime_types(MimeType::find_allowed).ok_or_else(|| {
|
||||
Error::new(ErrorKind::NotFound, "supported mime-type is not found")
|
||||
})?;
|
||||
|
||||
(selection.receive(mime_type.to_string())?, mime_type)
|
||||
},
|
||||
};
|
||||
|
||||
// Mark FD as non-blocking so we won't block ourselves.
|
||||
unsafe {
|
||||
set_non_blocking(read_pipe.as_raw_fd())?;
|
||||
}
|
||||
|
||||
let mut reader_buffer = [0; 4096];
|
||||
let mut content = Vec::new();
|
||||
let _ = self.loop_handle.insert_source(read_pipe, move |_, file, state| {
|
||||
let file = unsafe { file.get_mut() };
|
||||
loop {
|
||||
match file.read(&mut reader_buffer) {
|
||||
Ok(0) => {
|
||||
let utf8 = String::from_utf8_lossy(&content);
|
||||
let content = match utf8 {
|
||||
Cow::Borrowed(_) => {
|
||||
// Don't clone the read data.
|
||||
let mut to_send = Vec::new();
|
||||
mem::swap(&mut content, &mut to_send);
|
||||
String::from_utf8(to_send).unwrap()
|
||||
},
|
||||
Cow::Owned(content) => content,
|
||||
};
|
||||
|
||||
// Post-process the content according to mime type.
|
||||
let content = match mime_type {
|
||||
MimeType::TextPlainUtf8 => normalize_to_lf(content),
|
||||
MimeType::Utf8String => content,
|
||||
};
|
||||
|
||||
let _ = state.reply_tx.send(Ok(content));
|
||||
break PostAction::Remove;
|
||||
},
|
||||
Ok(n) => content.extend_from_slice(&reader_buffer[..n]),
|
||||
Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue,
|
||||
Err(err) => {
|
||||
let _ = state.reply_tx.send(Err(err));
|
||||
break PostAction::Remove;
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_request(&mut self, ty: SelectionTarget, write_pipe: WritePipe, mime: String) {
|
||||
// We can only send strings, so don't do anything with the mime-type.
|
||||
if MimeType::find_allowed(&[mime]).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark FD as non-blocking so we won't block ourselves.
|
||||
unsafe {
|
||||
if set_non_blocking(write_pipe.as_raw_fd()).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't access the content on the state directly, since it could change during
|
||||
// the send.
|
||||
let contents = match ty {
|
||||
SelectionTarget::Clipboard => self.data_selection_content.clone(),
|
||||
SelectionTarget::Primary => self.primary_selection_content.clone(),
|
||||
};
|
||||
|
||||
let mut written = 0;
|
||||
let _ = self.loop_handle.insert_source(write_pipe, move |_, file, _| {
|
||||
let file = unsafe { file.get_mut() };
|
||||
loop {
|
||||
match file.write(&contents[written..]) {
|
||||
Ok(n) if written + n == contents.len() => {
|
||||
written += n;
|
||||
break PostAction::Remove;
|
||||
},
|
||||
Ok(n) => written += n,
|
||||
Err(err) if err.kind() == ErrorKind::WouldBlock => break PostAction::Continue,
|
||||
Err(_) => break PostAction::Remove,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl SeatHandler for State {
|
||||
fn seat_state(&mut self) -> &mut SeatState {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: WlSeat) {
|
||||
self.seats.insert(seat.id(), Default::default());
|
||||
}
|
||||
|
||||
fn new_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
seat: WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
let seat_state = self.seats.get_mut(&seat.id()).unwrap();
|
||||
|
||||
match capability {
|
||||
Capability::Keyboard => {
|
||||
seat_state.keyboard = Some(seat.get_keyboard(qh, seat.id()));
|
||||
|
||||
// Selection sources are tied to the keyboard, so add/remove decives
|
||||
// when we gain/loss capability.
|
||||
|
||||
if seat_state.data_device.is_none() && self.data_device_manager_state.is_some() {
|
||||
seat_state.data_device = self
|
||||
.data_device_manager_state
|
||||
.as_ref()
|
||||
.map(|mgr| mgr.get_data_device(qh, &seat));
|
||||
}
|
||||
|
||||
if seat_state.primary_device.is_none()
|
||||
&& self.primary_selection_manager_state.is_some()
|
||||
{
|
||||
seat_state.primary_device = self
|
||||
.primary_selection_manager_state
|
||||
.as_ref()
|
||||
.map(|mgr| mgr.get_selection_device(qh, &seat));
|
||||
}
|
||||
},
|
||||
Capability::Pointer => {
|
||||
seat_state.pointer = self.seat_state.get_pointer(qh, &seat).ok();
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_capability(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
seat: WlSeat,
|
||||
capability: Capability,
|
||||
) {
|
||||
let seat_state = self.seats.get_mut(&seat.id()).unwrap();
|
||||
match capability {
|
||||
Capability::Keyboard => {
|
||||
seat_state.data_device = None;
|
||||
seat_state.primary_device = None;
|
||||
|
||||
if let Some(keyboard) = seat_state.keyboard.take() {
|
||||
if keyboard.version() >= 3 {
|
||||
keyboard.release()
|
||||
}
|
||||
}
|
||||
},
|
||||
Capability::Pointer => {
|
||||
if let Some(pointer) = seat_state.pointer.take() {
|
||||
if pointer.version() >= 3 {
|
||||
pointer.release()
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, seat: WlSeat) {
|
||||
self.seats.remove(&seat.id());
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerHandler for State {
|
||||
fn pointer_frame(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
pointer: &WlPointer,
|
||||
events: &[PointerEvent],
|
||||
) {
|
||||
let seat = pointer.data::<PointerData>().unwrap().seat();
|
||||
let seat_id = seat.id();
|
||||
let seat_state = match self.seats.get_mut(&seat_id) {
|
||||
Some(seat_state) => seat_state,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut updated_serial = false;
|
||||
for event in events {
|
||||
match event.kind {
|
||||
PointerEventKind::Press { serial, .. }
|
||||
| PointerEventKind::Release { serial, .. } => {
|
||||
updated_serial = true;
|
||||
seat_state.latest_serial = serial;
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Only update the seat we're using when the serial got updated.
|
||||
if updated_serial {
|
||||
self.latest_seat = Some(seat_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DataDeviceHandler for State {
|
||||
fn enter(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
|
||||
|
||||
fn leave(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
|
||||
|
||||
fn motion(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
|
||||
|
||||
fn drop_performed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
|
||||
|
||||
// The selection is finished and ready to be used.
|
||||
fn selection(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {}
|
||||
}
|
||||
|
||||
impl DataSourceHandler for State {
|
||||
fn send_request(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &WlDataSource,
|
||||
mime: String,
|
||||
write_pipe: WritePipe,
|
||||
) {
|
||||
self.send_request(SelectionTarget::Clipboard, write_pipe, mime)
|
||||
}
|
||||
|
||||
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, deleted: &WlDataSource) {
|
||||
self.data_sources.retain(|source| source.inner() != deleted)
|
||||
}
|
||||
|
||||
fn accept_mime(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &WlDataSource,
|
||||
_: Option<String>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
|
||||
|
||||
fn action(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource, _: DndAction) {}
|
||||
|
||||
fn dnd_finished(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {}
|
||||
}
|
||||
|
||||
impl DataOfferHandler for State {
|
||||
fn source_actions(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &mut DragOffer,
|
||||
_: DndAction,
|
||||
) {
|
||||
}
|
||||
|
||||
fn selected_action(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &mut DragOffer,
|
||||
_: DndAction,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl ProvidesRegistryState for State {
|
||||
registry_handlers![SeatState];
|
||||
|
||||
fn registry(&mut self) -> &mut RegistryState {
|
||||
&mut self.registry_state
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimarySelectionDeviceHandler for State {
|
||||
fn selection(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &ZwpPrimarySelectionDeviceV1,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl PrimarySelectionSourceHandler for State {
|
||||
fn send_request(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
_: &ZwpPrimarySelectionSourceV1,
|
||||
mime: String,
|
||||
write_pipe: WritePipe,
|
||||
) {
|
||||
self.send_request(SelectionTarget::Primary, write_pipe, mime);
|
||||
}
|
||||
|
||||
fn cancelled(
|
||||
&mut self,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
deleted: &ZwpPrimarySelectionSourceV1,
|
||||
) {
|
||||
self.primary_sources.retain(|source| source.inner() != deleted)
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<WlKeyboard, ObjectId, State> for State {
|
||||
fn event(
|
||||
state: &mut State,
|
||||
_: &WlKeyboard,
|
||||
event: <WlKeyboard as sctk::reexports::client::Proxy>::Event,
|
||||
data: &ObjectId,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<State>,
|
||||
) {
|
||||
use sctk::reexports::client::protocol::wl_keyboard::Event as WlKeyboardEvent;
|
||||
let seat_state = match state.seats.get_mut(data) {
|
||||
Some(seat_state) => seat_state,
|
||||
None => return,
|
||||
};
|
||||
match event {
|
||||
WlKeyboardEvent::Key { serial, .. } | WlKeyboardEvent::Modifiers { serial, .. } => {
|
||||
seat_state.latest_serial = serial;
|
||||
state.latest_seat = Some(data.clone());
|
||||
},
|
||||
// NOTE both selections rely on keyboard focus.
|
||||
WlKeyboardEvent::Enter { serial, .. } => {
|
||||
seat_state.latest_serial = serial;
|
||||
seat_state.has_focus = true;
|
||||
},
|
||||
WlKeyboardEvent::Leave { .. } => {
|
||||
seat_state.latest_serial = 0;
|
||||
seat_state.has_focus = false;
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_seat!(State);
|
||||
delegate_pointer!(State);
|
||||
delegate_data_device!(State);
|
||||
delegate_primary_selection!(State);
|
||||
delegate_registry!(State);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SelectionTarget {
|
||||
/// The target is clipboard selection.
|
||||
Clipboard,
|
||||
/// The target is primary selection.
|
||||
Primary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ClipboardSeatState {
|
||||
keyboard: Option<WlKeyboard>,
|
||||
pointer: Option<WlPointer>,
|
||||
data_device: Option<DataDevice>,
|
||||
primary_device: Option<PrimarySelectionDevice>,
|
||||
has_focus: bool,
|
||||
|
||||
/// The latest serial used to set the selection content.
|
||||
latest_serial: u32,
|
||||
}
|
||||
|
||||
impl Drop for ClipboardSeatState {
|
||||
fn drop(&mut self) {
|
||||
if let Some(keyboard) = self.keyboard.take() {
|
||||
if keyboard.version() >= 3 {
|
||||
keyboard.release();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pointer) = self.pointer.take() {
|
||||
if pointer.version() >= 3 {
|
||||
pointer.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn set_non_blocking(raw_fd: RawFd) -> std::io::Result<()> {
|
||||
let flags = libc::fcntl(raw_fd, libc::F_GETFL);
|
||||
|
||||
if flags < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let result = libc::fcntl(raw_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
|
||||
if result < 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
104
src/worker.rs
Normal file
104
src/worker.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use std::io::{Error, ErrorKind, Result};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use sctk::reexports::calloop::channel::Channel;
|
||||
use sctk::reexports::calloop::{channel, EventLoop};
|
||||
use sctk::reexports::calloop_wayland_source::WaylandSource;
|
||||
use sctk::reexports::client::globals::registry_queue_init;
|
||||
use sctk::reexports::client::Connection;
|
||||
|
||||
use crate::state::{SelectionTarget, State};
|
||||
|
||||
/// Spawn a clipboard worker, which dispatches its own `EventQueue` and handles
|
||||
/// clipboard requests.
|
||||
pub fn spawn(
|
||||
name: String,
|
||||
display: Connection,
|
||||
rx_chan: Channel<Command>,
|
||||
worker_replier: Sender<Result<String>>,
|
||||
) -> Option<std::thread::JoinHandle<()>> {
|
||||
std::thread::Builder::new()
|
||||
.name(name)
|
||||
.spawn(move || {
|
||||
worker_impl(display, rx_chan, 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(
|
||||
connection: Connection,
|
||||
rx_chan: Channel<Command>,
|
||||
reply_tx: Sender<Result<String>>,
|
||||
) {
|
||||
let (globals, event_queue) = match registry_queue_init(&connection) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut event_loop = EventLoop::<State>::try_new().unwrap();
|
||||
let loop_handle = event_loop.handle();
|
||||
|
||||
let mut state = match State::new(&globals, &event_queue.handle(), loop_handle.clone(), reply_tx)
|
||||
{
|
||||
Some(state) => state,
|
||||
None => return,
|
||||
};
|
||||
|
||||
loop_handle
|
||||
.insert_source(rx_chan, |event, _, state| {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
match event {
|
||||
Command::StorePrimary(contents) => {
|
||||
state.store_selection(SelectionTarget::Primary, contents);
|
||||
},
|
||||
Command::Store(contents) => {
|
||||
state.store_selection(SelectionTarget::Clipboard, contents);
|
||||
},
|
||||
Command::Load if state.data_device_manager_state.is_some() => {
|
||||
if let Err(err) = state.load_selection(SelectionTarget::Clipboard) {
|
||||
let _ = state.reply_tx.send(Err(err));
|
||||
}
|
||||
},
|
||||
Command::LoadPrimary if state.data_device_manager_state.is_some() => {
|
||||
if let Err(err) = state.load_selection(SelectionTarget::Primary) {
|
||||
let _ = state.reply_tx.send(Err(err));
|
||||
}
|
||||
},
|
||||
Command::Load | Command::LoadPrimary => {
|
||||
let _ = state.reply_tx.send(Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"requested selection is not supported",
|
||||
)));
|
||||
},
|
||||
Command::Exit => state.exit = true,
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
WaylandSource::new(connection, event_queue).insert(loop_handle).unwrap();
|
||||
|
||||
loop {
|
||||
event_loop.dispatch(None, &mut state).unwrap();
|
||||
|
||||
if state.exit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::slice::IterMut;
|
||||
|
||||
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
||||
|
||||
use super::seat_data::SeatData;
|
||||
|
||||
/// Data to track latest seat and serial for clipboard requests.
|
||||
pub struct ClipboardDispatchData {
|
||||
/// Seats that our application encountered. The first seat is the latest one we've encountered.
|
||||
observed_seats: VecDeque<(WlSeat, u32)>,
|
||||
|
||||
/// All the seats that were advertised.
|
||||
seats: Vec<SeatData>,
|
||||
}
|
||||
|
||||
impl ClipboardDispatchData {
|
||||
/// Builds new `ClipboardDispatchData` with all fields equal to `None`.
|
||||
pub fn new(seats: Vec<SeatData>) -> Self {
|
||||
Self { observed_seats: Default::default(), seats }
|
||||
}
|
||||
|
||||
/// Returns the requested seat's data or adds a new one.
|
||||
pub fn get_seat_data_or_add(&mut self, seat: WlSeat) -> &mut SeatData {
|
||||
let pos = self.seats.iter().position(|st| st.seat == seat);
|
||||
let index = pos.unwrap_or_else(|| {
|
||||
self.seats.push(SeatData::new(seat, None, None));
|
||||
self.seats.len() - 1
|
||||
});
|
||||
|
||||
&mut self.seats[index]
|
||||
}
|
||||
|
||||
pub fn seats(&mut self) -> IterMut<'_, SeatData> {
|
||||
self.seats.iter_mut()
|
||||
}
|
||||
|
||||
/// Set the last observed seat.
|
||||
pub fn set_last_observed_seat(&mut self, seat: WlSeat, serial: u32) {
|
||||
// Assure each seat exists only once.
|
||||
self.remove_observed_seat(&seat);
|
||||
|
||||
// Add the seat to front, making it the latest observed one.
|
||||
self.observed_seats.push_front((seat, serial));
|
||||
}
|
||||
|
||||
/// Remove the given seat from the observed seats.
|
||||
pub fn remove_observed_seat(&mut self, seat: &WlSeat) {
|
||||
if let Some(pos) = self.observed_seats.iter().position(|st| &st.0 == seat) {
|
||||
self.observed_seats.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the last observed seat and the serial.
|
||||
pub fn last_observed_seat(&self) -> Option<&(WlSeat, u32)> {
|
||||
self.observed_seats.front()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
#![macro_use]
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Result;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
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 ();
|
||||
}
|
||||
};
|
||||
|
||||
// If we fail here, it means that we likely won't be able
|
||||
// to read clipboard anyway, so return and reply to prevent block/crash.
|
||||
if $queue.sync_roundtrip(&mut (), |_, _, _| unreachable!()).is_err() {
|
||||
handlers::reply_error(&$tx, "failed to access clipboard.");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
let result = reader.read_to_string(&mut contents).map(|_| {
|
||||
if mime_type == MimeType::Utf8String {
|
||||
mime::normalize_to_lf(contents)
|
||||
} else {
|
||||
contents
|
||||
}
|
||||
});
|
||||
|
||||
let _ = $tx.send(result);
|
||||
});
|
||||
|
||||
// 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(), MimeType::Utf8String.to_string()],
|
||||
move |event, _| {
|
||||
if let $event_ty::Send { mut pipe, .. } = event {
|
||||
// If we fail to write here, it means that other side closed the pipe, thus
|
||||
// we can't do anything about it.
|
||||
let _ = write!(pipe, "{}", $contents);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
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) {
|
||||
let _ = tx.send(Err(std::io::Error::new(std::io::ErrorKind::Other, description)));
|
||||
}
|
||||
|
||||
/// 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_observed_seat(seat, serial);
|
||||
}
|
||||
PointerEvent::Button { serial, .. } => {
|
||||
dispatch_data.set_last_observed_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_observed_seat(seat, serial);
|
||||
}
|
||||
KeyboardEvent::Key { serial, .. } => {
|
||||
dispatch_data.set_last_observed_seat(seat, serial);
|
||||
}
|
||||
KeyboardEvent::Leave { .. } => {
|
||||
dispatch_data.remove_observed_seat(&seat);
|
||||
}
|
||||
KeyboardEvent::Keymap { fd, .. } => {
|
||||
// Prevent fd leaking.
|
||||
let _ = unsafe { File::from_raw_fd(fd) };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
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 = match Environment::new(&display_proxy, &mut queue, SmithayClipboard::new()) {
|
||||
Ok(env) => env,
|
||||
// We shouldn't crash the application if we've failed to create environment.
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// 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, mut dispatch_data| {
|
||||
let dispatch_data = match dispatch_data.get::<ClipboardDispatchData>() {
|
||||
Some(dispatch_data) => dispatch_data,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let seat_resources = dispatch_data.get_seat_data_or_add(seat.detach());
|
||||
|
||||
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(seats);
|
||||
|
||||
// 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();
|
||||
|
||||
if queue.sync_roundtrip(&mut dispatch_data, |_, _, _| unimplemented!()).is_err()
|
||||
&& (request == Command::LoadPrimary || request == Command::Load)
|
||||
{
|
||||
handlers::reply_error(&reply_tx, "primary clipboard is not available.");
|
||||
break;
|
||||
}
|
||||
|
||||
// Get latest observed seat and serial.
|
||||
let (seat, serial) = match dispatch_data.last_observed_seat() {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
handlers::reply_error(&reply_tx, "no focus on a seat.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
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 = match queue.dispatch_pending(&mut dispatch_data, |_, _, _| {}) {
|
||||
Ok(pending_events) => pending_events,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
// 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_observed_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();
|
||||
}
|
||||
}
|
||||
|
||||
// While everything inside this block is safe, the logic is generally unsafe, since we must
|
||||
// drop every proxy on the current `queue`, since dropping it in multithreaded context
|
||||
// could result in use-after-free in libwayland-client.
|
||||
//
|
||||
// For more see https://gitlab.freedesktop.org/wayland/wayland/-/issues/13.
|
||||
#[allow(unused_unsafe)]
|
||||
unsafe {
|
||||
for seat in dispatch_data.seats() {
|
||||
if let Some(pointer) = seat.pointer.take() {
|
||||
if pointer.as_ref().version() >= 3 {
|
||||
pointer.release();
|
||||
}
|
||||
}
|
||||
if let Some(keyboard) = seat.keyboard.take() {
|
||||
if keyboard.as_ref().version() >= 3 {
|
||||
keyboard.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
std::mem::drop(listener);
|
||||
|
||||
let _ = queue.sync_roundtrip(&mut dispatch_data, |_, _, _| unimplemented!());
|
||||
let _ = queue.display().flush();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
/// 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