diff --git a/qemu-display-listener/src/clipboard.rs b/qemu-display-listener/src/clipboard.rs new file mode 100644 index 0000000..6f1ccfe --- /dev/null +++ b/qemu-display-listener/src/clipboard.rs @@ -0,0 +1,175 @@ +use once_cell::sync::OnceCell; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::convert::TryFrom; +use std::sync::mpsc::{channel, SendError, Sender}; +use std::sync::Arc; +use zbus::{dbus_interface, dbus_proxy, export::zvariant::ObjectPath}; +use zvariant::derive::Type; + +use crate::{EventSender, Result}; + +#[repr(u32)] +#[derive(Deserialize_repr, Serialize_repr, Type, Debug, Hash, PartialEq, Eq, Clone, Copy)] +pub enum ClipboardSelection { + Clipboard, + Primary, + Secondary, +} + +#[dbus_proxy( + default_service = "org.qemu", + default_path = "/org/qemu/Display1/Clipboard", + interface = "org.qemu.Display1.Clipboard" +)] +pub trait Clipboard { + fn register(&self) -> zbus::Result<()>; + + fn unregister(&self) -> zbus::Result<()>; + + fn grab(&self, selection: ClipboardSelection, serial: u32, mimes: &[&str]) -> zbus::Result<()>; + + fn release(&self, selection: ClipboardSelection) -> zbus::Result<()>; + + fn request( + &self, + selection: ClipboardSelection, + mimes: &[&str], + ) -> zbus::Result<(String, Vec)>; +} + +// TODO: replace events mpsc with async traits +#[derive(Debug)] +pub enum ClipboardEvent { + Register, + Unregister, + Grab { + selection: ClipboardSelection, + serial: u32, + mimes: Vec, + }, + Release { + selection: ClipboardSelection, + }, + Request { + selection: ClipboardSelection, + mimes: Vec, + tx: Sender)>>, + }, +} + +#[derive(Debug)] +pub(crate) struct ClipboardListener> { + tx: E, + err: Arc>>, +} + +#[dbus_interface(name = "org.qemu.Display1.Clipboard")] +impl> ClipboardListener { + fn register(&mut self) { + self.send(ClipboardEvent::Register) + } + + fn unregister(&mut self) { + self.send(ClipboardEvent::Unregister) + } + + fn grab(&mut self, selection: ClipboardSelection, serial: u32, mimes: Vec) { + self.send(ClipboardEvent::Grab { + selection, + serial, + mimes, + }) + } + + fn release(&mut self, selection: ClipboardSelection) { + self.send(ClipboardEvent::Release { selection }) + } + + fn request( + &mut self, + selection: ClipboardSelection, + mimes: Vec, + ) -> zbus::fdo::Result<(String, Vec)> { + let (tx, rx) = channel(); + self.send(ClipboardEvent::Request { + selection, + mimes, + tx, + }); + rx.recv() + .map_err(|e| zbus::fdo::Error::Failed(format!("Request recv failed: {}", e)))? + .map_err(|e| zbus::fdo::Error::Failed(format!("Request failed: {}", e))) + } +} + +impl> ClipboardListener { + pub fn new(tx: E) -> Self { + Self { + tx, + err: Default::default(), + } + } + + fn send(&mut self, event: ClipboardEvent) { + if let Err(e) = self.tx.send_event(event) { + let _ = self.err.set(e); + } + } + + pub fn err(&self) -> Arc>> { + self.err.clone() + } +} + +#[derive(derivative::Derivative)] +#[derivative(Debug)] +pub struct Clipboard { + conn: zbus::azync::Connection, + #[derivative(Debug = "ignore")] + pub proxy: AsyncClipboardProxy<'static>, +} + +impl Clipboard { + pub async fn new(conn: &zbus::azync::Connection) -> Result { + let obj_path = ObjectPath::try_from("/org/qemu/Display1/Clipboard")?; + let proxy = AsyncClipboardProxy::builder(conn) + .path(&obj_path)? + .build_async() + .await?; + Ok(Self { + conn: conn.clone(), + proxy, + }) + } + + pub async fn register(&self) -> Result<()> { + self.proxy.register().await?; + Ok(()) + } +} + +#[cfg(feature = "glib")] +impl Clipboard { + pub async fn glib_listen(&self) -> Result> { + let (tx, rx) = glib::MainContext::channel(glib::source::Priority::default()); + let c = self.conn.clone().into(); + let _thread = std::thread::spawn(move || { + let mut s = zbus::ObjectServer::new(&c); + let listener = ClipboardListener::new(tx); + let err = listener.err(); + s.at("/org/qemu/Display1/Clipboard", listener).unwrap(); + loop { + if let Err(e) = s.try_handle_next() { + eprintln!("Listener DBus error: {}", e); + break; + } + if let Some(e) = err.get() { + eprintln!("Listener channel error: {}", e); + break; + } + } + }); + + Ok(rx) + } +} diff --git a/qemu-display-listener/src/error.rs b/qemu-display-listener/src/error.rs index f5246bf..34ddd51 100644 --- a/qemu-display-listener/src/error.rs +++ b/qemu-display-listener/src/error.rs @@ -7,6 +7,7 @@ pub enum Error { Io(io::Error), Zbus(zbus::Error), Zvariant(zvariant::Error), + Failed(String), } impl fmt::Display for Error { @@ -15,6 +16,7 @@ impl fmt::Display for Error { Error::Io(e) => write!(f, "{}", e), Error::Zbus(e) => write!(f, "{}", e), Error::Zvariant(e) => write!(f, "{}", e), + Error::Failed(e) => write!(f, "{}", e), } } } @@ -25,6 +27,7 @@ impl error::Error for Error { Error::Io(e) => Some(e), Error::Zbus(e) => Some(e), Error::Zvariant(e) => Some(e), + Error::Failed(_) => None, } } } diff --git a/qemu-display-listener/src/lib.rs b/qemu-display-listener/src/lib.rs index 2a7263b..2490c5e 100644 --- a/qemu-display-listener/src/lib.rs +++ b/qemu-display-listener/src/lib.rs @@ -12,6 +12,9 @@ pub use vm::*; mod audio; pub use audio::*; +mod clipboard; +pub use clipboard::*; + mod console; pub use console::*; diff --git a/qemu-rdw/src/clipboard.rs b/qemu-rdw/src/clipboard.rs new file mode 100644 index 0000000..e6f20fb --- /dev/null +++ b/qemu-rdw/src/clipboard.rs @@ -0,0 +1,187 @@ +use std::cell::Cell; +use std::error::Error; +use std::rc::Rc; +use std::result::Result; + +use crate::glib::{self, clone, prelude::*, SignalHandlerId, SourceId}; +use gtk::{gdk, gio, prelude::DisplayExt, prelude::*}; +use qemu_display_listener::{ + self as qdl, AsyncClipboardProxy, Clipboard, ClipboardEvent, ClipboardSelection, +}; + +#[derive(Debug)] +pub struct Handler { + rx: SourceId, + cb_handler: Option, + cb_primary_handler: Option, +} + +impl Handler { + pub async fn new(conn: &zbus::azync::Connection) -> Result> { + let ctxt = Clipboard::new(conn).await?; + + let rx = ctxt + .glib_listen() + .await + .expect("Failed to listen to the clipboard"); + let proxy = ctxt.proxy.clone(); + let serial = Rc::new(Cell::new(0)); + let current_serial = serial.clone(); + let rx = rx.attach(None, move |evt| { + use ClipboardEvent::*; + + log::debug!("Clipboard event: {:?}", evt); + match evt { + Register | Unregister => { + current_serial.set(0); + } + Grab { serial, .. } if serial < current_serial.get() => { + log::debug!("Ignored peer grab: {} < {}", serial, current_serial.get()); + } + Grab { + selection, + serial, + mimes, + } => { + current_serial.set(serial); + if let Some(clipboard) = clipboard_from_selection(selection) { + let m: Vec<_> = mimes.iter().map(|s|s.as_str()).collect(); + let p = proxy.clone(); + let content = rdw::ContentProvider::new(&m, move |mime, stream, prio| { + log::debug!("content-provider-write: {:?}", (mime, stream)); + + let p = p.clone(); + let mime = mime.to_string(); + Some(Box::pin(clone!(@strong stream => @default-return panic!(), async move { + match p.request(selection, &[&mime]).await { + Ok((_, data)) => { + let bytes = glib::Bytes::from(&data); + stream.write_bytes_async_future(&bytes, prio).await.map(|_| ()) + } + Err(e) => { + let err = format!("failed to request clipboard data: {}", e); + log::warn!("{}", err); + Err(glib::Error::new(gio::IOErrorEnum::Failed, &err)) + } + } + }))) + }); + + if let Err(e) = clipboard.set_content(Some(&content)) { + log::warn!("Failed to set clipboard grab: {}", e); + } + } + } + Release { selection } => { + if let Some(clipboard) = clipboard_from_selection(selection) { + // TODO: track if the outside/app changed the clipboard + if let Err(e) = clipboard.set_content(gdk::NONE_CONTENT_PROVIDER) { + log::warn!("Failed to release clipboard: {}", e); + } + } + } + Request { selection, mimes, tx } => { + if let Some(clipboard) = clipboard_from_selection(selection) { + glib::MainContext::default().spawn_local(async move { + let m: Vec<_> = mimes.iter().map(|s|s.as_str()).collect(); + let res = clipboard.read_async_future(&m, glib::Priority::default()).await; + log::debug!("clipboard-read: {}", res.is_ok()); + let reply = match res { + Ok((stream, mime)) => { + let out = gio::MemoryOutputStream::new_resizable(); + let res = out.splice_async_future( + &stream, + gio::OutputStreamSpliceFlags::CLOSE_SOURCE | gio::OutputStreamSpliceFlags::CLOSE_TARGET, + glib::Priority::default()).await; + match res { + Ok(_) => { + let data = out.steal_as_bytes(); + Ok((mime.to_string(), data.as_ref().to_vec())) + } + Err(e) => { + Err(qdl::Error::Failed(format!("{}", e))) + } + } + } + Err(e) => { + Err(qdl::Error::Failed(format!("{}", e))) + } + }; + let _ = tx.send(reply); + }); + } + } + } + Continue(true) + }); + + let cb_handler = watch_clipboard( + ctxt.proxy.clone(), + ClipboardSelection::Clipboard, + serial.clone(), + ); + let cb_primary_handler = watch_clipboard( + ctxt.proxy.clone(), + ClipboardSelection::Primary, + serial.clone(), + ); + + ctxt.register().await?; + Ok(Self { + rx, + cb_handler, + cb_primary_handler, + }) + } +} + +fn watch_clipboard( + proxy: AsyncClipboardProxy<'static>, + selection: ClipboardSelection, + serial: Rc>, +) -> Option { + let clipboard = match clipboard_from_selection(selection) { + Some(clipboard) => clipboard, + None => return None, + }; + + let id = clipboard.connect_changed(move |clipboard| { + if clipboard.is_local() { + return; + } + + if let Some(formats) = clipboard.formats() { + let types = formats.mime_types(); + log::debug!(">clipboard-changed({:?}): {:?}", selection, types); + let proxy_clone = proxy.clone(); + let serial = serial.clone(); + glib::MainContext::default().spawn_local(async move { + if types.is_empty() { + let _ = proxy_clone.release(selection).await; + } else { + let mimes: Vec<_> = types.iter().map(|s| s.as_str()).collect(); + let ser = serial.get(); + let _ = proxy_clone.grab(selection, ser, &mimes).await; + serial.set(ser + 1); + } + }); + } + }); + Some(id) +} + +fn clipboard_from_selection(selection: ClipboardSelection) -> Option { + let display = match gdk::Display::default() { + Some(display) => display, + None => return None, + }; + + match selection { + ClipboardSelection::Clipboard => Some(display.clipboard()), + ClipboardSelection::Primary => Some(display.primary_clipboard()), + _ => { + log::warn!("Unsupport clipboard selection: {:?}", selection); + None + } + } +} diff --git a/qemu-rdw/src/main.rs b/qemu-rdw/src/main.rs index 7e6d6fb..f749291 100644 --- a/qemu-rdw/src/main.rs +++ b/qemu-rdw/src/main.rs @@ -6,6 +6,7 @@ use qemu_display_listener::Console; use zbus::Connection; mod audio; +mod clipboard; mod display_qemu; fn main() { @@ -18,6 +19,7 @@ fn main() { .into(); let audio = std::sync::Arc::new(OnceCell::new()); + let clipboard = std::sync::Arc::new(OnceCell::new()); app.connect_activate(move |app| { let window = gtk::ApplicationWindow::new(app); @@ -27,6 +29,7 @@ fn main() { let conn = conn.clone(); let audio_clone = audio.clone(); + let clipboard_clone = clipboard.clone(); MainContext::default().spawn_local(clone!(@strong window => async move { let console = Console::new(&conn, 0).await.expect("Failed to get the QEMU console"); let display = display_qemu::DisplayQemu::new(console); @@ -37,6 +40,11 @@ fn main() { Err(e) => log::warn!("Failed to setup audio: {}", e), } + match clipboard::Handler::new(&conn).await { + Ok(handler) => clipboard_clone.set(handler).unwrap(), + Err(e) => log::warn!("Failed to setup clipboard: {}", e), + } + window.show(); })); });