mirror of
https://github.com/rustdesk/qemu-display.git
synced 2025-08-17 08:15:45 +00:00
Add clipboard
This commit is contained in:
parent
d98fa6dd7a
commit
4c29af1595
175
qemu-display-listener/src/clipboard.rs
Normal file
175
qemu-display-listener/src/clipboard.rs
Normal file
@ -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<u8>)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace events mpsc with async traits
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ClipboardEvent {
|
||||||
|
Register,
|
||||||
|
Unregister,
|
||||||
|
Grab {
|
||||||
|
selection: ClipboardSelection,
|
||||||
|
serial: u32,
|
||||||
|
mimes: Vec<String>,
|
||||||
|
},
|
||||||
|
Release {
|
||||||
|
selection: ClipboardSelection,
|
||||||
|
},
|
||||||
|
Request {
|
||||||
|
selection: ClipboardSelection,
|
||||||
|
mimes: Vec<String>,
|
||||||
|
tx: Sender<Result<(String, Vec<u8>)>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct ClipboardListener<E: EventSender<Event = ClipboardEvent>> {
|
||||||
|
tx: E,
|
||||||
|
err: Arc<OnceCell<SendError<ClipboardEvent>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[dbus_interface(name = "org.qemu.Display1.Clipboard")]
|
||||||
|
impl<E: 'static + EventSender<Event = ClipboardEvent>> ClipboardListener<E> {
|
||||||
|
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<String>) {
|
||||||
|
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<String>,
|
||||||
|
) -> zbus::fdo::Result<(String, Vec<u8>)> {
|
||||||
|
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<E: 'static + EventSender<Event = ClipboardEvent>> ClipboardListener<E> {
|
||||||
|
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<OnceCell<SendError<ClipboardEvent>>> {
|
||||||
|
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<Self> {
|
||||||
|
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<glib::Receiver<ClipboardEvent>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ pub enum Error {
|
|||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
Zbus(zbus::Error),
|
Zbus(zbus::Error),
|
||||||
Zvariant(zvariant::Error),
|
Zvariant(zvariant::Error),
|
||||||
|
Failed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@ -15,6 +16,7 @@ impl fmt::Display for Error {
|
|||||||
Error::Io(e) => write!(f, "{}", e),
|
Error::Io(e) => write!(f, "{}", e),
|
||||||
Error::Zbus(e) => write!(f, "{}", e),
|
Error::Zbus(e) => write!(f, "{}", e),
|
||||||
Error::Zvariant(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::Io(e) => Some(e),
|
||||||
Error::Zbus(e) => Some(e),
|
Error::Zbus(e) => Some(e),
|
||||||
Error::Zvariant(e) => Some(e),
|
Error::Zvariant(e) => Some(e),
|
||||||
|
Error::Failed(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,9 @@ pub use vm::*;
|
|||||||
mod audio;
|
mod audio;
|
||||||
pub use audio::*;
|
pub use audio::*;
|
||||||
|
|
||||||
|
mod clipboard;
|
||||||
|
pub use clipboard::*;
|
||||||
|
|
||||||
mod console;
|
mod console;
|
||||||
pub use console::*;
|
pub use console::*;
|
||||||
|
|
||||||
|
187
qemu-rdw/src/clipboard.rs
Normal file
187
qemu-rdw/src/clipboard.rs
Normal file
@ -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<SignalHandlerId>,
|
||||||
|
cb_primary_handler: Option<SignalHandlerId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler {
|
||||||
|
pub async fn new(conn: &zbus::azync::Connection) -> Result<Self, Box<dyn Error>> {
|
||||||
|
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<Cell<u32>>,
|
||||||
|
) -> Option<SignalHandlerId> {
|
||||||
|
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<gdk::Clipboard> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ use qemu_display_listener::Console;
|
|||||||
use zbus::Connection;
|
use zbus::Connection;
|
||||||
|
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod clipboard;
|
||||||
mod display_qemu;
|
mod display_qemu;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -18,6 +19,7 @@ fn main() {
|
|||||||
.into();
|
.into();
|
||||||
|
|
||||||
let audio = std::sync::Arc::new(OnceCell::new());
|
let audio = std::sync::Arc::new(OnceCell::new());
|
||||||
|
let clipboard = std::sync::Arc::new(OnceCell::new());
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
app.connect_activate(move |app| {
|
||||||
let window = gtk::ApplicationWindow::new(app);
|
let window = gtk::ApplicationWindow::new(app);
|
||||||
@ -27,6 +29,7 @@ fn main() {
|
|||||||
|
|
||||||
let conn = conn.clone();
|
let conn = conn.clone();
|
||||||
let audio_clone = audio.clone();
|
let audio_clone = audio.clone();
|
||||||
|
let clipboard_clone = clipboard.clone();
|
||||||
MainContext::default().spawn_local(clone!(@strong window => async move {
|
MainContext::default().spawn_local(clone!(@strong window => async move {
|
||||||
let console = Console::new(&conn, 0).await.expect("Failed to get the QEMU console");
|
let console = Console::new(&conn, 0).await.expect("Failed to get the QEMU console");
|
||||||
let display = display_qemu::DisplayQemu::new(console);
|
let display = display_qemu::DisplayQemu::new(console);
|
||||||
@ -37,6 +40,11 @@ fn main() {
|
|||||||
Err(e) => log::warn!("Failed to setup audio: {}", e),
|
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();
|
window.show();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user