diff --git a/src/clipboard/wayland.rs b/src/clipboard/wayland.rs index 64593e9..34d4358 100644 --- a/src/clipboard/wayland.rs +++ b/src/clipboard/wayland.rs @@ -3,13 +3,13 @@ use super::CopyConfig; use super::PasteConfig; use super::mime_type::decide_mime_type; use crate::protocol::SourceData; -use anyhow::{Context, Error, Result, bail}; -use nix::unistd::{pipe, read}; +use anyhow::{Context, Error, Result}; +use nix::unistd::pipe; use std::collections::HashMap; use std::ffi::CString; use std::fs::File; use std::io::Write; -use std::os::fd::AsRawFd; +use wayrs_client::core::ObjectId; use wayrs_client::protocol::wl_seat::WlSeat; use wayrs_client::{Connection, EventCtx, IoMode}; use wayrs_protocols::wlr_data_control_unstable_v1::{ @@ -33,14 +33,20 @@ struct CopyEventState { } struct PasteEventState { - finishied: bool, - result: Option, // Stored offers for selection and primary selection (middle-click paste). offers: HashMap>, + stage: PasteEventStage, config: PasteConfig, } +enum PasteEventStage { + Done, + Err(Error), + CollectingOffers, + GotSelection(ObjectId), +} + impl ClipBackend for WaylandBackend { fn copy(&self, config: CopyConfig) -> Result<()> { copy_wayland(config) @@ -77,26 +83,49 @@ fn paste_wayland(cfg: PasteConfig) -> Result<()> { ); let mut state = PasteEventState { - finishied: false, - result: None, offers: HashMap::new(), + stage: PasteEventStage::CollectingOffers, config: cfg, }; - client.conn.flush(IoMode::Blocking).unwrap(); - loop { - if state.finishied { - break; + let selection_id = loop { + match state.stage { + PasteEventStage::Done => return Ok(()), + PasteEventStage::Err(err) => return Err(err), + PasteEventStage::CollectingOffers => (), + PasteEventStage::GotSelection(id) => break id, } + + client.conn.flush(IoMode::Blocking).unwrap(); client.conn.recv_events(IoMode::Blocking).unwrap(); client.conn.dispatch_events(&mut state); - } + }; + + let (offer, supported_types) = state.offers.get_key_value(&selection_id).unwrap(); - if state.result.is_none() { + // with "-l", list the mime-types and return + if state.config.list_types_only { + for mt in supported_types { + writeln!(state.config.writter, "{mt}")?; + } return Ok(()); } - bail!(state.result.unwrap()); + let mime_type = CString::new(decide_mime_type( + &state.config.expected_mime_type, + supported_types, + )?)?; + + // offer.receive needs a fd to write, we cannot use the stdin since the read side of the + // pipe may close earlier before all data written. + let (pipe_read, pipe_write) = pipe()?; + offer.receive(&mut client.conn, mime_type, pipe_write); + client.conn.flush(IoMode::Blocking)?; + + let mut pipe_read = File::from(pipe_read); + std::io::copy(&mut pipe_read, &mut state.config.writter)?; + + Ok(()) } fn copy_wayland(config: CopyConfig) -> Result<()> { @@ -139,25 +168,6 @@ fn copy_wayland(config: CopyConfig) -> Result<()> { #[allow(clippy::collapsible_match)] fn wl_device_cb_for_paste(ctx: EventCtx) { - macro_rules! unwrap_or_return { - ( $e:expr, $report_error:expr) => { - match $e { - Ok(x) => x, - Err(e) => { - if $report_error { - ctx.state.result = Some(e.into()) - } else { - // Errors like empty clipboard are not real problems - log::error!("{}", e) - } - ctx.state.finishied = true; - ctx.conn.break_dispatch_loop(); - return; - } - } - }; - } - match ctx.event { // Received before Selection or PrimarySelection // Need to request mime-types here @@ -165,11 +175,11 @@ fn wl_device_cb_for_paste(ctx: EventCtx { - match ctx.event { - zwlr_data_control_device_v1::Event::PrimarySelection(_) => { - if !ctx.state.config.use_primary { - return; - } - } - _ => { - if ctx.state.config.use_primary { - return; - } - } - } - if o.is_none() { - log::error!("No data in the clipboard"); - ctx.state.finishied = true; - ctx.conn.break_dispatch_loop(); - return; - } - let obj_id = o.unwrap(); - - let (offer, supported_types) = ctx - .state - .offers - .iter() - .find(|pair| *(pair.0) == obj_id) - .unwrap(); - - // with "-l", list the mime-types and return - if ctx.state.config.list_types_only { - for mt in supported_types { - writeln!(ctx.state.config.writter, "{}", mt).unwrap() - } - ctx.state.finishied = true; - ctx.conn.break_dispatch_loop(); - return; + zwlr_data_control_device_v1::Event::Selection(o) => { + if !ctx.state.config.use_primary { + let Some(obj_id) = o else { + log::error!("No data in the clipboard"); + ctx.state.stage = PasteEventStage::Done; + ctx.conn.break_dispatch_loop(); + return; + }; + ctx.state.stage = PasteEventStage::GotSelection(obj_id); } - - let str = unwrap_or_return!( - decide_mime_type(&ctx.state.config.expected_mime_type, supported_types), - false - ); - let mime_type = unwrap_or_return!(CString::new(str), true); - - // offer.receive needs a fd to write, we cannot use the stdin since the read side of the - // pipe may close earlier before all data written. - let fds = unwrap_or_return!(pipe(), true); - offer.receive(ctx.conn, mime_type, fds.1); - // This looks strange, but it is working. It seems offer.receive is a request but nont a - // blocking call, which needs an extra loop to finish. Maybe a callback needs to be set - // to wait until it is processed, but I have no idea how to do that. - // conn.set_callback_for() doesn't work for the offer here. - ctx.conn.blocking_roundtrip().unwrap(); - let mut buffer = vec![0; 1024 * 4]; - loop { - // Read from the pipe until EOF - let n = unwrap_or_return!(read(fds.0.as_raw_fd(), &mut buffer), true); - if n > 0 { - // Write the content to the destination - unwrap_or_return!(ctx.state.config.writter.write(&buffer[0..n]), true); - } else { - break; - } + } + zwlr_data_control_device_v1::Event::PrimarySelection(o) => { + if ctx.state.config.use_primary { + let Some(obj_id) = o else { + log::error!("No data in the clipboard"); + ctx.state.stage = PasteEventStage::Done; + ctx.conn.break_dispatch_loop(); + return; + }; + ctx.state.stage = PasteEventStage::GotSelection(obj_id); } - - offer.destroy(ctx.conn); - ctx.state.finishied = true; - ctx.conn.break_dispatch_loop(); } zwlr_data_control_device_v1::Event::Finished => { log::debug!("Received 'Finished' event"); - ctx.state.result = Some(Error::msg("The data control object has been destroyed")); - ctx.state.finishied = true; + ctx.state.stage = + PasteEventStage::Err(Error::msg("The data control object has been destroyed")); ctx.conn.break_dispatch_loop(); } _ => unreachable!("Unexpected event for device callback"),