1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
//! ## General Induction
//! impl wlr-data-control-unstable-v1, handle the clipboard on sway, hyperland or kde animpl the
//! protocol.
//! You can view the protocol in [wlr-data-control-unstable-v1](https://wayland.app/protocols/wlr-data-control-unstable-v1). Here we simply explain it.
//!
//! This protocol involves there register: WlSeat, ZwlrDataControlManagerV1,
//! ZwlrDataControlDeviceV1, and zwlrDataControlOfferV1, seat is used to create a device, and the
//! device will handle the copy and paste,
//!
//! when you want to use this protocol, you need to init these first, then enter the eventloop, you
//! can view our code, part of `init()`
//!
//! ### Paste
//! Copy is mainly in the part of device dispatch and dataoffer one, there are two road to finished
//! a copy event, this is decided by the time you send the receive request of ZwlrDataControlDeviceV1;
//!
//! #### Road 1
//!
//! * 1. first, the event enter to DataOffer event of zwlrDataControlOfferV1, it will send a
//! zwlrDataControlOfferV1 object, this will include the data message of clipboard, if you send
//! this time, you will not know the mimetype. In this time, the data includes the text selected
//! and copied, here you can pass a file description to receive, and mimetype of TEXT, because at
//! this time you do not know any mimetype of the data
//! * 2. it will enter the event of zwlrDataControlOfferV1, there the mimetype be send, but before
//! , you ignore the mimetype
//! * 3. it enter the selection, follow the document of the protocol, you need to destory the offer,
//! if there is one,
//! * 4. the main loop is end, then you need to run roundtrip, again, for the pipeline finished,
//! then you will receive the text. Note, if in this routine, you need to check the mimetype in the
//! end, because the data in pipeline maybe not text
//!
//! ### Road 2
//! it is similar with Road 1, but send receive request when receive selection event, this time you
//! will receive mimetype. Here you can only receive the data which is by copy
//!
//! ### Copy
//!
//! Paste with wlr-data-control-unstable-v1, need data provider alive, you can make an experiment,
//! if you copy a text from firefox, and kill firefox, you will find, you cannot paste! It is
//! amazing, doesn't it? So the copy event need to keep alive if the data is still available. You
//! will find that if you copy a text with wl-copy, it will always alive in htop, if you view the
//! code, you will find it fork itself, and live in the backend, until you copy another text from
//! other place, it will die.
//!
//! Then the problem is, how to copy the data, and when to kill the progress?
//!
//! Copy event involves ZwlrDataControlDeviceV1 and ZwlrDataControlSourceV1.
//!
//! * 1. if you want to send the data, you need to create a new ZwlrDataControlSourceV1, use the
//! create_data_source function of zwlr_data_control_manager_v1, create a new one, and set the
//! mimetype to it , use `offer` request. You can set muti times,
//! * 2. start a never end loop of blocking_dispatch, but it is not never end loop, it should break
//! when receive cancelled event of ZwlrDataControlSourceV1, this means another data is copied, the
//! progress is not needed anymore
//! * 2.1 in blocking_dispatchs at the begining, you will receive some signals of send, with
//! mimetype and a file description, write the data to the fd, then copy will finished, data
//! will in clipboard
//! * 2.2 when received cancelled, exit the progress
//!
//! A simple example to create a clipboard listener is following:
//!
//! ```rust, no_run
//! use wayland_clipboard_listener::WlClipboardPasteStream;
//! use wayland_clipboard_listener::WlListenType;
//!
//! fn main() {
//! let mut stream = WlClipboardPasteStream::init(WlListenType::ListenOnCopy).unwrap();
//! for context in stream.paste_stream().flatten().flatten() {
//! println!("{context:?}");
//! }
//! }
//!
//! ```
//!
//! A simple example to create a wl-copy is following:
//! ``` rust, no_run
//! use wayland_clipboard_listener::{WlClipboardCopyStream, WlClipboardListenerError};
//! fn main() -> Result<(), WlClipboardListenerError> {
//! let args = std::env::args();
//! if args.len() != 2 {
//! println!("You need to pass a string to it");
//! return Ok(());
//! }
//! let context: &str = &args.last().unwrap();
//! let mut stream = WlClipboardCopyStream::init()?;
//! stream.copy_to_clipboard(context.as_bytes().to_vec(), vec!["TEXT"] ,false)?;
//! Ok(())
//! }
#![allow(clippy::needless_doctest_main)]
mod constvar;
mod dispatch;
use std::io::Read;
use wayland_client::{protocol::wl_seat, Connection, EventQueue};
use wayland_protocols_wlr::data_control::v1::client::{
zwlr_data_control_device_v1, zwlr_data_control_manager_v1,
};
use std::sync::{Arc, Mutex};
use thiserror::Error;
use constvar::{IMAGE, TEXT};
/// listentype
/// if ListenOnHover, it wll be useful for translation apps, but in dispatch, we cannot know the
/// mime_types, it can only handle text
///
/// ListenOnCopy will get the full mimetype, but you should copy to enable the listen,
#[derive(Debug)]
pub enum WlListenType {
ListenOnSelect,
ListenOnCopy,
}
/// Error
/// it describe three kind of error
/// 1. failed when init
/// 2. failed in queue
/// 3. failed in pipereader
#[derive(Error, Debug)]
pub enum WlClipboardListenerError {
#[error("Init Failed")]
InitFailed(String),
#[error("Error during queue")]
QueueError(String),
#[error("PipeError")]
PipeError,
}
/// context
/// here describe two types of context
/// 1. text, just [String]
/// 2. file , with [`Vec<u8>`]
#[derive(Debug)]
pub enum ClipBoardListenContext {
Text(String),
File(Vec<u8>),
}
#[derive(Debug)]
pub struct ClipBoardListenMessage {
pub mime_types: Vec<String>,
pub context: ClipBoardListenContext,
}
/// Paste stream
/// it is used to handle paste event
pub struct WlClipboardPasteStream {
inner: WlClipboardListenerStream,
}
impl WlClipboardPasteStream {
/// init a paste steam, you can use WlListenType::ListenOnSelect to watch the select event
/// It can just listen on text
/// use ListenOnCopy will receive the mimetype, can copy many types
pub fn init(listentype: WlListenType) -> Result<Self, WlClipboardListenerError> {
Ok(Self {
inner: WlClipboardListenerStream::init(listentype)?,
})
}
/// return a steam, to iter
/// ```rust, no_run
/// use wayland_clipboard_listener::WlClipboardPasteStream;
/// use wayland_clipboard_listener::WlListenType;
///
/// let mut stream = WlClipboardPasteStream::init(WlListenType::ListenOnCopy).unwrap();
///
/// for context in stream.paste_stream().flatten() {
/// println!("{context:?}")
/// }
/// ```
pub fn paste_stream(&mut self) -> &mut WlClipboardListenerStream {
&mut self.inner
}
/// just get the clipboard once
pub fn get_clipboard(
&mut self,
) -> Result<Option<ClipBoardListenMessage>, WlClipboardListenerError> {
self.inner.get_clipboard()
}
}
/// copy stream,
/// it can used to make a wl-copy
pub struct WlClipboardCopyStream {
inner: WlClipboardListenerStream,
}
impl WlClipboardCopyStream {
/// init a copy steam, you can use it to copy some files
pub fn init() -> Result<Self, WlClipboardListenerError> {
Ok(Self {
inner: WlClipboardListenerStream::init(WlListenType::ListenOnCopy)?,
})
}
/// it will run a never end loop, to handle the paste event, like what wl-copy do
/// it will live until next copy event happened
/// you need to pass data and if use useprimary to it,
/// if is useprimary, you can use the middle button of mouse to paste
/// Take [primary-selection](https://patchwork.freedesktop.org/patch/257267/) as reference
/// ``` rust, no_run
/// use wayland_clipboard_listener::{WlClipboardCopyStream, WlClipboardListenerError};
/// let args = std::env::args();
/// if args.len() != 2 {
/// println!("You need to pass a string to it");
/// } else {
/// let context: &str = &args.last().unwrap();
/// let mut stream = WlClipboardCopyStream::init().unwrap();
/// stream.copy_to_clipboard(context.as_bytes().to_vec(), vec!["STRING"], false).unwrap();
/// }
///```
pub fn copy_to_clipboard(
&mut self,
data: Vec<u8>,
mimetypes: Vec<&str>,
useprimary: bool,
) -> Result<(), WlClipboardListenerError> {
self.inner.copy_to_clipboard(data, mimetypes, useprimary)
}
}
/// Stream, provide a iter to listen to clipboard
/// Note, the iter will loop very fast, you would better to use thread sleep
/// or iter you self
pub struct WlClipboardListenerStream {
listentype: WlListenType,
seat: Option<wl_seat::WlSeat>,
seat_name: Option<String>,
data_manager: Option<zwlr_data_control_manager_v1::ZwlrDataControlManagerV1>,
data_device: Option<zwlr_data_control_device_v1::ZwlrDataControlDeviceV1>,
mime_types: Vec<String>,
pipereader: Option<os_pipe::PipeReader>,
queue: Option<Arc<Mutex<EventQueue<Self>>>>,
copy_data: Option<Vec<u8>>,
copy_cancelled: bool,
}
impl Iterator for WlClipboardListenerStream {
type Item = Result<Option<ClipBoardListenMessage>, WlClipboardListenerError>;
fn next(&mut self) -> Option<Self::Item> {
Some(self.get_clipboard())
}
}
impl WlClipboardListenerStream {
/// private init
/// to init a stream
fn init(listentype: WlListenType) -> Result<Self, WlClipboardListenerError> {
let conn = Connection::connect_to_env().map_err(|_| {
WlClipboardListenerError::InitFailed("Cannot connect to wayland".to_string())
})?;
let mut event_queue = conn.new_event_queue();
let qhandle = event_queue.handle();
let display = conn.display();
display.get_registry(&qhandle, ());
let mut state = WlClipboardListenerStream {
listentype,
seat: None,
seat_name: None,
data_manager: None,
data_device: None,
mime_types: Vec::new(),
pipereader: None,
queue: None,
copy_data: None,
copy_cancelled: false,
};
event_queue.blocking_dispatch(&mut state).map_err(|e| {
WlClipboardListenerError::InitFailed(format!("Inital dispatch failed: {e}"))
})?;
if !state.device_ready() {
return Err(WlClipboardListenerError::InitFailed(
"Cannot get seat and data manager".to_string(),
));
}
while state.seat_name.is_none() {
event_queue.roundtrip(&mut state).map_err(|_| {
WlClipboardListenerError::InitFailed("Cannot roundtrip during init".to_string())
})?;
}
state.set_data_device(&qhandle);
state.queue = Some(Arc::new(Mutex::new(event_queue)));
Ok(state)
}
/// copy data to stream
/// pass [Vec<u8>] as data
/// now it can just copy text
/// It will always live in the background, so you need to handle it yourself
fn copy_to_clipboard(
&mut self,
data: Vec<u8>,
mimetypes: Vec<&str>,
useprimary: bool,
) -> Result<(), WlClipboardListenerError> {
let eventqh = self.queue.clone().unwrap();
let mut event_queue = eventqh.lock().unwrap();
let qh = event_queue.handle();
let manager = self.data_manager.as_ref().unwrap();
let source = manager.create_data_source(&qh, ());
let device = self.data_device.as_ref().unwrap();
for mimetype in mimetypes {
source.offer(mimetype.to_string());
}
if useprimary {
device.set_primary_selection(Some(&source));
} else {
device.set_selection(Some(&source));
}
self.copy_data = Some(data);
while !self.copy_cancelled {
event_queue
.blocking_dispatch(self)
.map_err(|e| WlClipboardListenerError::QueueError(e.to_string()))?;
}
self.copy_data = None;
self.copy_cancelled = false;
Ok(())
}
/// get data from clipboard for once
/// it is also used in iter
fn get_clipboard(
&mut self,
) -> Result<Option<ClipBoardListenMessage>, WlClipboardListenerError> {
// get queue, start blocking_dispatch for first loop
let queue = self.queue.clone().unwrap();
let mut queue = queue
.lock()
.map_err(|e| WlClipboardListenerError::QueueError(e.to_string()))?;
queue
.blocking_dispatch(self)
.map_err(|e| WlClipboardListenerError::QueueError(e.to_string()))?;
if self.pipereader.is_some() {
// roundtrip to init pipereader
queue
.roundtrip(self)
.map_err(|e| WlClipboardListenerError::QueueError(e.to_string()))?;
let mut read = self.pipereader.as_ref().unwrap();
if self.is_text() {
let mut context = String::new();
read.read_to_string(&mut context)
.map_err(|_| WlClipboardListenerError::PipeError)?;
self.pipereader = None;
let mime_types = self.mime_types.clone();
self.mime_types.clear();
Ok(Some(ClipBoardListenMessage {
mime_types,
context: ClipBoardListenContext::Text(context),
}))
} else {
let mut context = vec![];
read.read_to_end(&mut context)
.map_err(|_| WlClipboardListenerError::PipeError)?;
self.pipereader = None;
let mime_types = self.mime_types.clone();
self.mime_types.clear();
// it is hover type, it will not receive the context
if let WlListenType::ListenOnSelect = self.listentype {
Ok(None)
} else {
Ok(Some(ClipBoardListenMessage {
mime_types,
context: ClipBoardListenContext::File(context),
}))
}
}
} else {
Ok(None)
}
}
fn device_ready(&self) -> bool {
self.seat.is_some() && self.data_manager.is_some()
}
fn set_data_device(&mut self, qh: &wayland_client::QueueHandle<Self>) {
let seat = self.seat.as_ref().unwrap();
let manager = self.data_manager.as_ref().unwrap();
let device = manager.get_data_device(seat, qh, ());
self.data_device = Some(device);
}
fn is_text(&self) -> bool {
!self.mime_types.is_empty()
&& self.mime_types.contains(&TEXT.to_string())
&& !self.mime_types.contains(&IMAGE.to_string())
}
}