diff --git a/src/client.rs b/src/client.rs index 3a59b4b4b..c3fe10688 100644 --- a/src/client.rs +++ b/src/client.rs @@ -64,11 +64,11 @@ use crate::{ ui_session_interface::{InvokeUiSession, Session}, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{check_clipboard, CLIPBOARD_INTERVAL}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_session_interface::SessionPermissionConfig; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::{check_clipboard, CLIPBOARD_INTERVAL}; pub use super::lang::*; @@ -136,7 +136,7 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); - static ref OLD_CLIPBOARD_DATA: Arc> = Default::default(); + static ref OLD_CLIPBOARD_DATA: Arc> = Default::default(); static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } @@ -144,7 +144,7 @@ const PUBLIC_SERVER: &str = "public"; #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_old_clipboard_text() -> Arc> { +pub fn get_old_clipboard_text() -> Arc> { OLD_CLIPBOARD_DATA.clone() } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 98dcaaad7..19074bbd1 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -41,7 +41,7 @@ use crate::client::{ new_voice_call_request, Client, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{self, update_clipboard}; +use crate::clipboard::{update_clipboard, CLIPBOARD_INTERVAL}; use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; #[cfg(not(any(target_os = "ios")))] @@ -1135,7 +1135,7 @@ impl Remote { // To make sure current text clipboard data is updated. #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(mut rx) = rx { - timeout(common::CLIPBOARD_INTERVAL, rx.recv()).await.ok(); + timeout(CLIPBOARD_INTERVAL, rx.recv()).await.ok(); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1144,7 +1144,7 @@ impl Remote { let permission_config = self.handler.get_permission_config(); tokio::spawn(async move { // due to clipboard service interval time - sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + sleep(CLIPBOARD_INTERVAL as f32 / 1_000.).await; if permission_config.is_text_clipboard_required() { sender.send(Data::Message(msg_out)).ok(); } diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 000000000..f86dc355c --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,409 @@ +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, Mutex, +}; + +use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; +use hbb_common::{ + ResultType, + allow_err, + compress::{compress as compress_func, decompress}, + log, + message_proto::*, +}; + +pub const CLIPBOARD_NAME: &'static str = "clipboard"; +pub const CLIPBOARD_INTERVAL: u64 = 333; + +lazy_static::lazy_static! { + pub static ref CONTENT: Arc> = Default::default(); + static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +static X11_CLIPBOARD: once_cell::sync::OnceCell = + once_cell::sync::OnceCell::new(); + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> { + X11_CLIPBOARD + .get_or_try_init(|| x11_clipboard::Clipboard::new()) + .map_err(|e| e.to_string()) +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +pub struct ClipboardContext { + string_setter: x11rb::protocol::xproto::Atom, + string_getter: x11rb::protocol::xproto::Atom, + text_uri_list: x11rb::protocol::xproto::Atom, + + clip: x11rb::protocol::xproto::Atom, + prop: x11rb::protocol::xproto::Atom, +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +fn parse_plain_uri_list(v: Vec) -> Result { + let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?; + let mut list = String::new(); + for line in text.lines() { + if !line.starts_with("file://") { + continue; + } + let decoded = percent_encoding::percent_decode_str(line) + .decode_utf8() + .map_err(|_| "ConversionFailure".to_owned())?; + list = list + "\n" + decoded.trim_start_matches("file://"); + } + list = list.trim().to_owned(); + Ok(list) +} + +#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] +impl ClipboardContext { + pub fn new() -> Result { + let clipboard = get_clipboard()?; + let string_getter = clipboard + .getter + .get_atom("UTF8_STRING") + .map_err(|e| e.to_string())?; + let string_setter = clipboard + .setter + .get_atom("UTF8_STRING") + .map_err(|e| e.to_string())?; + let text_uri_list = clipboard + .getter + .get_atom("text/uri-list") + .map_err(|e| e.to_string())?; + let prop = clipboard.getter.atoms.property; + let clip = clipboard.getter.atoms.clipboard; + Ok(Self { + text_uri_list, + string_setter, + string_getter, + clip, + prop, + }) + } + + pub fn get_text(&mut self) -> Result { + let clip = self.clip; + let prop = self.prop; + + const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120); + + let text_content = get_clipboard()? + .load(clip, self.string_getter, prop, TIMEOUT) + .map_err(|e| e.to_string())?; + + let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT); + + if file_urls.is_err() || file_urls.as_ref().unwrap().is_empty() { + log::trace!("clipboard get text, no file urls"); + return String::from_utf8(text_content).map_err(|e| e.to_string()); + } + + let file_urls = parse_plain_uri_list(file_urls.unwrap())?; + + let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?; + + if text_content.trim() == file_urls.trim() { + log::trace!("clipboard got text but polluted"); + return Err(String::from("polluted text")); + } + + Ok(text_content) + } + + pub fn set_text(&mut self, content: String) -> Result<(), String> { + let clip = self.clip; + + let value = content.clone().into_bytes(); + get_clipboard()? + .store(clip, self.string_setter, value) + .map_err(|e| e.to_string())?; + Ok(()) + } +} + +pub fn check_clipboard( + ctx: &mut Option, + old: Option>>, +) -> Option { + if ctx.is_none() { + *ctx = ClipboardContext::new(true).ok(); + } + let ctx2 = ctx.as_mut()?; + let side = if old.is_none() { "host" } else { "client" }; + let old = if let Some(old) = old { + old + } else { + CONTENT.clone() + }; + let content = ctx2.get(); + if let Ok(content) = content { + if !content.is_empty() { + let changed = content != *old.lock().unwrap(); + if changed { + log::info!("{} update found on {}", CLIPBOARD_NAME, side); + let msg = content.create_msg(); + *old.lock().unwrap() = content; + return Some(msg); + } + } + } + None +} + +fn update_clipboard_(clipboard: Clipboard, old: Option>>) { + let content = ClipboardData::from_msg(clipboard); + if content.is_empty() { + return; + } + match ClipboardContext::new(false) { + Ok(mut ctx) => { + let side = if old.is_none() { "host" } else { "client" }; + let old = if let Some(old) = old { + old + } else { + CONTENT.clone() + }; + allow_err!(ctx.set(&content)); + *old.lock().unwrap() = content; + log::debug!("{} updated on {}", CLIPBOARD_NAME, side); + } + Err(err) => { + log::error!("Failed to create clipboard context: {}", err); + } + } +} + +pub fn update_clipboard(clipboard: Clipboard, old: Option>>) { + std::thread::spawn(move || { + update_clipboard_(clipboard, old); + }); +} + +#[derive(Clone)] +pub enum ClipboardData { + Text(String), + Image(arboard::ImageData<'static>, u64), + Empty, +} + +impl Default for ClipboardData { + fn default() -> Self { + ClipboardData::Empty + } +} + +impl ClipboardData { + fn image(image: arboard::ImageData<'static>) -> ClipboardData { + let hash = 0; + /* + use std::hash::{DefaultHasher, Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + image.bytes.hash(&mut hasher); + let hash = hasher.finish(); + */ + ClipboardData::Image(image, hash) + } + + pub fn is_empty(&self) -> bool { + match self { + ClipboardData::Empty => true, + ClipboardData::Text(s) => s.is_empty(), + ClipboardData::Image(a, _) => a.bytes.is_empty(), + _ => false, + } + } + + fn from_msg(clipboard: Clipboard) -> Self { + let data = if clipboard.compress { + decompress(&clipboard.content) + } else { + clipboard.content.into() + }; + if clipboard.width > 0 && clipboard.height > 0 { + ClipboardData::Image( + arboard::ImageData { + bytes: data.into(), + width: clipboard.width as _, + height: clipboard.height as _, + }, + 0, + ) + } else { + if let Ok(content) = String::from_utf8(data) { + ClipboardData::Text(content) + } else { + ClipboardData::Empty + } + } + } + + pub fn create_msg(&self) -> Message { + let mut msg = Message::new(); + + match self { + ClipboardData::Text(s) => { + let compressed = compress_func(s.as_bytes()); + let compress = compressed.len() < s.as_bytes().len(); + let content = if compress { + compressed + } else { + s.clone().into_bytes() + }; + msg.set_clipboard(Clipboard { + compress, + content: content.into(), + ..Default::default() + }); + } + ClipboardData::Image(a, _) => { + let compressed = compress_func(&a.bytes); + let compress = compressed.len() < a.bytes.len(); + let content = if compress { + compressed + } else { + a.bytes.to_vec() + }; + msg.set_clipboard(Clipboard { + compress, + content: content.into(), + width: a.width as _, + height: a.height as _, + ..Default::default() + }); + } + _ => {} + } + msg + } +} + +impl PartialEq for ClipboardData { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (ClipboardData::Text(a), ClipboardData::Text(b)) => a == b, + (ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => { + a.width == b.width && a.height == b.height && a.bytes == b.bytes + } + (ClipboardData::Empty, ClipboardData::Empty) => true, + _ => false, + } + } +} + +#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] +pub struct ClipboardContext(arboard::Clipboard, (Arc, u64), Option); + +#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] +#[allow(unreachable_code)] +impl ClipboardContext { + pub fn new(listen: bool) -> ResultType { + let board; + #[cfg(not(target_os = "linux"))] + { + board = arboard::Clipboard::new()?; + } + #[cfg(target_os = "linux")] + { + let mut i = 1; + loop { + // Try 5 times to create clipboard + // Arboard::new() connect to X server or Wayland compositor, which shoud be ok at most time + // But sometimes, the connection may fail, so we retry here. + match arboard::Clipboard::new() { + Ok(x) => { + board = x; + break; + } + Err(e) => { + if i == 5 { + return Err(e.into()); + } else { + std::thread::sleep(std::time::Duration::from_millis(30 * i)); + } + } + } + i += 1; + } + } + + // starting from 1 so that we can always get initial clipboard data no matter if change + let change_count: Arc = Arc::new(AtomicU64::new(1)); + let mut shutdown = None; + if listen { + struct Handler(Arc); + impl ClipboardHandler for Handler { + fn on_clipboard_change(&mut self) -> CallbackResult { + self.0.fetch_add(1, Ordering::SeqCst); + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: std::io::Error) -> CallbackResult { + log::trace!("Error of clipboard listener: {}", error); + CallbackResult::Next + } + } + match Master::new(Handler(change_count.clone())) { + Ok(master) => { + let mut master = master; + shutdown = Some(master.shutdown_channel()); + std::thread::spawn(move || { + log::debug!("Clipboard listener started"); + if let Err(err) = master.run() { + log::error!("Failed to run clipboard listener: {}", err); + } else { + log::debug!("Clipboard listener stopped"); + } + }); + } + Err(err) => { + log::error!("Failed to create clipboard listener: {}", err); + } + } + } + Ok(ClipboardContext(board, (change_count, 0), shutdown)) + } + + #[inline] + pub fn change_count(&self) -> u64 { + debug_assert!(self.2.is_some()); + self.1 .0.load(Ordering::SeqCst) + } + + pub fn get(&mut self) -> ResultType { + let cn = self.change_count(); + let _lock = ARBOARD_MTX.lock().unwrap(); + // only for image for the time being, + // because I do not want to change behavior of text clipboard for the time being + if cn != self.1 .1 { + self.1 .1 = cn; + if let Ok(image) = self.0.get_image() { + if image.width > 0 && image.height > 0 { + return Ok(ClipboardData::image(image)); + } + } + } + Ok(ClipboardData::Text(self.0.get_text()?)) + } + + fn set(&mut self, data: &ClipboardData) -> ResultType<()> { + let _lock = ARBOARD_MTX.lock().unwrap(); + match data { + ClipboardData::Text(s) => self.0.set_text(s)?, + ClipboardData::Image(a, _) => self.0.set_image(a.clone())?, + _ => {} + } + Ok(()) + } +} + +impl Drop for ClipboardContext { + fn drop(&mut self) { + if let Some(shutdown) = self.2.take() { + let _ = shutdown.signal(); + } + } +} diff --git a/src/common.rs b/src/common.rs index 6e90a73c1..fcc65834b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,139 +1,19 @@ use std::{ collections::HashMap, future::Future, - sync::{ - atomic::{AtomicU64, Ordering}, - Arc, Mutex, RwLock, - }, + sync::{Arc, Mutex, RwLock}, task::Poll, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; use serde_json::Value; -#[derive(Debug, Eq, PartialEq)] -pub enum GrabState { - Ready, - Run, - Wait, - Exit, -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -static X11_CLIPBOARD: once_cell::sync::OnceCell = - once_cell::sync::OnceCell::new(); - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> { - X11_CLIPBOARD - .get_or_try_init(|| x11_clipboard::Clipboard::new()) - .map_err(|e| e.to_string()) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -pub struct ClipboardContext { - string_setter: x11rb::protocol::xproto::Atom, - string_getter: x11rb::protocol::xproto::Atom, - text_uri_list: x11rb::protocol::xproto::Atom, - - clip: x11rb::protocol::xproto::Atom, - prop: x11rb::protocol::xproto::Atom, -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn parse_plain_uri_list(v: Vec) -> Result { - let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?; - let mut list = String::new(); - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = percent_encoding::percent_decode_str(line) - .decode_utf8() - .map_err(|_| "ConversionFailure".to_owned())?; - list = list + "\n" + decoded.trim_start_matches("file://"); - } - list = list.trim().to_owned(); - Ok(list) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -impl ClipboardContext { - pub fn new() -> Result { - let clipboard = get_clipboard()?; - let string_getter = clipboard - .getter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let string_setter = clipboard - .setter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let text_uri_list = clipboard - .getter - .get_atom("text/uri-list") - .map_err(|e| e.to_string())?; - let prop = clipboard.getter.atoms.property; - let clip = clipboard.getter.atoms.clipboard; - Ok(Self { - text_uri_list, - string_setter, - string_getter, - clip, - prop, - }) - } - - pub fn get_text(&mut self) -> Result { - let clip = self.clip; - let prop = self.prop; - - const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120); - - let text_content = get_clipboard()? - .load(clip, self.string_getter, prop, TIMEOUT) - .map_err(|e| e.to_string())?; - - let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT); - - if file_urls.is_err() || file_urls.as_ref().unwrap().is_empty() { - log::trace!("clipboard get text, no file urls"); - return String::from_utf8(text_content).map_err(|e| e.to_string()); - } - - let file_urls = parse_plain_uri_list(file_urls.unwrap())?; - - let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?; - - if text_content.trim() == file_urls.trim() { - log::trace!("clipboard got text but polluted"); - return Err(String::from("polluted text")); - } - - Ok(text_content) - } - - pub fn set_text(&mut self, content: String) -> Result<(), String> { - let clip = self.clip; - - let value = content.clone().into_bytes(); - get_clipboard()? - .store(clip, self.string_setter, value) - .map_err(|e| e.to_string())?; - Ok(()) - } -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::compress::decompress; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, bail, base64, bytes::Bytes, - compress::compress as compress_func, - config::{self, Config, CONNECT_TIMEOUT, READ_TIMEOUT}, + config::{self, Config, CONNECT_TIMEOUT, READ_TIMEOUT, RENDEZVOUS_PORT}, + futures::future::join_all, futures_util::future::poll_fn, get_version_number, log, message_proto::*, @@ -149,18 +29,21 @@ use hbb_common::{ }, ResultType, }; -// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] -use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; use crate::{ hbbs_http::create_http_client_async, ui_interface::{get_option, set_option}, }; -pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; +#[derive(Debug, Eq, PartialEq)] +pub enum GrabState { + Ready, + Run, + Wait, + Exit, +} -pub const CLIPBOARD_NAME: &'static str = "clipboard"; -pub const CLIPBOARD_INTERVAL: u64 = 333; +pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; // the executable name of the portable version pub const PORTABLE_APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; @@ -186,11 +69,6 @@ pub mod input { pub const MOUSE_BUTTON_FORWARD: i32 = 0x10; } -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - pub static ref CONTENT: Arc> = Default::default(); -} - lazy_static::lazy_static! { pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); pub static ref DEVICE_ID: Arc> = Default::default(); @@ -205,11 +83,6 @@ lazy_static::lazy_static! { static ref IS_MAIN: bool = std::env::args().nth(1).map_or(true, |arg| !arg.starts_with("--")); } -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); -} - pub struct SimpleCallOnReturn { pub b: bool, pub f: Box, @@ -278,36 +151,6 @@ pub fn valid_for_numlock(evt: &KeyEvent) -> bool { } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn check_clipboard( - ctx: &mut Option, - old: Option>>, -) -> Option { - if ctx.is_none() { - *ctx = ClipboardContext::new(true).ok(); - } - let ctx2 = ctx.as_mut()?; - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old - } else { - CONTENT.clone() - }; - let content = ctx2.get(); - if let Ok(content) = content { - if !content.is_empty() { - let changed = content != *old.lock().unwrap(); - if changed { - log::info!("{} update found on {}", CLIPBOARD_NAME, side); - let msg = content.create_msg(); - *old.lock().unwrap() = content; - return Some(msg); - } - } - } - None -} - /// Set sound input device. pub fn set_sound_input(device: String) { let prior_device = get_option("audio-input".to_owned()); @@ -356,37 +199,6 @@ pub fn get_default_sound_input() -> Option { None } -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn update_clipboard_(clipboard: Clipboard, old: Option>>) { - let content = ClipboardData::from_msg(clipboard); - if content.is_empty() { - return; - } - match ClipboardContext::new(false) { - Ok(mut ctx) => { - let side = if old.is_none() { "host" } else { "client" }; - let old = if let Some(old) = old { - old - } else { - CONTENT.clone() - }; - allow_err!(ctx.set(&content)); - *old.lock().unwrap() = content; - log::debug!("{} updated on {}", CLIPBOARD_NAME, side); - } - Err(err) => { - log::error!("Failed to create clipboard context: {}", err); - } - } -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn update_clipboard(clipboard: Clipboard, old: Option>>) { - std::thread::spawn(move || { - update_clipboard_(clipboard, old); - }); -} - #[cfg(feature = "use_rubato")] pub fn resample_channels( data: &[f32], @@ -1499,244 +1311,6 @@ pub fn rustdesk_interval(i: Interval) -> ThrottledInterval { ThrottledInterval::new(i) } -#[cfg(not(any(target_os = "android", target_os = "ios")))] -#[derive(Clone)] -pub enum ClipboardData { - Text(String), - Image(arboard::ImageData<'static>, u64), - Empty, -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -impl Default for ClipboardData { - fn default() -> Self { - ClipboardData::Empty - } -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -impl ClipboardData { - fn image(image: arboard::ImageData<'static>) -> ClipboardData { - let hash = 0; - /* - use std::hash::{DefaultHasher, Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - image.bytes.hash(&mut hasher); - let hash = hasher.finish(); - */ - ClipboardData::Image(image, hash) - } - - pub fn is_empty(&self) -> bool { - match self { - ClipboardData::Empty => true, - ClipboardData::Text(s) => s.is_empty(), - ClipboardData::Image(a, _) => a.bytes.is_empty(), - _ => false, - } - } - - fn from_msg(clipboard: Clipboard) -> Self { - let data = if clipboard.compress { - decompress(&clipboard.content) - } else { - clipboard.content.into() - }; - if clipboard.width > 0 && clipboard.height > 0 { - ClipboardData::Image( - arboard::ImageData { - bytes: data.into(), - width: clipboard.width as _, - height: clipboard.height as _, - }, - 0, - ) - } else { - if let Ok(content) = String::from_utf8(data) { - ClipboardData::Text(content) - } else { - ClipboardData::Empty - } - } - } - - pub fn create_msg(&self) -> Message { - let mut msg = Message::new(); - - match self { - ClipboardData::Text(s) => { - let compressed = compress_func(s.as_bytes()); - let compress = compressed.len() < s.as_bytes().len(); - let content = if compress { - compressed - } else { - s.clone().into_bytes() - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - ..Default::default() - }); - } - ClipboardData::Image(a, _) => { - let compressed = compress_func(&a.bytes); - let compress = compressed.len() < a.bytes.len(); - let content = if compress { - compressed - } else { - a.bytes.to_vec() - }; - msg.set_clipboard(Clipboard { - compress, - content: content.into(), - width: a.width as _, - height: a.height as _, - ..Default::default() - }); - } - _ => {} - } - msg - } -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -impl PartialEq for ClipboardData { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (ClipboardData::Text(a), ClipboardData::Text(b)) => a == b, - (ClipboardData::Image(a, _), ClipboardData::Image(b, _)) => { - a.width == b.width && a.height == b.height && a.bytes == b.bytes - } - (ClipboardData::Empty, ClipboardData::Empty) => true, - _ => false, - } - } -} - -#[cfg(not(any( - target_os = "android", - target_os = "ios", - all(target_os = "linux", feature = "unix-file-copy-paste") -)))] -pub struct ClipboardContext(arboard::Clipboard, (Arc, u64), Option); - -#[cfg(not(any( - target_os = "android", - target_os = "ios", - all(target_os = "linux", feature = "unix-file-copy-paste") -)))] -#[allow(unreachable_code)] -impl ClipboardContext { - pub fn new(listen: bool) -> ResultType { - let board; - #[cfg(not(target_os = "linux"))] - { - board = arboard::Clipboard::new()?; - } - #[cfg(target_os = "linux")] - { - let mut i = 1; - loop { - // Try 5 times to create clipboard - // Arboard::new() connect to X server or Wayland compositor, which shoud be ok at most time - // But sometimes, the connection may fail, so we retry here. - match arboard::Clipboard::new() { - Ok(x) => { - board = x; - break; - } - Err(e) => { - if i == 5 { - return Err(e.into()); - } else { - std::thread::sleep(std::time::Duration::from_millis(30 * i)); - } - } - } - i += 1; - } - } - - // starting from 1 so that we can always get initial clipboard data no matter if change - let change_count: Arc = Arc::new(AtomicU64::new(1)); - let mut shutdown = None; - if listen { - struct Handler(Arc); - impl ClipboardHandler for Handler { - fn on_clipboard_change(&mut self) -> CallbackResult { - self.0.fetch_add(1, Ordering::SeqCst); - CallbackResult::Next - } - - fn on_clipboard_error(&mut self, error: std::io::Error) -> CallbackResult { - log::trace!("Error of clipboard listener: {}", error); - CallbackResult::Next - } - } - match Master::new(Handler(change_count.clone())) { - Ok(master) => { - let mut master = master; - shutdown = Some(master.shutdown_channel()); - std::thread::spawn(move || { - log::debug!("Clipboard listener started"); - if let Err(err) = master.run() { - log::error!("Failed to run clipboard listener: {}", err); - } else { - log::debug!("Clipboard listener stopped"); - } - }); - } - Err(err) => { - log::error!("Failed to create clipboard listener: {}", err); - } - } - } - Ok(ClipboardContext(board, (change_count, 0), shutdown)) - } - - #[inline] - pub fn change_count(&self) -> u64 { - debug_assert!(self.2.is_some()); - self.1 .0.load(Ordering::SeqCst) - } - - pub fn get(&mut self) -> ResultType { - let cn = self.change_count(); - let _lock = ARBOARD_MTX.lock().unwrap(); - // only for image for the time being, - // because I do not want to change behavior of text clipboard for the time being - if cn != self.1 .1 { - self.1 .1 = cn; - if let Ok(image) = self.0.get_image() { - if image.width > 0 && image.height > 0 { - return Ok(ClipboardData::image(image)); - } - } - } - Ok(ClipboardData::Text(self.0.get_text()?)) - } - - fn set(&mut self, data: &ClipboardData) -> ResultType<()> { - let _lock = ARBOARD_MTX.lock().unwrap(); - match data { - ClipboardData::Text(s) => self.0.set_text(s)?, - ClipboardData::Image(a, _) => self.0.set_image(a.clone())?, - _ => {} - } - Ok(()) - } -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -impl Drop for ClipboardContext { - fn drop(&mut self) { - if let Some(shutdown) = self.2.take() { - let _ = shutdown.signal(); - } - } -} - pub fn load_custom_client() { #[cfg(debug_assertions)] if let Ok(data) = std::fs::read_to_string("./custom.txt") { diff --git a/src/lib.rs b/src/lib.rs index 1794dff9d..f8d917a51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,8 @@ mod custom_server; mod lang; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod clipboard; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 3a782d919..eeeea4999 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,5 +1,5 @@ use super::*; -pub use crate::common::{ +pub use crate::clipboard::{ check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME, CONTENT, }; @@ -38,7 +38,7 @@ fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { sp.send(msg); } sp.snapshot(|sps| { - let data = crate::CONTENT.lock().unwrap().clone(); + let data = CONTENT.lock().unwrap().clone(); if !data.is_empty() { let msg_out = data.create_msg(); sps.send_shared(Arc::new(msg_out)); diff --git a/src/server/connection.rs b/src/server/connection.rs index 30261c8be..b45da7052 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2,7 +2,7 @@ use super::{input_service::*, *}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::update_clipboard; +use crate::clipboard::update_clipboard; #[cfg(target_os = "android")] use crate::keyboard::client::map_key_to_control_key; #[cfg(target_os = "linux")]