From c655a0885437bb53eddd0efd38daa7dce2c2ba9f Mon Sep 17 00:00:00 2001 From: taojiacheng Date: Thu, 7 Mar 2024 16:28:40 +0800 Subject: [PATCH] clear code --- .github/workflows/test.yml | 12 + examples/buffer.rs | 12 +- examples/files.rs | 26 +- examples/helloworld.rs | 26 +- examples/image.rs | 14 +- examples/multi.rs | 32 +- examples/watch_change.rs | 28 +- rustfmt.toml | 4 + src/common.rs | 428 +++++----- src/lib.rs | 72 +- src/platform/macos.rs | 1013 +++++++++++----------- src/platform/mod.rs | 22 +- src/platform/win.rs | 900 ++++++++++---------- src/platform/x11.rs | 1618 ++++++++++++++++++------------------ tests/file_test.rs | 54 +- tests/image_test.rs | 40 +- tests/string_test.rs | 96 +-- 17 files changed, 2199 insertions(+), 2198 deletions(-) create mode 100644 rustfmt.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cda845c..e747de4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,19 @@ on: - "README.md" jobs: + rustfmt: + runs-on: ubuntu-22.04 + steps: + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + components: rustfmt + - uses: actions/checkout@v4 + - name: Check formatting + run: cargo fmt --all -- --check + clippy: + needs: rustfmt runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/examples/buffer.rs b/examples/buffer.rs index 3e1cc22..6522a34 100644 --- a/examples/buffer.rs +++ b/examples/buffer.rs @@ -1,13 +1,13 @@ use clipboard_rs::{Clipboard, ClipboardContext}; fn main() { - let ctx = ClipboardContext::new().unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let ctx = ClipboardContext::new().unwrap(); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let buffer = ctx.get_buffer("public.html").unwrap(); + let buffer = ctx.get_buffer("public.html").unwrap(); - let string = String::from_utf8(buffer).unwrap(); + let string = String::from_utf8(buffer).unwrap(); - println!("{}", string); + println!("{}", string); } diff --git a/examples/files.rs b/examples/files.rs index 2411bb7..a463165 100644 --- a/examples/files.rs +++ b/examples/files.rs @@ -1,22 +1,22 @@ use clipboard_rs::{Clipboard, ClipboardContext, ContentFormat}; fn main() { - let ctx = ClipboardContext::new().unwrap(); + let ctx = ClipboardContext::new().unwrap(); - // change the file paths to your own - // let files = vec![ - // "file:///home/parallels/clipboard-rs/Cargo.toml".to_string(), - // "file:///home/parallels/clipboard-rs/CHANGELOG.md".to_string(), - // ]; + // change the file paths to your own + // let files = vec![ + // "file:///home/parallels/clipboard-rs/Cargo.toml".to_string(), + // "file:///home/parallels/clipboard-rs/CHANGELOG.md".to_string(), + // ]; - // ctx.set_files(files).unwrap(); + // ctx.set_files(files).unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let has = ctx.has(ContentFormat::Files); - println!("has_files={}", has); + let has = ctx.has(ContentFormat::Files); + println!("has_files={}", has); - let files = ctx.get_files().unwrap(); - println!("{:?}", files); + let files = ctx.get_files().unwrap(); + println!("{:?}", files); } diff --git a/examples/helloworld.rs b/examples/helloworld.rs index 99b73e8..12e5f3e 100644 --- a/examples/helloworld.rs +++ b/examples/helloworld.rs @@ -1,25 +1,25 @@ use clipboard_rs::{Clipboard, ClipboardContext, ContentFormat}; fn main() { - let ctx = ClipboardContext::new().unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let ctx = ClipboardContext::new().unwrap(); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let has_rtf = ctx.has(ContentFormat::Rtf); - println!("has_rtf={}", has_rtf); + let has_rtf = ctx.has(ContentFormat::Rtf); + println!("has_rtf={}", has_rtf); - let rtf = ctx.get_rich_text().unwrap(); + let rtf = ctx.get_rich_text().unwrap(); - println!("rtf={}", rtf); + println!("rtf={}", rtf); - let has_html = ctx.has(ContentFormat::Html); - println!("has_html={}", has_html); + let has_html = ctx.has(ContentFormat::Html); + println!("has_html={}", has_html); - let html = ctx.get_html().unwrap(); + let html = ctx.get_html().unwrap(); - println!("html={}", html); + println!("html={}", html); - let content = ctx.get_text().unwrap(); + let content = ctx.get_text().unwrap(); - println!("txt={}", content); + println!("txt={}", content); } diff --git a/examples/image.rs b/examples/image.rs index 2909e1f..a96980c 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -1,15 +1,15 @@ use clipboard_rs::{common::RustImage, Clipboard, ClipboardContext}; fn main() { - let ctx = ClipboardContext::new().unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let ctx = ClipboardContext::new().unwrap(); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let img = ctx.get_image().unwrap(); + let img = ctx.get_image().unwrap(); - img.save_to_path("/tmp/test.png").unwrap(); + img.save_to_path("/tmp/test.png").unwrap(); - let resize_img = img.thumbnail(300, 300).unwrap(); + let resize_img = img.thumbnail(300, 300).unwrap(); - resize_img.save_to_path("/tmp/test_thumbnail.png").unwrap(); + resize_img.save_to_path("/tmp/test_thumbnail.png").unwrap(); } diff --git a/examples/multi.rs b/examples/multi.rs index 75fb933..caae854 100644 --- a/examples/multi.rs +++ b/examples/multi.rs @@ -1,26 +1,26 @@ use clipboard_rs::{ - common::ContentData, Clipboard, ClipboardContent, ClipboardContext, ContentFormat, + common::ContentData, Clipboard, ClipboardContent, ClipboardContext, ContentFormat, }; fn main() { - let ctx = ClipboardContext::new().unwrap(); + let ctx = ClipboardContext::new().unwrap(); - let contents: Vec = vec![ - ClipboardContent::Text("hell@$#%^&U都98好的😊o Rust!!!".to_string()), - ClipboardContent::Rtf("\x1b[1m\x1b[4m\x1b[31mHello, Rust!\x1b[0m".to_string()), - ClipboardContent::Html("

Hello, Rust!

".to_string()), - ]; + let contents: Vec = vec![ + ClipboardContent::Text("hell@$#%^&U都98好的😊o Rust!!!".to_string()), + ClipboardContent::Rtf("\x1b[1m\x1b[4m\x1b[31mHello, Rust!\x1b[0m".to_string()), + ClipboardContent::Html("

Hello, Rust!

".to_string()), + ]; - ctx.set(contents).unwrap(); + ctx.set(contents).unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let read = ctx - .get(&[ContentFormat::Text, ContentFormat::Rtf, ContentFormat::Html]) - .unwrap(); + let read = ctx + .get(&[ContentFormat::Text, ContentFormat::Rtf, ContentFormat::Html]) + .unwrap(); - for c in read { - println!("{}", c.as_str().unwrap()); - } + for c in read { + println!("{}", c.as_str().unwrap()); + } } diff --git a/examples/watch_change.rs b/examples/watch_change.rs index c0359db..a97d8eb 100644 --- a/examples/watch_change.rs +++ b/examples/watch_change.rs @@ -2,22 +2,22 @@ use clipboard_rs::{Clipboard, ClipboardContext, ClipboardWatcher, ClipboardWatch use std::{thread, time::Duration}; fn main() { - let ctx = ClipboardContext::new().unwrap(); - let mut watcher = ClipboardWatcherContext::new().unwrap(); + let ctx = ClipboardContext::new().unwrap(); + let mut watcher = ClipboardWatcherContext::new().unwrap(); - watcher.add_handler(Box::new(move || { - let content = ctx.get_text().unwrap(); - println!("read:{}", content); - })); + watcher.add_handler(Box::new(move || { + let content = ctx.get_text().unwrap(); + println!("read:{}", content); + })); - let watcher_shutdown = watcher.get_shutdown_channel(); + let watcher_shutdown = watcher.get_shutdown_channel(); - thread::spawn(move || { - thread::sleep(Duration::from_secs(5)); - println!("stop watch!"); - watcher_shutdown.stop(); - }); + thread::spawn(move || { + thread::sleep(Duration::from_secs(5)); + println!("stop watch!"); + watcher_shutdown.stop(); + }); - println!("start watch!"); - watcher.start_watch(); + println!("start watch!"); + watcher.start_watch(); } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..9e8a319 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +hard_tabs = true +use_field_init_shorthand = true +use_try_shorthand = true +reorder_imports = true \ No newline at end of file diff --git a/src/common.rs b/src/common.rs index 38d17a3..76b9dff 100644 --- a/src/common.rs +++ b/src/common.rs @@ -6,258 +6,258 @@ pub type Result = std::result::Result; pub trait ContentData { - fn get_format(&self) -> ContentFormat; + fn get_format(&self) -> ContentFormat; - fn as_bytes(&self) -> &[u8]; + fn as_bytes(&self) -> &[u8]; - fn as_str(&self) -> Result<&str>; + fn as_str(&self) -> Result<&str>; } pub enum ClipboardContent { - Text(String), - Rtf(String), - Html(String), - Image(RustImageData), - Files(Vec), - Other(String, Vec), + Text(String), + Rtf(String), + Html(String), + Image(RustImageData), + Files(Vec), + Other(String, Vec), } impl ContentData for ClipboardContent { - fn get_format(&self) -> ContentFormat { - match self { - ClipboardContent::Text(_) => ContentFormat::Text, - ClipboardContent::Rtf(_) => ContentFormat::Rtf, - ClipboardContent::Html(_) => ContentFormat::Html, - ClipboardContent::Image(_) => ContentFormat::Image, - ClipboardContent::Files(_) => ContentFormat::Files, - ClipboardContent::Other(format, _) => ContentFormat::Other(format.clone()), - } - } - - fn as_bytes(&self) -> &[u8] { - match self { - ClipboardContent::Text(data) => data.as_bytes(), - ClipboardContent::Rtf(data) => data.as_bytes(), - ClipboardContent::Html(data) => data.as_bytes(), - ClipboardContent::Image(data) => data.as_bytes(), - ClipboardContent::Files(data) => { - // use first file path as data - if let Some(path) = data.first() { - path.as_bytes() - } else { - &[] - } - } - ClipboardContent::Other(_, data) => data.as_slice(), - } - } - - fn as_str(&self) -> Result<&str> { - match self { - ClipboardContent::Text(data) => Ok(data), - ClipboardContent::Rtf(data) => Ok(data), - ClipboardContent::Html(data) => Ok(data), - ClipboardContent::Image(_) => Err("can't convert image to string".into()), - ClipboardContent::Files(data) => { - // use first file path as data - if let Some(path) = data.first() { - Ok(path) - } else { - Err("content is empty".into()) - } - } - ClipboardContent::Other(_, data) => std::str::from_utf8(data).map_err(|e| e.into()), - } - } + fn get_format(&self) -> ContentFormat { + match self { + ClipboardContent::Text(_) => ContentFormat::Text, + ClipboardContent::Rtf(_) => ContentFormat::Rtf, + ClipboardContent::Html(_) => ContentFormat::Html, + ClipboardContent::Image(_) => ContentFormat::Image, + ClipboardContent::Files(_) => ContentFormat::Files, + ClipboardContent::Other(format, _) => ContentFormat::Other(format.clone()), + } + } + + fn as_bytes(&self) -> &[u8] { + match self { + ClipboardContent::Text(data) => data.as_bytes(), + ClipboardContent::Rtf(data) => data.as_bytes(), + ClipboardContent::Html(data) => data.as_bytes(), + ClipboardContent::Image(data) => data.as_bytes(), + ClipboardContent::Files(data) => { + // use first file path as data + if let Some(path) = data.first() { + path.as_bytes() + } else { + &[] + } + } + ClipboardContent::Other(_, data) => data.as_slice(), + } + } + + fn as_str(&self) -> Result<&str> { + match self { + ClipboardContent::Text(data) => Ok(data), + ClipboardContent::Rtf(data) => Ok(data), + ClipboardContent::Html(data) => Ok(data), + ClipboardContent::Image(_) => Err("can't convert image to string".into()), + ClipboardContent::Files(data) => { + // use first file path as data + if let Some(path) = data.first() { + Ok(path) + } else { + Err("content is empty".into()) + } + } + ClipboardContent::Other(_, data) => std::str::from_utf8(data).map_err(|e| e.into()), + } + } } #[derive(Clone)] pub enum ContentFormat { - Text, - Rtf, - Html, - Image, - Files, - Other(String), + Text, + Rtf, + Html, + Image, + Files, + Other(String), } pub struct RustImageData { - width: u32, - height: u32, - data: Option, + width: u32, + height: u32, + data: Option, } /// 此处的 RustImageBuffer 已经是带有图片格式的字节流,例如 png,jpeg; pub struct RustImageBuffer(Vec); impl RustImageData { - pub fn as_bytes(&self) -> &[u8] { - match &self.data { - Some(image) => image.as_bytes(), - None => &[], - } - } + pub fn as_bytes(&self) -> &[u8] { + match &self.data { + Some(image) => image.as_bytes(), + None => &[], + } + } } pub trait RustImage: Sized { - /// create an empty image - fn empty() -> Self; + /// create an empty image + fn empty() -> Self; - fn is_empty(&self) -> bool; + fn is_empty(&self) -> bool; - /// Read image from file path - fn from_path(path: &str) -> Result; + /// Read image from file path + fn from_path(path: &str) -> Result; - /// Create a new image from a byte slice - fn from_bytes(bytes: &[u8]) -> Result; + /// Create a new image from a byte slice + fn from_bytes(bytes: &[u8]) -> Result; - /// width and height - fn get_size(&self) -> (u32, u32); + /// width and height + fn get_size(&self) -> (u32, u32); - /// Scale this image down to fit within a specific size. - /// Returns a new image. The image's aspect ratio is preserved. - /// The image is scaled to the maximum possible size that fits - /// within the bounds specified by `nwidth` and `nheight`. - /// - /// This method uses a fast integer algorithm where each source - /// pixel contributes to exactly one target pixel. - /// May give aliasing artifacts if new size is close to old size. - fn thumbnail(&self, width: u32, height: u32) -> Result; + /// Scale this image down to fit within a specific size. + /// Returns a new image. The image's aspect ratio is preserved. + /// The image is scaled to the maximum possible size that fits + /// within the bounds specified by `nwidth` and `nheight`. + /// + /// This method uses a fast integer algorithm where each source + /// pixel contributes to exactly one target pixel. + /// May give aliasing artifacts if new size is close to old size. + fn thumbnail(&self, width: u32, height: u32) -> Result; - /// en: Adjust the size of the image without retaining the aspect ratio - /// zh: 调整图片大小,不保留长宽比 - fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result; + /// en: Adjust the size of the image without retaining the aspect ratio + /// zh: 调整图片大小,不保留长宽比 + fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result; - /// en: Convert image to jpeg format, quality is the quality, give a value of 0-100, 100 is the highest quality, - /// the returned image is a new image, and the data itself will not be modified - /// zh: 把图片转为 jpeg 格式,quality(0-100) 为质量,输出字节数组,可直接通过 io 写入文件 - fn to_jpeg(&self, quality: u8) -> Result; + /// en: Convert image to jpeg format, quality is the quality, give a value of 0-100, 100 is the highest quality, + /// the returned image is a new image, and the data itself will not be modified + /// zh: 把图片转为 jpeg 格式,quality(0-100) 为质量,输出字节数组,可直接通过 io 写入文件 + fn to_jpeg(&self, quality: u8) -> Result; - /// en: Convert to png format, the returned image is a new image, and the data itself will not be modified - /// zh: 转为 png 格式,返回的为新的图片,本身数据不会修改 - fn to_png(&self) -> Result; + /// en: Convert to png format, the returned image is a new image, and the data itself will not be modified + /// zh: 转为 png 格式,返回的为新的图片,本身数据不会修改 + fn to_png(&self) -> Result; - fn to_bitmap(&self) -> Result; + fn to_bitmap(&self) -> Result; - fn save_to_path(&self, path: &str) -> Result<()>; + fn save_to_path(&self, path: &str) -> Result<()>; } impl RustImage for RustImageData { - fn empty() -> Self { - RustImageData { - width: 0, - height: 0, - data: None, - } - } - - fn is_empty(&self) -> bool { - self.data.is_none() - } - - fn from_bytes(bytes: &[u8]) -> Result { - let image = image::load_from_memory(bytes)?; - let (width, height) = image.dimensions(); - Ok(RustImageData { - width, - height, - data: Some(image), - }) - } - - fn from_path(path: &str) -> Result { - let image = image::open(path)?; - let (width, height) = image.dimensions(); - Ok(RustImageData { - width, - height, - data: Some(image), - }) - } - - fn get_size(&self) -> (u32, u32) { - (self.width, self.height) - } - - fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result { - match &self.data { - Some(image) => { - let resized = image.resize_exact(width, height, filter); - Ok(RustImageData { - width: resized.width(), - height: resized.height(), - data: Some(resized), - }) - } - None => Err("image is empty".into()), - } - } - - fn to_jpeg(&self, quality: u8) -> Result { - match &self.data { - Some(image) => { - let mut buf = Cursor::new(Vec::new()); - image.write_to(&mut buf, image::ImageOutputFormat::Jpeg(quality))?; - Ok(RustImageBuffer(buf.into_inner())) - } - None => Err("image is empty".into()), - } - } - - fn save_to_path(&self, path: &str) -> Result<()> { - match &self.data { - Some(image) => { - image.save(path)?; - Ok(()) - } - None => Err("image is empty".into()), - } - } - - fn to_png(&self) -> Result { - match &self.data { - Some(image) => { - let mut buf = Cursor::new(Vec::new()); - image.write_to(&mut buf, image::ImageOutputFormat::Png)?; - Ok(RustImageBuffer(buf.into_inner())) - } - None => Err("image is empty".into()), - } - } - - fn thumbnail(&self, width: u32, height: u32) -> Result { - match &self.data { - Some(image) => { - let resized = image.thumbnail(width, height); - Ok(RustImageData { - width: resized.width(), - height: resized.height(), - data: Some(resized), - }) - } - None => Err("image is empty".into()), - } - } - - fn to_bitmap(&self) -> Result { - match &self.data { - Some(image) => { - let mut buf = Cursor::new(Vec::new()); - image.write_to(&mut buf, image::ImageOutputFormat::Bmp)?; - Ok(RustImageBuffer(buf.into_inner())) - } - None => Err("image is empty".into()), - } - } + fn empty() -> Self { + RustImageData { + width: 0, + height: 0, + data: None, + } + } + + fn is_empty(&self) -> bool { + self.data.is_none() + } + + fn from_bytes(bytes: &[u8]) -> Result { + let image = image::load_from_memory(bytes)?; + let (width, height) = image.dimensions(); + Ok(RustImageData { + width, + height, + data: Some(image), + }) + } + + fn from_path(path: &str) -> Result { + let image = image::open(path)?; + let (width, height) = image.dimensions(); + Ok(RustImageData { + width, + height, + data: Some(image), + }) + } + + fn get_size(&self) -> (u32, u32) { + (self.width, self.height) + } + + fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result { + match &self.data { + Some(image) => { + let resized = image.resize_exact(width, height, filter); + Ok(RustImageData { + width: resized.width(), + height: resized.height(), + data: Some(resized), + }) + } + None => Err("image is empty".into()), + } + } + + fn to_jpeg(&self, quality: u8) -> Result { + match &self.data { + Some(image) => { + let mut buf = Cursor::new(Vec::new()); + image.write_to(&mut buf, image::ImageOutputFormat::Jpeg(quality))?; + Ok(RustImageBuffer(buf.into_inner())) + } + None => Err("image is empty".into()), + } + } + + fn save_to_path(&self, path: &str) -> Result<()> { + match &self.data { + Some(image) => { + image.save(path)?; + Ok(()) + } + None => Err("image is empty".into()), + } + } + + fn to_png(&self) -> Result { + match &self.data { + Some(image) => { + let mut buf = Cursor::new(Vec::new()); + image.write_to(&mut buf, image::ImageOutputFormat::Png)?; + Ok(RustImageBuffer(buf.into_inner())) + } + None => Err("image is empty".into()), + } + } + + fn thumbnail(&self, width: u32, height: u32) -> Result { + match &self.data { + Some(image) => { + let resized = image.thumbnail(width, height); + Ok(RustImageData { + width: resized.width(), + height: resized.height(), + data: Some(resized), + }) + } + None => Err("image is empty".into()), + } + } + + fn to_bitmap(&self) -> Result { + match &self.data { + Some(image) => { + let mut buf = Cursor::new(Vec::new()); + image.write_to(&mut buf, image::ImageOutputFormat::Bmp)?; + Ok(RustImageBuffer(buf.into_inner())) + } + None => Err("image is empty".into()), + } + } } impl RustImageBuffer { - pub fn get_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn save_to_path(&self, path: &str) -> Result<()> { - std::fs::write(path, &self.0)?; - Ok(()) - } + pub fn get_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn save_to_path(&self, path: &str) -> Result<()> { + std::fs::write(path, &self.0)?; + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index 213053e..741578f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,64 +4,64 @@ pub use common::{CallBack, ClipboardContent, ContentFormat, Result, RustImageDat pub use image::imageops::FilterType; pub use platform::{ClipboardContext, ClipboardWatcherContext, WatcherShutdown}; pub trait Clipboard: Send { - /// zh: 获得剪切板当前内容的所有格式 - /// en: Get all formats of the current content in the clipboard - fn available_formats(&self) -> Result>; + /// zh: 获得剪切板当前内容的所有格式 + /// en: Get all formats of the current content in the clipboard + fn available_formats(&self) -> Result>; - fn has(&self, format: ContentFormat) -> bool; + fn has(&self, format: ContentFormat) -> bool; - /// zh: 清空剪切板 - /// en: clear clipboard - fn clear(&self) -> Result<()>; + /// zh: 清空剪切板 + /// en: clear clipboard + fn clear(&self) -> Result<()>; - /// zh: 获得指定格式的数据,以字节数组形式返回 - /// en: Get the data in the specified format in the clipboard as a byte array - fn get_buffer(&self, format: &str) -> Result>; + /// zh: 获得指定格式的数据,以字节数组形式返回 + /// en: Get the data in the specified format in the clipboard as a byte array + fn get_buffer(&self, format: &str) -> Result>; - /// zh: 仅获得无格式纯文本,以字符串形式返回 - /// en: Get plain text content in the clipboard as string - fn get_text(&self) -> Result; + /// zh: 仅获得无格式纯文本,以字符串形式返回 + /// en: Get plain text content in the clipboard as string + fn get_text(&self) -> Result; - /// zh: 获得剪贴板中的富文本内容,以字符串形式返回 - /// en: Get the rich text content in the clipboard as string - fn get_rich_text(&self) -> Result; + /// zh: 获得剪贴板中的富文本内容,以字符串形式返回 + /// en: Get the rich text content in the clipboard as string + fn get_rich_text(&self) -> Result; - /// zh: 获得剪贴板中的html内容,以字符串形式返回 - /// en: Get the html format content in the clipboard as string - fn get_html(&self) -> Result; + /// zh: 获得剪贴板中的html内容,以字符串形式返回 + /// en: Get the html format content in the clipboard as string + fn get_html(&self) -> Result; - fn get_image(&self) -> Result; + fn get_image(&self) -> Result; - fn get_files(&self) -> Result>; + fn get_files(&self) -> Result>; - fn get(&self, formats: &[ContentFormat]) -> Result>; + fn get(&self, formats: &[ContentFormat]) -> Result>; - fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()>; + fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()>; - fn set_text(&self, text: String) -> Result<()>; + fn set_text(&self, text: String) -> Result<()>; - fn set_rich_text(&self, text: String) -> Result<()>; + fn set_rich_text(&self, text: String) -> Result<()>; - fn set_html(&self, html: String) -> Result<()>; + fn set_html(&self, html: String) -> Result<()>; - fn set_image(&self, image: RustImageData) -> Result<()>; + fn set_image(&self, image: RustImageData) -> Result<()>; - fn set_files(&self, files: Vec) -> Result<()>; + fn set_files(&self, files: Vec) -> Result<()>; - fn set(&self, contents: Vec) -> Result<()>; + fn set(&self, contents: Vec) -> Result<()>; } pub trait ClipboardWatcher: Send { - fn add_handler(&mut self, f: CallBack) -> &mut Self; + fn add_handler(&mut self, f: CallBack) -> &mut Self; - fn start_watch(&mut self); + fn start_watch(&mut self); - fn get_shutdown_channel(&self) -> WatcherShutdown; + fn get_shutdown_channel(&self) -> WatcherShutdown; } impl WatcherShutdown { - ///Signals shutdown - pub fn stop(self) { - drop(self); - } + ///Signals shutdown + pub fn stop(self) { + drop(self); + } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 12667b4..5fbd87e 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,8 +1,8 @@ use crate::common::{CallBack, ContentData, Result, RustImage, RustImageData}; use crate::{Clipboard, ClipboardContent, ClipboardWatcher, ContentFormat}; use cocoa::appkit::{ - NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypePNG, - NSPasteboardTypeRTF, NSPasteboardTypeString, + NSFilenamesPboardType, NSPasteboard, NSPasteboardTypeHTML, NSPasteboardTypePNG, + NSPasteboardTypeRTF, NSPasteboardTypeString, }; use cocoa::base::{id, nil}; use cocoa::foundation::{NSArray, NSData, NSFastEnumeration, NSString}; @@ -13,560 +13,529 @@ use std::{slice, vec}; const NS_FILES: &str = "public.file-url"; -// required for Send trait because *mut runtime::Object; cannot be sent between threads safely -pub struct SafeId(id); -unsafe impl Send for SafeId {} -unsafe impl Sync for SafeId {} - pub struct ClipboardContext { - clipboard: id, + clipboard: id, } pub struct ClipboardWatcherContext { - clipboard: id, - handlers: Vec, - stop_signal: Sender<()>, - stop_receiver: Receiver<()>, - running: bool, + clipboard: id, + handlers: Vec, + stop_signal: Sender<()>, + stop_receiver: Receiver<()>, + running: bool, } unsafe impl Send for ClipboardWatcherContext {} + impl ClipboardWatcherContext { - pub fn new() -> Result { - let ns_pastboard = unsafe { NSPasteboard::generalPasteboard(nil) }; - let (tx, rx) = mpsc::channel(); - Ok(ClipboardWatcherContext { - clipboard: ns_pastboard, - handlers: Vec::new(), - stop_signal: tx, - stop_receiver: rx, - running: false, - }) - } + pub fn new() -> Result { + let ns_pasteboard = unsafe { NSPasteboard::generalPasteboard(nil) }; + let (tx, rx) = mpsc::channel(); + Ok(ClipboardWatcherContext { + clipboard: ns_pasteboard, + handlers: Vec::new(), + stop_signal: tx, + stop_receiver: rx, + running: false, + }) + } } impl ClipboardWatcher for ClipboardWatcherContext { - fn add_handler(&mut self, f: CallBack) -> &mut Self { - self.handlers.push(f); - self - } - - fn start_watch(&mut self) { - if self.running { - println!("already start watch!"); - return; - } - self.running = true; - let mut last_change_count: i64 = unsafe { self.clipboard.changeCount() }; - loop { - // if receive stop signal, break loop - if self - .stop_receiver - .recv_timeout(Duration::from_millis(500)) - .is_ok() - { - break; - } - let change_count = unsafe { self.clipboard.changeCount() }; - if last_change_count == 0 { - last_change_count = change_count; - } else if change_count != last_change_count { - self.handlers.iter().for_each(|handler| { - handler(); - }); - last_change_count = change_count; - } - } - self.running = false; - } - - fn get_shutdown_channel(&self) -> WatcherShutdown { - WatcherShutdown { - stop_signal: self.stop_signal.clone(), - } - } + fn add_handler(&mut self, f: CallBack) -> &mut Self { + self.handlers.push(f); + self + } + + fn start_watch(&mut self) { + if self.running { + println!("already start watch!"); + return; + } + self.running = true; + let mut last_change_count: i64 = unsafe { self.clipboard.changeCount() }; + loop { + // if receive stop signal, break loop + if self + .stop_receiver + .recv_timeout(Duration::from_millis(500)) + .is_ok() + { + break; + } + let change_count = unsafe { self.clipboard.changeCount() }; + if last_change_count == 0 { + last_change_count = change_count; + } else if change_count != last_change_count { + self.handlers.iter().for_each(|handler| { + handler(); + }); + last_change_count = change_count; + } + } + self.running = false; + } + + fn get_shutdown_channel(&self) -> WatcherShutdown { + WatcherShutdown { + stop_signal: self.stop_signal.clone(), + } + } } impl ClipboardContext { - pub fn new() -> Result { - let ns_pastboard = unsafe { - NSPasteboard::generalPasteboard(nil) - // let format_ns_array = NSArray::arrayWithObjects( - // nil, - // vec![ - // NSPasteboardTypeString, - // NSPasteboardTypeRTF, - // NSPasteboardTypeHTML, - // NSPasteboardTypePNG, - // ] - // .as_ref(), - // ); - // np.declareTypes_owner(format_ns_array, nil); - }; - let clipboard_ctx = ClipboardContext { - clipboard: ns_pastboard, - }; - Ok(clipboard_ctx) - } - - /// Read from clipboard return trait by NSPasteboardItem - fn read_from_clipboard(&self) -> Result> { - let res = unsafe { - let ns_array: id = self.clipboard.pasteboardItems(); - if ns_array.count() == 0 { - return Ok(Vec::new()); - } - ns_array.iter().collect::>() - }; - Ok(res) - } - - // learn from https://github.com/zed-industries/zed/blob/79c1003b344ee513cf97ee8313c38c7c3f02c916/crates/gpui/src/platform/mac/platform.rs#L793 - fn write_to_clipboard(&self, data: &[WriteToClipboardData], with_clear: bool) -> Result<()> { - if with_clear { - unsafe { - self.clipboard.clearContents(); - } - } - data.iter().for_each(|d| unsafe { - let ns_type = match d.format.clone() { - ContentFormat::Text => NSPasteboardTypeString, - ContentFormat::Rtf => NSPasteboardTypeRTF, - ContentFormat::Html => NSPasteboardTypeHTML, - ContentFormat::Image => NSPasteboardTypePNG, - ContentFormat::Files => NSFilenamesPboardType, - ContentFormat::Other(other_format) => { - NSString::alloc(nil).init_str(other_format.as_str()) - } - }; - if let ContentFormat::Other(_) | ContentFormat::Files = d.format { - self.clipboard - .declareTypes_owner(NSArray::arrayWithObject(nil, ns_type), nil); - } - if d.is_multi { - self.clipboard.setPropertyList_forType( - NSArray::arrayByAddingObjectsFromArray(nil, d.data), - ns_type, - ); - } else { - let ns_data = d.data; - self.clipboard.setData_forType(ns_data, ns_type); - } - }); - Ok(()) - } + pub fn new() -> Result { + let ns_pasteboard = unsafe { + NSPasteboard::generalPasteboard(nil) + // let format_ns_array = NSArray::arrayWithObjects( + // nil, + // vec![ + // NSPasteboardTypeString, + // NSPasteboardTypeRTF, + // NSPasteboardTypeHTML, + // NSPasteboardTypePNG, + // ] + // .as_ref(), + // ); + // np.declareTypes_owner(format_ns_array, nil); + }; + let clipboard_ctx = ClipboardContext { + clipboard: ns_pasteboard, + }; + Ok(clipboard_ctx) + } + + /// Read from clipboard return trait by NSPasteboardItem + fn read_from_clipboard(&self) -> Result> { + let res = unsafe { + let ns_array: id = self.clipboard.pasteboardItems(); + if ns_array.count() == 0 { + return Ok(Vec::new()); + } + ns_array.iter().collect::>() + }; + Ok(res) + } + + fn read_string(&self, ns_type: id) -> Result { + let res = unsafe { + let ns_string: id = self.clipboard.stringForType(ns_type); + if ns_string.len() == 0 { + return Ok("".to_owned()); + } + let bytes = ns_string.UTF8String(); + let c_str = CStr::from_ptr(bytes); + let str_slice = c_str.to_str()?; + str_slice.to_owned() + }; + Ok(res) + } + + // learn from https://github.com/zed-industries/zed/blob/79c1003b344ee513cf97ee8313c38c7c3f02c916/crates/gpui/src/platform/mac/platform.rs#L793 + fn write_to_clipboard(&self, data: &[WriteToClipboardData], with_clear: bool) -> Result<()> { + if with_clear { + unsafe { + self.clipboard.clearContents(); + } + } + data.iter().for_each(|d| unsafe { + let ns_type = match d.format.clone() { + ContentFormat::Text => NSPasteboardTypeString, + ContentFormat::Rtf => NSPasteboardTypeRTF, + ContentFormat::Html => NSPasteboardTypeHTML, + ContentFormat::Image => NSPasteboardTypePNG, + ContentFormat::Files => NSFilenamesPboardType, + ContentFormat::Other(other_format) => { + NSString::alloc(nil).init_str(other_format.as_str()) + } + }; + if let ContentFormat::Other(_) | ContentFormat::Files = d.format { + self.clipboard + .declareTypes_owner(NSArray::arrayWithObject(nil, ns_type), nil); + } + if d.is_multi { + self.clipboard.setPropertyList_forType( + NSArray::arrayByAddingObjectsFromArray(nil, d.data), + ns_type, + ); + } else { + let ns_data = d.data; + self.clipboard.setData_forType(ns_data, ns_type); + } + }); + Ok(()) + } } unsafe impl Send for ClipboardContext {} + unsafe impl Sync for ClipboardContext {} struct WriteToClipboardData { - data: id, - format: ContentFormat, - is_multi: bool, + data: id, + format: ContentFormat, + is_multi: bool, } impl Clipboard for ClipboardContext { - fn available_formats(&self) -> Result> { - let res = unsafe { - // let _pool = NSAutoreleasePool::new(nil); - // let types = self.clipboard.types().autorelease(); - let types = self.clipboard.types(); - if types.count() == 0 { - return Ok(Vec::new()); - } - types - .iter() - .map(|t| { - let bytes = t.UTF8String(); - let c_str = CStr::from_ptr(bytes); - let str_slice = c_str.to_str()?; - Ok(str_slice.to_owned()) - }) - .collect::>>()? - }; - Ok(res) - } - - fn get_text(&self) -> Result { - let res = unsafe { - let ns_string: id = self.clipboard.stringForType(NSPasteboardTypeString); - if ns_string.len() == 0 { - return Ok("".to_owned()); - } - let bytes = ns_string.UTF8String(); - let c_str = CStr::from_ptr(bytes); - let str_slice = c_str.to_str()?; - str_slice.to_owned() - }; - Ok(res) - } - - fn get_rich_text(&self) -> Result { - let res = unsafe { - let ns_string: id = self.clipboard.stringForType(NSPasteboardTypeRTF); - if ns_string.len() == 0 { - return Ok("".to_owned()); - } - let bytes = ns_string.UTF8String(); - let c_str = CStr::from_ptr(bytes); - let str_slice = c_str.to_str()?; - str_slice.to_owned() - }; - Ok(res) - } - - fn get_html(&self) -> Result { - let res = unsafe { - let ns_string: id = self.clipboard.stringForType(NSPasteboardTypeHTML); - if ns_string.len() == 0 { - return Ok("".to_owned()); - } - let bytes = ns_string.UTF8String(); - let c_str = CStr::from_ptr(bytes); - let str_slice = c_str.to_str()?; - str_slice.to_owned() - }; - Ok(res) - } - - fn get_image(&self) -> Result { - let res = unsafe { - let ns_data = self.clipboard.dataForType(NSPasteboardTypePNG); - if ns_data.length() == 0 { - return Ok(RustImageData::empty()); - } - let length: usize = ns_data.length() as usize; - let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length); - RustImageData::from_bytes(bytes)? - }; - Ok(res) - } - - fn get_buffer(&self, format: &str) -> Result> { - let res = unsafe { - let ns_data = self - .clipboard - .dataForType(NSString::alloc(nil).init_str(format)); - if ns_data.length() == 0 { - return Ok(Vec::new()); - } - let length: usize = ns_data.length() as usize; - let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length).to_vec(); - bytes - }; - Ok(res) - } - - fn set_text(&self, text: String) -> Result<()> { - self.write_to_clipboard( - &[WriteToClipboardData { - data: unsafe { - NSData::dataWithBytes_length_( - nil, - text.as_ptr() as *const c_void, - text.len() as u64, - ) - }, - is_multi: false, - format: ContentFormat::Text, - }], - true, - ) - } - - fn set_rich_text(&self, text: String) -> Result<()> { - self.write_to_clipboard( - &[WriteToClipboardData { - data: unsafe { - NSData::dataWithBytes_length_( - nil, - text.as_ptr() as *const c_void, - text.len() as u64, - ) - }, - is_multi: false, - format: ContentFormat::Rtf, - }], - true, - ) - } - - fn set_html(&self, html: String) -> Result<()> { - self.write_to_clipboard( - &[WriteToClipboardData { - data: unsafe { - NSData::dataWithBytes_length_( - nil, - html.as_ptr() as *const c_void, - html.len() as u64, - ) - }, - is_multi: false, - format: ContentFormat::Html, - }], - true, - ) - } - - fn set_image(&self, image: RustImageData) -> Result<()> { - let png = image.to_png()?; - let res = self.write_to_clipboard( - &[WriteToClipboardData { - data: unsafe { - NSData::dataWithBytes_length_( - nil, - png.get_bytes().as_ptr() as *const c_void, - png.get_bytes().len() as u64, - ) - }, - is_multi: false, - format: ContentFormat::Image, - }], - true, - ); - res - } - - fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()> { - self.write_to_clipboard( - &[WriteToClipboardData { - data: unsafe { - NSData::dataWithBytes_length_( - nil, - buffer.as_ptr() as *const c_void, - buffer.len() as u64, - ) - }, - is_multi: false, - format: ContentFormat::Other(format.to_owned()), - }], - true, - ) - } - - fn clear(&self) -> Result<()> { - unsafe { self.clipboard.clearContents() }; - Ok(()) - } - - fn has(&self, format: ContentFormat) -> bool { - match format { - ContentFormat::Text => unsafe { - let types = NSArray::arrayWithObject(nil, NSPasteboardTypeString); - // https://developer.apple.com/documentation/appkit/nspasteboard/1526078-availabletypefromarray?language=objc - // The first pasteboard type in types that is available on the pasteboard, or nil if the receiver does not contain any of the types in types. - // self.clipboard.availableTypeFromArray(types) - self.clipboard.availableTypeFromArray(types) != nil - }, - ContentFormat::Rtf => unsafe { - let types = NSArray::arrayWithObject(nil, NSPasteboardTypeRTF); - self.clipboard.availableTypeFromArray(types) != nil - }, - ContentFormat::Html => unsafe { - // Currently only judge whether there is a public.html format - let types = NSArray::arrayWithObjects(nil, &[NSPasteboardTypeHTML]); - self.clipboard.availableTypeFromArray(types) != nil - }, - ContentFormat::Image => unsafe { - // Currently only judge whether there is a png format - let types = NSArray::arrayWithObjects(nil, &[NSPasteboardTypePNG]); - self.clipboard.availableTypeFromArray(types) != nil - }, - ContentFormat::Files => unsafe { - // Currently only judge whether there is a public.file-url format - let types = - NSArray::arrayWithObjects(nil, &[NSString::alloc(nil).init_str(NS_FILES)]); - self.clipboard.availableTypeFromArray(types) != nil - }, - ContentFormat::Other(format) => unsafe { - let types = NSArray::arrayWithObjects( - nil, - &[NSString::alloc(nil).init_str(format.as_str())], - ); - self.clipboard.availableTypeFromArray(types) != nil - }, - } - } - - fn get_files(&self) -> Result> { - let res = unsafe { - let ns_array: id = self.clipboard.pasteboardItems(); - if ns_array.count() == 0 { - return Ok(vec![]); - } - ns_array - .iter() - .map(|ns_pastboard_item| { - let ns_string: id = - ns_pastboard_item.stringForType(NSString::alloc(nil).init_str(NS_FILES)); - let bytes = ns_string.UTF8String(); - let c_str = CStr::from_ptr(bytes); - let str_slice = c_str.to_str()?; - Ok(str_slice.to_owned()) - }) - .collect::>>()? - }; - Ok(res) - } - - fn get(&self, formats: &[ContentFormat]) -> Result> { - let ns_pastboard_item_arr = self.read_from_clipboard()?; - let mut res: Vec = vec![]; - if ns_pastboard_item_arr.is_empty() { - return Ok(res); - } - for format in formats { - let content = convert_to_clipboard_content(&ns_pastboard_item_arr, format); - res.push(content); - } - Ok(res) - } - - fn set_files(&self, file: Vec) -> Result<()> { - unsafe { - let ns_string_arr = file - .iter() - .map(|f| NSString::alloc(nil).init_str(f)) - .collect::>(); - self.clipboard - .declareTypes_owner(NSArray::arrayWithObject(nil, NSFilenamesPboardType), nil); - self.clipboard.setPropertyList_forType( - NSArray::arrayWithObjects(nil, ns_string_arr.as_ref()), - NSFilenamesPboardType, - ); - } - Ok(()) - } - - fn set(&self, contents: Vec) -> Result<()> { - let mut write_data_vec = vec![]; - for content in contents { - let write_data = content.to_write_data()?; - write_data_vec.push(write_data); - } - self.write_to_clipboard(&write_data_vec, true) - } + fn available_formats(&self) -> Result> { + let res = unsafe { + // let _pool = NSAutoreleasePool::new(nil); + // let types = self.clipboard.types().autorelease(); + let types = self.clipboard.types(); + if types.count() == 0 { + return Ok(Vec::new()); + } + types + .iter() + .map(|t| { + let bytes = t.UTF8String(); + let c_str = CStr::from_ptr(bytes); + let str_slice = c_str.to_str()?; + Ok(str_slice.to_owned()) + }) + .collect::>>()? + }; + Ok(res) + } + + fn has(&self, format: ContentFormat) -> bool { + match format { + ContentFormat::Text => unsafe { + let types = NSArray::arrayWithObject(nil, NSPasteboardTypeString); + // https://developer.apple.com/documentation/appkit/nspasteboard/1526078-availabletypefromarray?language=objc + // The first pasteboard type in types that is available on the pasteboard, or nil if the receiver does not contain any of the types in types. + // self.clipboard.availableTypeFromArray(types) + self.clipboard.availableTypeFromArray(types) != nil + }, + ContentFormat::Rtf => unsafe { + let types = NSArray::arrayWithObject(nil, NSPasteboardTypeRTF); + self.clipboard.availableTypeFromArray(types) != nil + }, + ContentFormat::Html => unsafe { + // Currently only judge whether there is a public.html format + let types = NSArray::arrayWithObjects(nil, &[NSPasteboardTypeHTML]); + self.clipboard.availableTypeFromArray(types) != nil + }, + ContentFormat::Image => unsafe { + // Currently only judge whether there is a png format + let types = NSArray::arrayWithObjects(nil, &[NSPasteboardTypePNG]); + self.clipboard.availableTypeFromArray(types) != nil + }, + ContentFormat::Files => unsafe { + // Currently only judge whether there is a public.file-url format + let types = + NSArray::arrayWithObjects(nil, &[NSString::alloc(nil).init_str(NS_FILES)]); + self.clipboard.availableTypeFromArray(types) != nil + }, + ContentFormat::Other(format) => unsafe { + let types = NSArray::arrayWithObjects( + nil, + &[NSString::alloc(nil).init_str(format.as_str())], + ); + self.clipboard.availableTypeFromArray(types) != nil + }, + } + } + + fn clear(&self) -> Result<()> { + unsafe { self.clipboard.clearContents() }; + Ok(()) + } + + fn get_buffer(&self, format: &str) -> Result> { + let res = unsafe { + let ns_data = self + .clipboard + .dataForType(NSString::alloc(nil).init_str(format)); + if ns_data.length() == 0 { + return Ok(Vec::new()); + } + let length: usize = ns_data.length() as usize; + let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length).to_vec(); + bytes + }; + Ok(res) + } + + fn get_text(&self) -> Result { + self.read_string(unsafe { NSPasteboardTypeString }) + } + + fn get_rich_text(&self) -> Result { + self.read_string(unsafe { NSPasteboardTypeRTF }) + } + + fn get_html(&self) -> Result { + self.read_string(unsafe { NSPasteboardTypeHTML }) + } + + fn get_image(&self) -> Result { + let res = unsafe { + let ns_data = self.clipboard.dataForType(NSPasteboardTypePNG); + if ns_data.length() == 0 { + return Ok(RustImageData::empty()); + } + let length: usize = ns_data.length() as usize; + let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length); + RustImageData::from_bytes(bytes)? + }; + Ok(res) + } + + fn get_files(&self) -> Result> { + let res = unsafe { + let ns_array: id = self.clipboard.pasteboardItems(); + if ns_array.count() == 0 { + return Ok(vec![]); + } + ns_array + .iter() + .map(|ns_pasteboard_item| { + let ns_string: id = + ns_pasteboard_item.stringForType(NSString::alloc(nil).init_str(NS_FILES)); + let bytes = ns_string.UTF8String(); + let c_str = CStr::from_ptr(bytes); + let str_slice = c_str.to_str()?; + Ok(str_slice.to_owned()) + }) + .collect::>>()? + }; + Ok(res) + } + + fn get(&self, formats: &[ContentFormat]) -> Result> { + let ns_pasteboard_item_arr = self.read_from_clipboard()?; + let mut res: Vec = vec![]; + if ns_pasteboard_item_arr.is_empty() { + return Ok(res); + } + for format in formats { + let content = convert_to_clipboard_content(&ns_pasteboard_item_arr, format); + res.push(content); + } + Ok(res) + } + + fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()> { + self.write_to_clipboard( + &[WriteToClipboardData { + data: unsafe { + NSData::dataWithBytes_length_( + nil, + buffer.as_ptr() as *const c_void, + buffer.len() as u64, + ) + }, + is_multi: false, + format: ContentFormat::Other(format.to_owned()), + }], + true, + ) + } + + fn set_text(&self, text: String) -> Result<()> { + self.write_to_clipboard( + &[WriteToClipboardData { + data: string_to_ns_data(text), + is_multi: false, + format: ContentFormat::Text, + }], + true, + ) + } + + fn set_rich_text(&self, text: String) -> Result<()> { + self.write_to_clipboard( + &[WriteToClipboardData { + data: string_to_ns_data(text), + is_multi: false, + format: ContentFormat::Rtf, + }], + true, + ) + } + + fn set_html(&self, html: String) -> Result<()> { + self.write_to_clipboard( + &[WriteToClipboardData { + data: string_to_ns_data(html), + is_multi: false, + format: ContentFormat::Html, + }], + true, + ) + } + + fn set_image(&self, image: RustImageData) -> Result<()> { + let png = image.to_png()?; + let res = self.write_to_clipboard( + &[WriteToClipboardData { + data: unsafe { + NSData::dataWithBytes_length_( + nil, + png.get_bytes().as_ptr() as *const c_void, + png.get_bytes().len() as u64, + ) + }, + is_multi: false, + format: ContentFormat::Image, + }], + true, + ); + res + } + + fn set_files(&self, file: Vec) -> Result<()> { + unsafe { + let ns_string_arr = file + .iter() + .map(|f| NSString::alloc(nil).init_str(f)) + .collect::>(); + self.clipboard + .declareTypes_owner(NSArray::arrayWithObject(nil, NSFilenamesPboardType), nil); + self.clipboard.setPropertyList_forType( + NSArray::arrayWithObjects(nil, ns_string_arr.as_ref()), + NSFilenamesPboardType, + ); + } + Ok(()) + } + + fn set(&self, contents: Vec) -> Result<()> { + let mut write_data_vec = vec![]; + for content in contents { + let write_data = content.to_write_data()?; + write_data_vec.push(write_data); + } + self.write_to_clipboard(&write_data_vec, true) + } } impl ClipboardContent { - fn to_write_data(&self) -> Result { - let write_data = match self { - ClipboardContent::Files(file_list) => { - let ns_string_arr = file_list - .iter() - .map(|f| unsafe { NSString::alloc(nil).init_str(f) }) - .collect::>(); - let ns_array = unsafe { NSArray::arrayWithObjects(nil, ns_string_arr.as_ref()) }; - WriteToClipboardData { - data: ns_array, - is_multi: true, - format: ContentFormat::Files, - } - } - _ => WriteToClipboardData { - data: unsafe { - NSData::dataWithBytes_length_( - nil, - self.as_bytes().as_ptr() as *const c_void, - self.as_bytes().len() as u64, - ) - }, - is_multi: false, - format: self.get_format(), - }, - }; - Ok(write_data) - } + fn to_write_data(&self) -> Result { + let write_data = match self { + ClipboardContent::Files(file_list) => { + let ns_string_arr = file_list + .iter() + .map(|f| unsafe { NSString::alloc(nil).init_str(f) }) + .collect::>(); + let ns_array = unsafe { NSArray::arrayWithObjects(nil, ns_string_arr.as_ref()) }; + WriteToClipboardData { + data: ns_array, + is_multi: true, + format: ContentFormat::Files, + } + } + _ => WriteToClipboardData { + data: unsafe { + NSData::dataWithBytes_length_( + nil, + self.as_bytes().as_ptr() as *const c_void, + self.as_bytes().len() as u64, + ) + }, + is_multi: false, + format: self.get_format(), + }, + }; + Ok(write_data) + } } fn convert_to_clipboard_content( - ns_pastboard_item_arr: &Vec, - format: &ContentFormat, + ns_pasteboard_item_arr: &Vec, + format: &ContentFormat, ) -> ClipboardContent { - unsafe { - let ns_type = { - match format { - ContentFormat::Text => NSPasteboardTypeString, - ContentFormat::Rtf => NSPasteboardTypeRTF, - ContentFormat::Html => NSPasteboardTypeHTML, - ContentFormat::Image => NSPasteboardTypePNG, - ContentFormat::Files => NSString::alloc(nil).init_str(NS_FILES), - ContentFormat::Other(other_format) => { - NSString::alloc(nil).init_str(other_format.as_str()) - } - } - }; - let content: ClipboardContent = match format { - ContentFormat::Text | ContentFormat::Rtf | ContentFormat::Html => { - let mut string_vec = Vec::new(); - for ns_pastboard_item in ns_pastboard_item_arr { - let ns_string: id = ns_pastboard_item.stringForType(ns_type); - if ns_string.len() == 0 { - continue; - } - let bytes = ns_string.UTF8String(); - let c_str = CStr::from_ptr(bytes); - let str_slice = c_str.to_str().unwrap(); - string_vec.push(str_slice); - } - match format { - ContentFormat::Text => ClipboardContent::Text(string_vec.join("\n")), - ContentFormat::Rtf => ClipboardContent::Rtf(string_vec.join("\n")), - ContentFormat::Html => ClipboardContent::Html(string_vec.join("\n")), - _ => panic!("unexpected format"), - } - } - ContentFormat::Image => match ns_pastboard_item_arr.first() { - Some(ns_pastboard_item) => { - let ns_data = ns_pastboard_item.dataForType(ns_type); - if ns_data.length() == 0 { - return ClipboardContent::Image(RustImageData::empty()); - } - let length: usize = ns_data.length() as usize; - let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length); - let image = RustImageData::from_bytes(bytes).unwrap(); - ClipboardContent::Image(image) - } - None => ClipboardContent::Image(RustImageData::empty()), - }, - ContentFormat::Files => { - let mut string_vec = Vec::new(); - for ns_pastboard_item in ns_pastboard_item_arr { - let ns_string: id = ns_pastboard_item.stringForType(ns_type); - if ns_string.len() == 0 { - continue; - } - let bytes = ns_string.UTF8String(); - let c_str = CStr::from_ptr(bytes); - let str_slice = c_str.to_str().unwrap(); - string_vec.push(str_slice.to_owned()); - } - ClipboardContent::Files(string_vec) - } - ContentFormat::Other(format) => match ns_pastboard_item_arr.first() { - Some(ns_pastboard_item) => { - let ns_data = ns_pastboard_item.dataForType(ns_type); - if ns_data.length() == 0 { - return ClipboardContent::Other(format.clone(), Vec::new()); - } - let length: usize = ns_data.length() as usize; - let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length); - ClipboardContent::Other(format.to_string(), bytes.to_vec()) - } - None => ClipboardContent::Other(format.clone(), Vec::new()), - }, - }; - content - } + unsafe { + let ns_type = { + match format { + ContentFormat::Text => NSPasteboardTypeString, + ContentFormat::Rtf => NSPasteboardTypeRTF, + ContentFormat::Html => NSPasteboardTypeHTML, + ContentFormat::Image => NSPasteboardTypePNG, + ContentFormat::Files => NSString::alloc(nil).init_str(NS_FILES), + ContentFormat::Other(other_format) => { + NSString::alloc(nil).init_str(other_format.as_str()) + } + } + }; + let content: ClipboardContent = match format { + ContentFormat::Text | ContentFormat::Rtf | ContentFormat::Html => { + let mut string_vec = Vec::new(); + for ns_pasteboard_item in ns_pasteboard_item_arr { + let ns_string: id = ns_pasteboard_item.stringForType(ns_type); + if ns_string.len() == 0 { + continue; + } + let bytes = ns_string.UTF8String(); + let c_str = CStr::from_ptr(bytes); + let str_slice = c_str.to_str().unwrap(); + string_vec.push(str_slice); + } + match format { + ContentFormat::Text => ClipboardContent::Text(string_vec.join("\n")), + ContentFormat::Rtf => ClipboardContent::Rtf(string_vec.join("\n")), + ContentFormat::Html => ClipboardContent::Html(string_vec.join("\n")), + _ => panic!("unexpected format"), + } + } + ContentFormat::Image => match ns_pasteboard_item_arr.first() { + Some(ns_pasteboard_item) => { + let ns_data = ns_pasteboard_item.dataForType(ns_type); + if ns_data.length() == 0 { + return ClipboardContent::Image(RustImageData::empty()); + } + let length: usize = ns_data.length() as usize; + let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length); + let image = RustImageData::from_bytes(bytes).unwrap(); + ClipboardContent::Image(image) + } + None => ClipboardContent::Image(RustImageData::empty()), + }, + ContentFormat::Files => { + let mut string_vec = Vec::new(); + for ns_pasteboard_item in ns_pasteboard_item_arr { + let ns_string: id = ns_pasteboard_item.stringForType(ns_type); + if ns_string.len() == 0 { + continue; + } + let bytes = ns_string.UTF8String(); + let c_str = CStr::from_ptr(bytes); + let str_slice = c_str.to_str().unwrap(); + string_vec.push(str_slice.to_owned()); + } + ClipboardContent::Files(string_vec) + } + ContentFormat::Other(format) => match ns_pasteboard_item_arr.first() { + Some(ns_pasteboard_item) => { + let ns_data = ns_pasteboard_item.dataForType(ns_type); + if ns_data.length() == 0 { + return ClipboardContent::Other(format.clone(), Vec::new()); + } + let length: usize = ns_data.length() as usize; + let bytes = slice::from_raw_parts(ns_data.bytes() as *const u8, length); + ClipboardContent::Other(format.to_string(), bytes.to_vec()) + } + None => ClipboardContent::Other(format.clone(), Vec::new()), + }, + }; + content + } +} + +fn string_to_ns_data(string: String) -> id { + unsafe { + NSData::dataWithBytes_length_(nil, string.as_ptr() as *const c_void, string.len() as u64) + } } pub struct WatcherShutdown { - stop_signal: Sender<()>, + stop_signal: Sender<()>, } impl Drop for WatcherShutdown { - fn drop(&mut self) { - let _ = self.stop_signal.send(()); - } + fn drop(&mut self) { + let _ = self.stop_signal.send(()); + } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index cebbc8a..5bcd7df 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -6,7 +6,23 @@ pub use macos::{ClipboardContext, ClipboardWatcherContext, WatcherShutdown}; mod win; #[cfg(target_os = "windows")] pub use win::{ClipboardContext, ClipboardWatcherContext, WatcherShutdown}; -#[cfg(all(unix, not(any(target_os="macos", target_os="ios", target_os="android", target_os="emscripten"))))] +#[cfg(all( + unix, + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )) +))] mod x11; -#[cfg(all(unix, not(any(target_os="macos", target_os="ios", target_os="android", target_os="emscripten"))))] -pub use x11::{ClipboardContext, ClipboardWatcherContext, WatcherShutdown}; \ No newline at end of file +#[cfg(all( + unix, + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )) +))] +pub use x11::{ClipboardContext, ClipboardWatcherContext, WatcherShutdown}; diff --git a/src/platform/win.rs b/src/platform/win.rs index e4dfefb..511145b 100644 --- a/src/platform/win.rs +++ b/src/platform/win.rs @@ -5,12 +5,12 @@ use crate::{Clipboard, ClipboardContent, ClipboardWatcher, ContentFormat}; use clipboard_win::raw::set_without_clear; use clipboard_win::types::c_uint; use clipboard_win::{ - formats, get, get_clipboard, raw, set_clipboard, Clipboard as ClipboardWin, Setter, SysResult, + formats, get, get_clipboard, raw, set_clipboard, Clipboard as ClipboardWin, Setter, SysResult, }; use image::EncodableLayout; use windows_win::sys::{ - AddClipboardFormatListener, PostMessageW, RemoveClipboardFormatListener, HWND, - WM_CLIPBOARDUPDATE, + AddClipboardFormatListener, PostMessageW, RemoveClipboardFormatListener, HWND, + WM_CLIPBOARDUPDATE, }; use windows_win::{Messages, Window}; @@ -20,12 +20,12 @@ static CF_HTML: &str = "HTML Format"; static CF_PNG: &str = "PNG"; pub struct ClipboardContext { - format_map: HashMap<&'static str, c_uint>, + format_map: HashMap<&'static str, c_uint>, } pub struct ClipboardWatcherContext { - handlers: Vec, - window: Window, + handlers: Vec, + window: Window, } unsafe impl Send for ClipboardContext {} @@ -33,13 +33,13 @@ unsafe impl Sync for ClipboardContext {} unsafe impl Send for ClipboardWatcherContext {} pub struct WatcherShutdown { - window: HWND, + window: HWND, } impl Drop for WatcherShutdown { - fn drop(&mut self) { - unsafe { PostMessageW(self.window, WM_CLIPBOARDUPDATE, 0, -1) }; - } + fn drop(&mut self) { + unsafe { PostMessageW(self.window, WM_CLIPBOARDUPDATE, 0, -1) }; + } } unsafe impl Send for WatcherShutdown {} @@ -47,394 +47,394 @@ unsafe impl Send for WatcherShutdown {} pub struct ClipboardListener(HWND); impl ClipboardListener { - pub fn new(window: HWND) -> Result { - unsafe { - if AddClipboardFormatListener(window) != 1 { - Err("AddClipboardFormatListener failed".into()) - } else { - Ok(ClipboardListener(window)) - } - } - } + pub fn new(window: HWND) -> Result { + unsafe { + if AddClipboardFormatListener(window) != 1 { + Err("AddClipboardFormatListener failed".into()) + } else { + Ok(ClipboardListener(window)) + } + } + } } impl Drop for ClipboardListener { - fn drop(&mut self) { - unsafe { - RemoveClipboardFormatListener(self.0); - } - } + fn drop(&mut self) { + unsafe { + RemoveClipboardFormatListener(self.0); + } + } } impl ClipboardContext { - pub fn new() -> Result { - let window = core::ptr::null_mut(); - let _ = ClipboardWin::new_attempts_for(window, 10).expect("Open clipboard"); - let format_map = { - let cf_html_uint = clipboard_win::register_format(CF_HTML); - let cf_rtf_uint = clipboard_win::register_format(CF_RTF); - let cf_png_uint = clipboard_win::register_format(CF_PNG); - let mut m: HashMap<&str, c_uint> = HashMap::new(); - if let Some(cf_html) = cf_html_uint { - m.insert(CF_HTML, cf_html.get()); - } - if let Some(cf_rtf) = cf_rtf_uint { - m.insert(CF_RTF, cf_rtf.get()); - } - if let Some(cf_png) = cf_png_uint { - m.insert(CF_PNG, cf_png.get()); - } - m - }; - Ok(ClipboardContext { format_map }) - } - - fn get_format(&self, format: &ContentFormat) -> c_uint { - match format { - ContentFormat::Text => formats::CF_UNICODETEXT, - ContentFormat::Rtf => *self.format_map.get(CF_RTF).unwrap(), - ContentFormat::Html => *self.format_map.get(CF_HTML).unwrap(), - ContentFormat::Image => *self.format_map.get(CF_PNG).unwrap(), - ContentFormat::Files => formats::CF_HDROP, - ContentFormat::Other(format) => clipboard_win::register_format(format).unwrap().get(), - } - } + pub fn new() -> Result { + let window = core::ptr::null_mut(); + let _ = ClipboardWin::new_attempts_for(window, 10).expect("Open clipboard"); + let format_map = { + let cf_html_uint = clipboard_win::register_format(CF_HTML); + let cf_rtf_uint = clipboard_win::register_format(CF_RTF); + let cf_png_uint = clipboard_win::register_format(CF_PNG); + let mut m: HashMap<&str, c_uint> = HashMap::new(); + if let Some(cf_html) = cf_html_uint { + m.insert(CF_HTML, cf_html.get()); + } + if let Some(cf_rtf) = cf_rtf_uint { + m.insert(CF_RTF, cf_rtf.get()); + } + if let Some(cf_png) = cf_png_uint { + m.insert(CF_PNG, cf_png.get()); + } + m + }; + Ok(ClipboardContext { format_map }) + } + + fn get_format(&self, format: &ContentFormat) -> c_uint { + match format { + ContentFormat::Text => formats::CF_UNICODETEXT, + ContentFormat::Rtf => *self.format_map.get(CF_RTF).unwrap(), + ContentFormat::Html => *self.format_map.get(CF_HTML).unwrap(), + ContentFormat::Image => *self.format_map.get(CF_PNG).unwrap(), + ContentFormat::Files => formats::CF_HDROP, + ContentFormat::Other(format) => clipboard_win::register_format(format).unwrap().get(), + } + } } impl ClipboardWatcherContext { - pub fn new() -> Result { - let window = match Window::from_builder( - windows_win::raw::window::Builder::new() - .class_name("STATIC") - .parent_message(), - ) { - Ok(window) => window, - Err(_) => return Err("create window error".into()), - }; - Ok(Self { - handlers: Vec::new(), - window, - }) - } + pub fn new() -> Result { + let window = match Window::from_builder( + windows_win::raw::window::Builder::new() + .class_name("STATIC") + .parent_message(), + ) { + Ok(window) => window, + Err(_) => return Err("create window error".into()), + }; + Ok(Self { + handlers: Vec::new(), + window, + }) + } } impl Clipboard for ClipboardContext { - fn available_formats(&self) -> Result> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); - let format_count = clipboard_win::count_formats(); - if format_count.is_none() { - return Ok(Vec::new()); - } - let mut res = Vec::new(); - let enum_formats = clipboard_win::raw::EnumFormats::new(); - enum_formats.into_iter().for_each(|format| { - let f_name = raw::format_name_big(format); - match f_name { - Some(name) => res.push(name), - None => { - res.push(UNKNOW_FORMAT.to_string()); - } - } - }); - Ok(res) - } - - fn clear(&self) -> Result<()> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); - let res = clipboard_win::empty(); - if res.is_err() { - return Err("clear clipboard error".into()); - } - Ok(()) - } - - fn get_buffer(&self, format: &str) -> Result> { - let format_uint = clipboard_win::register_format(format); - if format_uint.is_none() { - return Err("register format error".into()); - } - let format_uint = format_uint.unwrap().get(); - let buffer = get_clipboard(formats::RawData(format_uint)); - match buffer { - Ok(data) => Ok(data), - Err(_) => Err("get buffer error".into()), - } - } - - fn get_text(&self) -> Result { - let string: SysResult = get_clipboard(formats::Unicode); - match string { - Ok(s) => Ok(s), - Err(_) => Ok("".to_string()), - } - } - - fn get_rich_text(&self) -> Result { - let rtf_raw_data = self.get_buffer(CF_RTF); - match rtf_raw_data { - Ok(data) => { - let rtf = String::from_utf8(data); - match rtf { - Ok(s) => Ok(s), - Err(_) => Ok("".to_string()), - } - } - Err(_) => Ok("".to_string()), - } - } - - fn get_html(&self) -> Result { - let html_raw_data = self.get_buffer(CF_HTML); - match html_raw_data { - Ok(data) => cf_html_to_plain_html(data), - Err(_) => Ok("".to_string()), - } - } - - fn get_image(&self) -> Result { - let image_raw_data = self.get_buffer(CF_PNG); - match image_raw_data { - Ok(data) => RustImageData::from_bytes(&data), - Err(_) => Ok(RustImageData::empty()), - } - } - - fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()> { - let format_uint = clipboard_win::register_format(format); - if format_uint.is_none() { - return Err("register format error".into()); - } - let format_uint = format_uint.unwrap().get(); - let res = set_clipboard(formats::RawData(format_uint), buffer); - if res.is_err() { - return Err("set buffer error".into()); - } - Ok(()) - } - - fn set_text(&self, text: String) -> Result<()> { - let res = set_clipboard(formats::Unicode, text); - if res.is_err() { - return Err("set text error".into()); - } - Ok(()) - } - - fn set_rich_text(&self, text: String) -> Result<()> { - let res = self.set_buffer(CF_RTF, text.as_bytes().to_vec()); - if res.is_err() { - return Err("set rich text error".into()); - } - Ok(()) - } - - fn set_html(&self, html: String) -> Result<()> { - let cf_html = plain_html_to_cf_html(&html); - let res = self.set_buffer(CF_HTML, cf_html.as_bytes().to_vec()); - if res.is_err() { - return Err("set html error".into()); - } - Ok(()) - } - - fn set_image(&self, image: RustImageData) -> Result<()> { - let png = image.to_png()?; - let res = self.set_buffer(CF_PNG, png.get_bytes().to_vec()); - if res.is_err() { - return Err("set image error".into()); - } - Ok(()) - } - - fn has(&self, format: ContentFormat) -> bool { - match format { - ContentFormat::Text => clipboard_win::is_format_avail(formats::CF_UNICODETEXT), - ContentFormat::Rtf => { - let cf_rtf_uint = self.format_map.get(CF_RTF).unwrap(); - clipboard_win::is_format_avail(*cf_rtf_uint) - } - ContentFormat::Html => { - let cf_html_uint = self.format_map.get(CF_HTML).unwrap(); - clipboard_win::is_format_avail(*cf_html_uint) - } - ContentFormat::Image => { - // Currently only judge whether there is a png format - let cf_png_uint = self.format_map.get(CF_PNG).unwrap(); - clipboard_win::is_format_avail(*cf_png_uint) - } - ContentFormat::Files => clipboard_win::is_format_avail(formats::CF_HDROP), - ContentFormat::Other(format) => { - let format_uint = clipboard_win::register_format(format.as_str()); - if let Some(format_uint) = format_uint { - return clipboard_win::is_format_avail(format_uint.get()); - } - false - } - } - } - - fn get_files(&self) -> Result> { - let files: SysResult> = get_clipboard(formats::FileList); - match files { - Ok(f) => Ok(f), - Err(_) => Ok(Vec::new()), - } - } - - fn get(&self, formats: &[ContentFormat]) -> Result> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); - let mut res = Vec::new(); - for format in formats { - match format { - ContentFormat::Text => { - let r = get(formats::Unicode); - match r { - Ok(txt) => { - res.push(ClipboardContent::Text(txt)); - } - Err(_) => continue, - } - } - ContentFormat::Rtf => { - let format_uint = self.get_format(format); - let buffer = get(formats::RawData(format_uint)); - match buffer { - Ok(buffer) => { - let rtf = String::from_utf8(buffer)?; - res.push(ClipboardContent::Rtf(rtf)); - } - Err(_) => continue, - } - } - ContentFormat::Html => { - let format_uint = self.get_format(format); - let buffer = get(formats::RawData(format_uint)); - match buffer { - Ok(buffer) => { - let html = cf_html_to_plain_html(buffer)?; - res.push(ClipboardContent::Html(html)); - } - Err(_) => continue, - } - } - ContentFormat::Image => { - let format_uint = self.get_format(format); - let buffer = get(formats::RawData(format_uint)); - match buffer { - Ok(buffer) => { - let image = RustImage::from_bytes(&buffer)?; - res.push(ClipboardContent::Image(image)); - } - Err(_) => continue, - } - } - ContentFormat::Other(fmt) => { - let format_uint = self.get_format(format); - let buffer = get(formats::RawData(format_uint)); - match buffer { - Ok(buffer) => { - res.push(ClipboardContent::Other(fmt.clone(), buffer)); - } - Err(_) => continue, - } - } - ContentFormat::Files => { - let files = self.get_files(); - match files { - Ok(files) => { - res.push(ClipboardContent::Files(files)); - } - Err(_) => continue, - } - } - } - } - Ok(res) - } - - fn set_files(&self, files: Vec) -> Result<()> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); - let res = formats::FileList.write_clipboard(&files); - if res.is_err() { - return Err("set files error".into()); - } - Ok(()) - } - - fn set(&self, contents: Vec) -> Result<()> { - let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); - for content in contents { - match content { - ClipboardContent::Text(txt) => { - let format_uint = formats::CF_UNICODETEXT; - let u16_str = utf8_to_utf16(txt.as_str()); - let res = set_without_clear(format_uint, u16_str.as_bytes()); - if res.is_err() { - continue; - } - } - ClipboardContent::Rtf(_) - | ClipboardContent::Html(_) - | ClipboardContent::Image(_) - | ClipboardContent::Other(_, _) => { - let format_uint = self.get_format(&content.get_format()); - let res = set_without_clear(format_uint, content.as_bytes()); - if res.is_err() { - continue; - } - } - ClipboardContent::Files(file_list) => { - let res = formats::FileList.write_clipboard(&file_list); - if res.is_err() { - continue; - } - } - } - } - Ok(()) - } + fn available_formats(&self) -> Result> { + let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let format_count = clipboard_win::count_formats(); + if format_count.is_none() { + return Ok(Vec::new()); + } + let mut res = Vec::new(); + let enum_formats = clipboard_win::raw::EnumFormats::new(); + enum_formats.into_iter().for_each(|format| { + let f_name = raw::format_name_big(format); + match f_name { + Some(name) => res.push(name), + None => { + res.push(UNKNOW_FORMAT.to_string()); + } + } + }); + Ok(res) + } + + fn clear(&self) -> Result<()> { + let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let res = clipboard_win::empty(); + if res.is_err() { + return Err("clear clipboard error".into()); + } + Ok(()) + } + + fn get_buffer(&self, format: &str) -> Result> { + let format_uint = clipboard_win::register_format(format); + if format_uint.is_none() { + return Err("register format error".into()); + } + let format_uint = format_uint.unwrap().get(); + let buffer = get_clipboard(formats::RawData(format_uint)); + match buffer { + Ok(data) => Ok(data), + Err(_) => Err("get buffer error".into()), + } + } + + fn get_text(&self) -> Result { + let string: SysResult = get_clipboard(formats::Unicode); + match string { + Ok(s) => Ok(s), + Err(_) => Ok("".to_string()), + } + } + + fn get_rich_text(&self) -> Result { + let rtf_raw_data = self.get_buffer(CF_RTF); + match rtf_raw_data { + Ok(data) => { + let rtf = String::from_utf8(data); + match rtf { + Ok(s) => Ok(s), + Err(_) => Ok("".to_string()), + } + } + Err(_) => Ok("".to_string()), + } + } + + fn get_html(&self) -> Result { + let html_raw_data = self.get_buffer(CF_HTML); + match html_raw_data { + Ok(data) => cf_html_to_plain_html(data), + Err(_) => Ok("".to_string()), + } + } + + fn get_image(&self) -> Result { + let image_raw_data = self.get_buffer(CF_PNG); + match image_raw_data { + Ok(data) => RustImageData::from_bytes(&data), + Err(_) => Ok(RustImageData::empty()), + } + } + + fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()> { + let format_uint = clipboard_win::register_format(format); + if format_uint.is_none() { + return Err("register format error".into()); + } + let format_uint = format_uint.unwrap().get(); + let res = set_clipboard(formats::RawData(format_uint), buffer); + if res.is_err() { + return Err("set buffer error".into()); + } + Ok(()) + } + + fn set_text(&self, text: String) -> Result<()> { + let res = set_clipboard(formats::Unicode, text); + if res.is_err() { + return Err("set text error".into()); + } + Ok(()) + } + + fn set_rich_text(&self, text: String) -> Result<()> { + let res = self.set_buffer(CF_RTF, text.as_bytes().to_vec()); + if res.is_err() { + return Err("set rich text error".into()); + } + Ok(()) + } + + fn set_html(&self, html: String) -> Result<()> { + let cf_html = plain_html_to_cf_html(&html); + let res = self.set_buffer(CF_HTML, cf_html.as_bytes().to_vec()); + if res.is_err() { + return Err("set html error".into()); + } + Ok(()) + } + + fn set_image(&self, image: RustImageData) -> Result<()> { + let png = image.to_png()?; + let res = self.set_buffer(CF_PNG, png.get_bytes().to_vec()); + if res.is_err() { + return Err("set image error".into()); + } + Ok(()) + } + + fn has(&self, format: ContentFormat) -> bool { + match format { + ContentFormat::Text => clipboard_win::is_format_avail(formats::CF_UNICODETEXT), + ContentFormat::Rtf => { + let cf_rtf_uint = self.format_map.get(CF_RTF).unwrap(); + clipboard_win::is_format_avail(*cf_rtf_uint) + } + ContentFormat::Html => { + let cf_html_uint = self.format_map.get(CF_HTML).unwrap(); + clipboard_win::is_format_avail(*cf_html_uint) + } + ContentFormat::Image => { + // Currently only judge whether there is a png format + let cf_png_uint = self.format_map.get(CF_PNG).unwrap(); + clipboard_win::is_format_avail(*cf_png_uint) + } + ContentFormat::Files => clipboard_win::is_format_avail(formats::CF_HDROP), + ContentFormat::Other(format) => { + let format_uint = clipboard_win::register_format(format.as_str()); + if let Some(format_uint) = format_uint { + return clipboard_win::is_format_avail(format_uint.get()); + } + false + } + } + } + + fn get_files(&self) -> Result> { + let files: SysResult> = get_clipboard(formats::FileList); + match files { + Ok(f) => Ok(f), + Err(_) => Ok(Vec::new()), + } + } + + fn get(&self, formats: &[ContentFormat]) -> Result> { + let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let mut res = Vec::new(); + for format in formats { + match format { + ContentFormat::Text => { + let r = get(formats::Unicode); + match r { + Ok(txt) => { + res.push(ClipboardContent::Text(txt)); + } + Err(_) => continue, + } + } + ContentFormat::Rtf => { + let format_uint = self.get_format(format); + let buffer = get(formats::RawData(format_uint)); + match buffer { + Ok(buffer) => { + let rtf = String::from_utf8(buffer)?; + res.push(ClipboardContent::Rtf(rtf)); + } + Err(_) => continue, + } + } + ContentFormat::Html => { + let format_uint = self.get_format(format); + let buffer = get(formats::RawData(format_uint)); + match buffer { + Ok(buffer) => { + let html = cf_html_to_plain_html(buffer)?; + res.push(ClipboardContent::Html(html)); + } + Err(_) => continue, + } + } + ContentFormat::Image => { + let format_uint = self.get_format(format); + let buffer = get(formats::RawData(format_uint)); + match buffer { + Ok(buffer) => { + let image = RustImage::from_bytes(&buffer)?; + res.push(ClipboardContent::Image(image)); + } + Err(_) => continue, + } + } + ContentFormat::Other(fmt) => { + let format_uint = self.get_format(format); + let buffer = get(formats::RawData(format_uint)); + match buffer { + Ok(buffer) => { + res.push(ClipboardContent::Other(fmt.clone(), buffer)); + } + Err(_) => continue, + } + } + ContentFormat::Files => { + let files = self.get_files(); + match files { + Ok(files) => { + res.push(ClipboardContent::Files(files)); + } + Err(_) => continue, + } + } + } + } + Ok(res) + } + + fn set_files(&self, files: Vec) -> Result<()> { + let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + let res = formats::FileList.write_clipboard(&files); + if res.is_err() { + return Err("set files error".into()); + } + Ok(()) + } + + fn set(&self, contents: Vec) -> Result<()> { + let _clip = ClipboardWin::new_attempts(10).expect("Open clipboard"); + for content in contents { + match content { + ClipboardContent::Text(txt) => { + let format_uint = formats::CF_UNICODETEXT; + let u16_str = utf8_to_utf16(txt.as_str()); + let res = set_without_clear(format_uint, u16_str.as_bytes()); + if res.is_err() { + continue; + } + } + ClipboardContent::Rtf(_) + | ClipboardContent::Html(_) + | ClipboardContent::Image(_) + | ClipboardContent::Other(_, _) => { + let format_uint = self.get_format(&content.get_format()); + let res = set_without_clear(format_uint, content.as_bytes()); + if res.is_err() { + continue; + } + } + ClipboardContent::Files(file_list) => { + let res = formats::FileList.write_clipboard(&file_list); + if res.is_err() { + continue; + } + } + } + } + Ok(()) + } } impl ClipboardWatcher for ClipboardWatcherContext { - fn add_handler(&mut self, f: CallBack) -> &mut Self { - self.handlers.push(f); - self - } - - fn start_watch(&mut self) { - let _guard = ClipboardListener::new(self.window.inner()).unwrap(); - for msg in Messages::new() - .window(Some(self.window.inner())) - .low(Some(WM_CLIPBOARDUPDATE)) - .high(Some(WM_CLIPBOARDUPDATE)) - { - match msg { - Ok(msg) => match msg.id() { - WM_CLIPBOARDUPDATE => { - let msg = msg.inner(); - - //Shutdown requested - if msg.lParam == -1 { - break; - } - self.handlers.iter().for_each(|handler| { - handler(); - }); - } - _ => panic!("Unexpected message"), - }, - Err(e) => { - println!("msg: error: {:?}", e); - } - } - } - } - - fn get_shutdown_channel(&self) -> WatcherShutdown { - WatcherShutdown { - window: self.window.inner(), - } - } + fn add_handler(&mut self, f: CallBack) -> &mut Self { + self.handlers.push(f); + self + } + + fn start_watch(&mut self) { + let _guard = ClipboardListener::new(self.window.inner()).unwrap(); + for msg in Messages::new() + .window(Some(self.window.inner())) + .low(Some(WM_CLIPBOARDUPDATE)) + .high(Some(WM_CLIPBOARDUPDATE)) + { + match msg { + Ok(msg) => match msg.id() { + WM_CLIPBOARDUPDATE => { + let msg = msg.inner(); + + //Shutdown requested + if msg.lParam == -1 { + break; + } + self.handlers.iter().for_each(|handler| { + handler(); + }); + } + _ => panic!("Unexpected message"), + }, + Err(e) => { + println!("msg: error: {:?}", e); + } + } + } + } + + fn get_shutdown_channel(&self) -> WatcherShutdown { + WatcherShutdown { + window: self.window.inner(), + } + } } // https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format @@ -456,93 +456,93 @@ impl ClipboardWatcher for ClipboardWatcherContext { // EndFragment:000000375 //
sellChannel
fn cf_html_to_plain_html(cf_html: Vec) -> Result { - let cf_html_str = String::from_utf8(cf_html)?; - let cf_html_bytes = cf_html_str.as_bytes(); - let mut start_fragment_offset_in_bytes = 0; - let mut end_fragment_offset_in_bytes = 0; - for line in cf_html_str.lines() { - match line.split_once(':') { - Some((k, v)) => match k { - "StartFragment" => { - start_fragment_offset_in_bytes = v.trim_start_matches('0').parse::()?; - } - "EndFragment" => { - end_fragment_offset_in_bytes = v.trim_start_matches('0').parse::()?; - } - _ => {} - }, - None => { - if start_fragment_offset_in_bytes != 0 && end_fragment_offset_in_bytes != 0 { - return Ok(String::from_utf8( - cf_html_bytes[start_fragment_offset_in_bytes..end_fragment_offset_in_bytes] - .to_vec(), - )?); - } - } - } - } - // if no StartFragment and EndFragment, return the whole html - Ok(cf_html_str) + let cf_html_str = String::from_utf8(cf_html)?; + let cf_html_bytes = cf_html_str.as_bytes(); + let mut start_fragment_offset_in_bytes = 0; + let mut end_fragment_offset_in_bytes = 0; + for line in cf_html_str.lines() { + match line.split_once(':') { + Some((k, v)) => match k { + "StartFragment" => { + start_fragment_offset_in_bytes = v.trim_start_matches('0').parse::()?; + } + "EndFragment" => { + end_fragment_offset_in_bytes = v.trim_start_matches('0').parse::()?; + } + _ => {} + }, + None => { + if start_fragment_offset_in_bytes != 0 && end_fragment_offset_in_bytes != 0 { + return Ok(String::from_utf8( + cf_html_bytes[start_fragment_offset_in_bytes..end_fragment_offset_in_bytes] + .to_vec(), + )?); + } + } + } + } + // if no StartFragment and EndFragment, return the whole html + Ok(cf_html_str) } // cp from https://github.com/Devolutions/IronRDP/blob/37aa6426dba3272f38a2bb46a513144a326854ee/crates/ironrdp-cliprdr-format/src/html.rs#L91 fn plain_html_to_cf_html(fragment: &str) -> String { - const POS_PLACEHOLDER: &str = "0000000000"; + const POS_PLACEHOLDER: &str = "0000000000"; - let mut buffer = String::new(); + let mut buffer = String::new(); - let mut write_header = |key: &str, value: &str| { - let size = key.len() + value.len() + ":\r\n".len(); - buffer.reserve(size); + let mut write_header = |key: &str, value: &str| { + let size = key.len() + value.len() + ":\r\n".len(); + buffer.reserve(size); - buffer.push_str(key); - buffer.push(':'); - let value_pos = buffer.len(); - buffer.push_str(value); - buffer.push_str("\r\n"); + buffer.push_str(key); + buffer.push(':'); + let value_pos = buffer.len(); + buffer.push_str(value); + buffer.push_str("\r\n"); - value_pos - }; + value_pos + }; - write_header("Version", "0.9"); + write_header("Version", "0.9"); - let start_html_header_value_pos = write_header("StartHTML", POS_PLACEHOLDER); - let end_html_header_value_pos = write_header("EndHTML", POS_PLACEHOLDER); - let start_fragment_header_value_pos = write_header("StartFragment", POS_PLACEHOLDER); - let end_fragment_header_value_pos = write_header("EndFragment", POS_PLACEHOLDER); + let start_html_header_value_pos = write_header("StartHTML", POS_PLACEHOLDER); + let end_html_header_value_pos = write_header("EndHTML", POS_PLACEHOLDER); + let start_fragment_header_value_pos = write_header("StartFragment", POS_PLACEHOLDER); + let end_fragment_header_value_pos = write_header("EndFragment", POS_PLACEHOLDER); - let start_html_pos = buffer.len(); - buffer.push_str("\r\n\r\n"); + let start_html_pos = buffer.len(); + buffer.push_str("\r\n\r\n"); - let start_fragment_pos = buffer.len(); - buffer.push_str(fragment); + let start_fragment_pos = buffer.len(); + buffer.push_str(fragment); - let end_fragment_pos = buffer.len(); - buffer.push_str("\r\n\r\n"); + let end_fragment_pos = buffer.len(); + buffer.push_str("\r\n\r\n"); - let end_html_pos = buffer.len(); + let end_html_pos = buffer.len(); - let start_html_pos_value = format!("{:0>10}", start_html_pos); - let end_html_pos_value = format!("{:0>10}", end_html_pos); - let start_fragment_pos_value = format!("{:0>10}", start_fragment_pos); - let end_fragment_pos_value = format!("{:0>10}", end_fragment_pos); + let start_html_pos_value = format!("{:0>10}", start_html_pos); + let end_html_pos_value = format!("{:0>10}", end_html_pos); + let start_fragment_pos_value = format!("{:0>10}", start_fragment_pos); + let end_fragment_pos_value = format!("{:0>10}", end_fragment_pos); - let mut replace_placeholder = |value_begin_idx: usize, header_value: &str| { - let value_end_idx = value_begin_idx + POS_PLACEHOLDER.len(); - buffer.replace_range(value_begin_idx..value_end_idx, header_value); - }; + let mut replace_placeholder = |value_begin_idx: usize, header_value: &str| { + let value_end_idx = value_begin_idx + POS_PLACEHOLDER.len(); + buffer.replace_range(value_begin_idx..value_end_idx, header_value); + }; - replace_placeholder(start_html_header_value_pos, &start_html_pos_value); - replace_placeholder(end_html_header_value_pos, &end_html_pos_value); - replace_placeholder(start_fragment_header_value_pos, &start_fragment_pos_value); - replace_placeholder(end_fragment_header_value_pos, &end_fragment_pos_value); + replace_placeholder(start_html_header_value_pos, &start_html_pos_value); + replace_placeholder(end_html_header_value_pos, &end_html_pos_value); + replace_placeholder(start_fragment_header_value_pos, &start_fragment_pos_value); + replace_placeholder(end_fragment_header_value_pos, &end_fragment_pos_value); - buffer + buffer } /// 将输入的 UTF-8 字符串转换为宽字符(UTF-16)字符串 fn utf8_to_utf16(input: &str) -> Vec { - let mut vec: Vec = input.encode_utf16().collect(); - vec.push(0); - vec + let mut vec: Vec = input.encode_utf16().collect(); + vec.push(0); + vec } diff --git a/src/platform/x11.rs b/src/platform/x11.rs index 80febd1..cc292b7 100644 --- a/src/platform/x11.rs +++ b/src/platform/x11.rs @@ -1,866 +1,866 @@ use crate::{ - common::{Result, RustImage}, - ClipboardContent, ContentFormat, RustImageData, + common::{Result, RustImage}, + ClipboardContent, ContentFormat, RustImageData, }; use crate::{CallBack, Clipboard, ClipboardWatcher}; use std::sync::mpsc::{self, Receiver, Sender}; use std::{ - sync::{Arc, RwLock}, - thread, - time::{Duration, Instant}, + sync::{Arc, RwLock}, + thread, + time::{Duration, Instant}, }; use x11rb::{ - connection::Connection, - protocol::{ - xfixes, - xproto::{ - Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, - SelectionNotifyEvent, SelectionRequestEvent, WindowClass, SELECTION_NOTIFY_EVENT, - }, - Event, - }, - rust_connection::RustConnection, - wrapper::ConnectionExt as _, - COPY_DEPTH_FROM_PARENT, CURRENT_TIME, + connection::Connection, + protocol::{ + xfixes, + xproto::{ + Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property, + SelectionNotifyEvent, SelectionRequestEvent, WindowClass, SELECTION_NOTIFY_EVENT, + }, + Event, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, + COPY_DEPTH_FROM_PARENT, CURRENT_TIME, }; x11rb::atom_manager! { - pub Atoms: AtomCookies { - CLIPBOARD, - CLIPBOARD_MANAGER, - PROPERTY, - SAVE_TARGETS, - TARGETS, - ATOM, - INCR, - TIMESTAMP, - MULTIPLE, - - UTF8_STRING, - UTF8_MIME_0: b"text/plain;charset=utf-8", - UTF8_MIME_1: b"text/plain;charset=UTF-8", - // Text in ISO Latin-1 encoding - // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 - STRING, - // Text in unknown encoding - // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 - TEXT, - TEXT_MIME_UNKNOWN: b"text/plain", - // Rich Text Format - RTF: b"text/rtf", - RTF_1: b"text/richtext", - HTML: b"text/html", - PNG_MIME: b"image/png", - FILE_LIST: b"text/uri-list", - GNOME_COPY_FILES: b"x-special/gnome-copied-files", - NAUTILUS_FILE_LIST: b"x-special/nautilus-clipboard", - } + pub Atoms: AtomCookies { + CLIPBOARD, + CLIPBOARD_MANAGER, + PROPERTY, + SAVE_TARGETS, + TARGETS, + ATOM, + INCR, + TIMESTAMP, + MULTIPLE, + + UTF8_STRING, + UTF8_MIME_0: b"text/plain;charset=utf-8", + UTF8_MIME_1: b"text/plain;charset=UTF-8", + // Text in ISO Latin-1 encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + STRING, + // Text in unknown encoding + // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2 + TEXT, + TEXT_MIME_UNKNOWN: b"text/plain", + // Rich Text Format + RTF: b"text/rtf", + RTF_1: b"text/richtext", + HTML: b"text/html", + PNG_MIME: b"image/png", + FILE_LIST: b"text/uri-list", + GNOME_COPY_FILES: b"x-special/gnome-copied-files", + NAUTILUS_FILE_LIST: b"x-special/nautilus-clipboard", + } } const FILE_PATH_PREFIX: &str = "file://"; pub struct ClipboardContext { - inner: Arc, + inner: Arc, } struct ClipboardData { - format: Atom, - data: Vec, + format: Atom, + data: Vec, } struct InnerContext { - server: XServerContext, - server_for_write: XServerContext, - ignore_formats: Vec, - // 此刻待写入的剪贴板内容 - wait_write_data: RwLock>, + server: XServerContext, + server_for_write: XServerContext, + ignore_formats: Vec, + // 此刻待写入的剪贴板内容 + wait_write_data: RwLock>, } impl InnerContext { - pub fn new() -> Result { - let server = XServerContext::new()?; - let server_for_write = XServerContext::new()?; - let wait_write_data = RwLock::new(Vec::new()); - - let ignore_formats = vec![ - server.atoms.TIMESTAMP, - server.atoms.MULTIPLE, - server.atoms.TARGETS, - server.atoms.SAVE_TARGETS, - ]; - - Ok(Self { - server, - server_for_write, - ignore_formats, - wait_write_data, - }) - } - - pub fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> { - let success; - let ctx = &self.server_for_write; - let atoms = ctx.atoms; - // we are asked for a list of supported conversion targets - if event.target == atoms.TARGETS { - let reader = self.wait_write_data.read(); - match reader { - Ok(data_list) => { - let mut targets = Vec::with_capacity(10); - targets.push(atoms.TARGETS); - targets.push(atoms.SAVE_TARGETS); - if data_list.len() > 0 { - data_list.iter().for_each(|data| { - targets.push(data.format); - }); - } - ctx.conn.change_property32( - PropMode::REPLACE, - event.requestor, - event.property, - AtomEnum::ATOM, - &targets, - )?; - success = true; - } - Err(_) => return Err("Failed to read clipboard data".into()), - } - } else { - let reader = self.wait_write_data.read(); - match reader { - Ok(data_list) => { - success = match data_list.iter().find(|d| d.format == event.target) { - Some(data) => { - ctx.conn.change_property8( - PropMode::REPLACE, - event.requestor, - event.property, - event.target, - &data.data, - )?; - true - } - None => false, - }; - } - Err(_) => return Err("Failed to read clipboard data".into()), - } - } - // on failure we notify the requester of it - let property = if success { - event.property - } else { - AtomEnum::NONE.into() - }; - // tell the requestor that we finished sending data - ctx.conn.send_event( - false, - event.requestor, - EventMask::NO_EVENT, - SelectionNotifyEvent { - response_type: SELECTION_NOTIFY_EVENT, - sequence: event.sequence, - time: event.time, - requestor: event.requestor, - selection: event.selection, - target: event.target, - property, - }, - )?; - ctx.conn.flush()?; - Ok(()) - } - - pub fn process_event( - &self, - buff: &mut Vec, - selection: Atom, - target: Atom, - property: Atom, - timeout: Option, - sequence_number: u64, - ) -> Result<()> { - let mut is_incr = false; - let start_time = if timeout.is_some() { - Some(Instant::now()) - } else { - None - }; - let ctx = &self.server; - let atoms = ctx.atoms; - loop { - if timeout - .into_iter() - .zip(start_time) - .next() - .map(|(timeout, time)| (Instant::now() - time) >= timeout) - .unwrap_or(false) - { - return Err("Timeout while waiting for clipboard data".into()); - } - - let (event, seq) = match ctx.conn.poll_for_event_with_sequence()? { - Some(event) => event, - None => { - thread::park_timeout(Duration::from_millis(50)); - continue; - } - }; - - if seq < sequence_number { - continue; - } - - match event { - Event::SelectionNotify(event) => { - if event.selection != selection { - continue; - }; - - let target_type = { - if target == atoms.TARGETS { - atoms.ATOM - } else { - target - } - }; - - let reply = ctx - .conn - .get_property( - false, - event.requestor, - event.property, - target_type, - buff.len() as u32, - u32::MAX, - )? - .reply()?; - - if reply.type_ == atoms.INCR { - if let Some(mut value) = reply.value32() { - if let Some(size) = value.next() { - buff.reserve(size as usize); - } - } - ctx.conn.delete_property(ctx.win_id, property)?.check()?; - is_incr = true; - continue; - } else if reply.type_ != target && reply.type_ != atoms.ATOM { - return Err("Clipboard data type mismatch".into()); - } - buff.extend_from_slice(&reply.value); - break; - } - - Event::PropertyNotify(event) if is_incr => { - if event.state != Property::NEW_VALUE { - continue; - }; - - let cookie = - ctx.conn - .get_property(false, ctx.win_id, property, AtomEnum::ATOM, 0, 0)?; - - let length = cookie.reply()?.bytes_after; - - let cookie = ctx.conn.get_property( - true, - ctx.win_id, - property, - AtomEnum::NONE, - 0, - length, - )?; - let reply = cookie.reply()?; - if reply.type_ != target { - continue; - }; - - let value = reply.value; - - if !value.is_empty() { - buff.extend_from_slice(&value); - } else { - break; - } - } - _ => (), - } - } - Ok(()) - } + pub fn new() -> Result { + let server = XServerContext::new()?; + let server_for_write = XServerContext::new()?; + let wait_write_data = RwLock::new(Vec::new()); + + let ignore_formats = vec![ + server.atoms.TIMESTAMP, + server.atoms.MULTIPLE, + server.atoms.TARGETS, + server.atoms.SAVE_TARGETS, + ]; + + Ok(Self { + server, + server_for_write, + ignore_formats, + wait_write_data, + }) + } + + pub fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> { + let success; + let ctx = &self.server_for_write; + let atoms = ctx.atoms; + // we are asked for a list of supported conversion targets + if event.target == atoms.TARGETS { + let reader = self.wait_write_data.read(); + match reader { + Ok(data_list) => { + let mut targets = Vec::with_capacity(10); + targets.push(atoms.TARGETS); + targets.push(atoms.SAVE_TARGETS); + if data_list.len() > 0 { + data_list.iter().for_each(|data| { + targets.push(data.format); + }); + } + ctx.conn.change_property32( + PropMode::REPLACE, + event.requestor, + event.property, + AtomEnum::ATOM, + &targets, + )?; + success = true; + } + Err(_) => return Err("Failed to read clipboard data".into()), + } + } else { + let reader = self.wait_write_data.read(); + match reader { + Ok(data_list) => { + success = match data_list.iter().find(|d| d.format == event.target) { + Some(data) => { + ctx.conn.change_property8( + PropMode::REPLACE, + event.requestor, + event.property, + event.target, + &data.data, + )?; + true + } + None => false, + }; + } + Err(_) => return Err("Failed to read clipboard data".into()), + } + } + // on failure we notify the requester of it + let property = if success { + event.property + } else { + AtomEnum::NONE.into() + }; + // tell the requestor that we finished sending data + ctx.conn.send_event( + false, + event.requestor, + EventMask::NO_EVENT, + SelectionNotifyEvent { + response_type: SELECTION_NOTIFY_EVENT, + sequence: event.sequence, + time: event.time, + requestor: event.requestor, + selection: event.selection, + target: event.target, + property, + }, + )?; + ctx.conn.flush()?; + Ok(()) + } + + pub fn process_event( + &self, + buff: &mut Vec, + selection: Atom, + target: Atom, + property: Atom, + timeout: Option, + sequence_number: u64, + ) -> Result<()> { + let mut is_incr = false; + let start_time = if timeout.is_some() { + Some(Instant::now()) + } else { + None + }; + let ctx = &self.server; + let atoms = ctx.atoms; + loop { + if timeout + .into_iter() + .zip(start_time) + .next() + .map(|(timeout, time)| (Instant::now() - time) >= timeout) + .unwrap_or(false) + { + return Err("Timeout while waiting for clipboard data".into()); + } + + let (event, seq) = match ctx.conn.poll_for_event_with_sequence()? { + Some(event) => event, + None => { + thread::park_timeout(Duration::from_millis(50)); + continue; + } + }; + + if seq < sequence_number { + continue; + } + + match event { + Event::SelectionNotify(event) => { + if event.selection != selection { + continue; + }; + + let target_type = { + if target == atoms.TARGETS { + atoms.ATOM + } else { + target + } + }; + + let reply = ctx + .conn + .get_property( + false, + event.requestor, + event.property, + target_type, + buff.len() as u32, + u32::MAX, + )? + .reply()?; + + if reply.type_ == atoms.INCR { + if let Some(mut value) = reply.value32() { + if let Some(size) = value.next() { + buff.reserve(size as usize); + } + } + ctx.conn.delete_property(ctx.win_id, property)?.check()?; + is_incr = true; + continue; + } else if reply.type_ != target && reply.type_ != atoms.ATOM { + return Err("Clipboard data type mismatch".into()); + } + buff.extend_from_slice(&reply.value); + break; + } + + Event::PropertyNotify(event) if is_incr => { + if event.state != Property::NEW_VALUE { + continue; + }; + + let cookie = + ctx.conn + .get_property(false, ctx.win_id, property, AtomEnum::ATOM, 0, 0)?; + + let length = cookie.reply()?.bytes_after; + + let cookie = ctx.conn.get_property( + true, + ctx.win_id, + property, + AtomEnum::NONE, + 0, + length, + )?; + let reply = cookie.reply()?; + if reply.type_ != target { + continue; + }; + + let value = reply.value; + + if !value.is_empty() { + buff.extend_from_slice(&value); + } else { + break; + } + } + _ => (), + } + } + Ok(()) + } } impl ClipboardContext { - pub fn new() -> Result { - // build connection to X server - let ctx = InnerContext::new()?; - let ctx_arc = Arc::new(ctx); - let ctx_clone = ctx_arc.clone(); - - thread::spawn(move || { - let res = process_server_req(&ctx_clone); - if let Err(e) = res { - println!("process_server_req error: {:?}", e); - } - }); - Ok(Self { inner: ctx_arc }) - } - - fn read(&self, format: &Atom) -> Result> { - let ctx = &self.inner.server; - let atoms = ctx.atoms; - let clipboard = atoms.CLIPBOARD; - let win_id = ctx.win_id; - let cookie = - ctx.conn - .convert_selection(win_id, clipboard, *format, atoms.PROPERTY, CURRENT_TIME)?; - let sequence_num = cookie.sequence_number(); - cookie.check()?; - let mut buff = Vec::new(); - - self.inner.process_event( - &mut buff, - clipboard, - *format, - atoms.PROPERTY, - None, - sequence_num, - )?; - - ctx.conn.delete_property(win_id, atoms.PROPERTY)?.check()?; - - Ok(buff) - } - - fn write(&self, data: Vec) -> Result<()> { - let writer = self.inner.wait_write_data.write(); - match writer { - Ok(mut writer) => { - writer.clear(); - writer.extend(data); - } - Err(_) => return Err("Failed to write clipboard data".into()), - } - let ctx = &self.inner.server_for_write; - let atoms = ctx.atoms; - - let win_id = ctx.win_id; - let clipboard = atoms.CLIPBOARD; - ctx.conn - .set_selection_owner(win_id, clipboard, CURRENT_TIME)? - .check()?; - - if ctx - .conn - .get_selection_owner(clipboard)? - .reply() - .map(|reply| reply.owner == win_id) - .unwrap_or(false) - { - Ok(()) - } else { - Err("Failed to take ownership of the clipboard".into()) - } - } + pub fn new() -> Result { + // build connection to X server + let ctx = InnerContext::new()?; + let ctx_arc = Arc::new(ctx); + let ctx_clone = ctx_arc.clone(); + + thread::spawn(move || { + let res = process_server_req(&ctx_clone); + if let Err(e) = res { + println!("process_server_req error: {:?}", e); + } + }); + Ok(Self { inner: ctx_arc }) + } + + fn read(&self, format: &Atom) -> Result> { + let ctx = &self.inner.server; + let atoms = ctx.atoms; + let clipboard = atoms.CLIPBOARD; + let win_id = ctx.win_id; + let cookie = + ctx.conn + .convert_selection(win_id, clipboard, *format, atoms.PROPERTY, CURRENT_TIME)?; + let sequence_num = cookie.sequence_number(); + cookie.check()?; + let mut buff = Vec::new(); + + self.inner.process_event( + &mut buff, + clipboard, + *format, + atoms.PROPERTY, + None, + sequence_num, + )?; + + ctx.conn.delete_property(win_id, atoms.PROPERTY)?.check()?; + + Ok(buff) + } + + fn write(&self, data: Vec) -> Result<()> { + let writer = self.inner.wait_write_data.write(); + match writer { + Ok(mut writer) => { + writer.clear(); + writer.extend(data); + } + Err(_) => return Err("Failed to write clipboard data".into()), + } + let ctx = &self.inner.server_for_write; + let atoms = ctx.atoms; + + let win_id = ctx.win_id; + let clipboard = atoms.CLIPBOARD; + ctx.conn + .set_selection_owner(win_id, clipboard, CURRENT_TIME)? + .check()?; + + if ctx + .conn + .get_selection_owner(clipboard)? + .reply() + .map(|reply| reply.owner == win_id) + .unwrap_or(false) + { + Ok(()) + } else { + Err("Failed to take ownership of the clipboard".into()) + } + } } fn process_server_req(context: &InnerContext) -> Result<()> { - let atoms = context.server_for_write.atoms; - loop { - match context - .server_for_write - .conn - .wait_for_event() - .map_err(|e| format!("wait_for_event error: {:?}", e))? - { - Event::DestroyNotify(_) => { - // This window is being destroyed. - println!("Clipboard server window is being destroyed x_x"); - break; - } - Event::SelectionClear(event) => { - // Someone else has new content in the clipboard, so it is - // notifying us that we should delete our data now. - println!("Somebody else owns the clipboard now"); - if event.selection == atoms.CLIPBOARD { - // Clear the clipboard contents - context - .wait_write_data - .write() - .map(|mut writer| writer.clear()) - .map_err(|e| format!("write clipboard data error: {:?}", e))?; - } - } - Event::SelectionRequest(event) => { - // Someone is requesting the clipboard content from us. - context - .handle_selection_request(event) - .map_err(|e| format!("handle_selection_request error: {:?}", e))?; - } - Event::SelectionNotify(event) => { - // We've requested the clipboard content and this is the answer. - // Considering that this thread is not responsible for reading - // clipboard contents, this must come from the clipboard manager - // signaling that the data was handed over successfully. - if event.selection != atoms.CLIPBOARD_MANAGER { - println!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread."); - continue; - } - } - _event => { - // May be useful for debugging but nothing else really. - // trace!("Received unwanted event: {:?}", event); - } - } - } - Ok(()) + let atoms = context.server_for_write.atoms; + loop { + match context + .server_for_write + .conn + .wait_for_event() + .map_err(|e| format!("wait_for_event error: {:?}", e))? + { + Event::DestroyNotify(_) => { + // This window is being destroyed. + println!("Clipboard server window is being destroyed x_x"); + break; + } + Event::SelectionClear(event) => { + // Someone else has new content in the clipboard, so it is + // notifying us that we should delete our data now. + println!("Somebody else owns the clipboard now"); + if event.selection == atoms.CLIPBOARD { + // Clear the clipboard contents + context + .wait_write_data + .write() + .map(|mut writer| writer.clear()) + .map_err(|e| format!("write clipboard data error: {:?}", e))?; + } + } + Event::SelectionRequest(event) => { + // Someone is requesting the clipboard content from us. + context + .handle_selection_request(event) + .map_err(|e| format!("handle_selection_request error: {:?}", e))?; + } + Event::SelectionNotify(event) => { + // We've requested the clipboard content and this is the answer. + // Considering that this thread is not responsible for reading + // clipboard contents, this must come from the clipboard manager + // signaling that the data was handed over successfully. + if event.selection != atoms.CLIPBOARD_MANAGER { + println!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread."); + continue; + } + } + _event => { + // May be useful for debugging but nothing else really. + // trace!("Received unwanted event: {:?}", event); + } + } + } + Ok(()) } impl Clipboard for ClipboardContext { - fn available_formats(&self) -> Result> { - let ctx = &self.inner.server; - let atoms = ctx.atoms; - self.read(&atoms.TARGETS).map(|data| { - let mut formats = Vec::new(); - // 解析原子标识符列表 - let atom_list: Vec = data - .chunks(4) - .map(|chunk| { - let mut bytes = [0u8; 4]; - bytes.copy_from_slice(chunk); - u32::from_ne_bytes(bytes) - }) - .collect(); - for atom in atom_list { - if self.inner.ignore_formats.contains(&atom) { - continue; - } - let atom_name = ctx.get_atom_name(atom).unwrap_or("Unknown".to_string()); - formats.push(atom_name); - } - formats - }) - } - - fn has(&self, format: crate::ContentFormat) -> bool { - let ctx = &self.inner.server; - let atoms = ctx.atoms; - let atom_list = self.read(&atoms.TARGETS).map(|data| { - let atom_list: Vec = data - .chunks(4) - .map(|chunk| { - let mut bytes = [0u8; 4]; - bytes.copy_from_slice(chunk); - u32::from_ne_bytes(bytes) - }) - .collect(); - atom_list - }); - match atom_list { - Ok(formats) => match format { - ContentFormat::Text => formats.contains(&atoms.UTF8_STRING), - ContentFormat::Rtf => formats.contains(&atoms.RTF), - ContentFormat::Html => formats.contains(&atoms.HTML), - ContentFormat::Image => formats.contains(&atoms.PNG_MIME), - ContentFormat::Files => formats.contains(&atoms.FILE_LIST), - ContentFormat::Other(format_name) => { - let atom = ctx.get_atom(format_name.as_str()); - match atom { - Ok(atom) => formats.contains(&atom), - Err(_) => false, - } - } - }, - Err(_) => false, - } - } - - fn clear(&self) -> Result<()> { - self.write(vec![]) - } - - fn get_buffer(&self, format: &str) -> Result> { - let atom = self.inner.server.get_atom(format); - match atom { - Ok(atom) => self.read(&atom), - Err(_) => Err("Invalid format".into()), - } - } - - fn get_text(&self) -> Result { - let atoms = self.inner.server.atoms; - let text_data = self.read(&atoms.UTF8_STRING); - match text_data { - Ok(data) => { - let text = String::from_utf8_lossy(&data).to_string(); - Ok(text) - } - Err(_) => Ok("".to_string()), - } - } - - fn get_rich_text(&self) -> Result { - let atoms = self.inner.server.atoms; - let rtf_data = self.read(&atoms.RTF); - match rtf_data { - Ok(data) => { - let rtf = String::from_utf8_lossy(&data).to_string(); - Ok(rtf) - } - Err(_) => Ok("".to_string()), - } - } - - fn get_html(&self) -> Result { - let atoms = self.inner.server.atoms; - let html_data = self.read(&atoms.HTML); - match html_data { - Ok(data) => { - let html = String::from_utf8_lossy(&data).to_string(); - Ok(html) - } - Err(_) => Ok("".to_string()), - } - } - - fn get_image(&self) -> Result { - let atoms = self.inner.server.atoms; - let image_bytes = self.read(&atoms.PNG_MIME); - match image_bytes { - Ok(bytes) => { - let image = RustImageData::from_bytes(&bytes); - match image { - Ok(image) => Ok(image), - Err(_) => Err("Invalid image data".into()), - } - } - Err(_) => Err("No image data found".into()), - } - } - - fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()> { - let atom = self.inner.server_for_write.get_atom(format)?; - let data = ClipboardData { - format: atom, - data: buffer, - }; - self.write(vec![data]) - } - - fn set_text(&self, text: String) -> Result<()> { - let atoms = self.inner.server_for_write.atoms; - let text_bytes = text.as_bytes().to_vec(); - - let data = ClipboardData { - format: atoms.UTF8_STRING, - data: text_bytes, - }; - self.write(vec![data]) - } - - fn set_rich_text(&self, text: String) -> Result<()> { - let atoms = self.inner.server_for_write.atoms; - let text_bytes = text.as_bytes().to_vec(); - - let data = ClipboardData { - format: atoms.RTF, - data: text_bytes, - }; - self.write(vec![data]) - } - - fn set_html(&self, html: String) -> Result<()> { - let atoms = self.inner.server_for_write.atoms; - let html_bytes = html.as_bytes().to_vec(); - - let data = ClipboardData { - format: atoms.HTML, - data: html_bytes, - }; - self.write(vec![data]) - } - - fn set_image(&self, image: crate::RustImageData) -> Result<()> { - let atoms = self.inner.server_for_write.atoms; - let image_png = image.to_png()?; - let data = ClipboardData { - format: atoms.PNG_MIME, - data: image_png.get_bytes().to_vec(), - }; - self.write(vec![data]) - } - - fn get_files(&self) -> Result> { - let atoms = self.inner.server.atoms; - let file_list_data = self.read(&atoms.FILE_LIST); - match file_list_data { - Ok(data) => { - let file_list_str = String::from_utf8_lossy(&data).to_string(); - let mut list = Vec::new(); - for line in file_list_str.lines() { - if !line.starts_with(FILE_PATH_PREFIX) { - continue; - } - list.push(line.to_string()) - } - Ok(list) - } - Err(_) => Ok(vec![]), - } - } - - fn get(&self, formats: &[ContentFormat]) -> Result> { - let mut contents = Vec::new(); - for format in formats { - match format { - ContentFormat::Text => match self.get_text() { - Ok(text) => contents.push(ClipboardContent::Text(text)), - Err(_) => continue, - }, - ContentFormat::Rtf => match self.get_rich_text() { - Ok(rtf) => contents.push(ClipboardContent::Rtf(rtf)), - Err(_) => continue, - }, - ContentFormat::Html => match self.get_html() { - Ok(html) => contents.push(ClipboardContent::Html(html)), - Err(_) => continue, - }, - ContentFormat::Image => match self.get_image() { - Ok(image) => contents.push(ClipboardContent::Image(image)), - Err(_) => continue, - }, - ContentFormat::Files => match self.get_files() { - Ok(files) => contents.push(ClipboardContent::Files(files)), - Err(_) => continue, - }, - ContentFormat::Other(format_name) => match self.get_buffer(format_name) { - Ok(buffer) => { - contents.push(ClipboardContent::Other(format_name.clone(), buffer)) - } - Err(_) => continue, - }, - } - } - Ok(contents) - } - - fn set_files(&self, files: Vec) -> Result<()> { - let atoms = self.inner.server_for_write.atoms; - let data = file_uri_list_to_clipboard_data(files, atoms); - self.write(data) - } - - fn set(&self, contents: Vec) -> Result<()> { - let mut data = Vec::new(); - let atoms = self.inner.server_for_write.atoms; - for content in contents { - match content { - ClipboardContent::Text(text) => { - data.push(ClipboardData { - format: atoms.UTF8_STRING, - data: text.as_bytes().to_vec(), - }); - } - ClipboardContent::Rtf(rtf) => { - data.push(ClipboardData { - format: atoms.RTF, - data: rtf.as_bytes().to_vec(), - }); - } - ClipboardContent::Html(html) => { - data.push(ClipboardData { - format: atoms.HTML, - data: html.as_bytes().to_vec(), - }); - } - ClipboardContent::Image(image) => { - let image_png = image.to_png()?; - data.push(ClipboardData { - format: atoms.PNG_MIME, - data: image_png.get_bytes().to_vec(), - }); - } - ClipboardContent::Files(files) => { - let data_arr = file_uri_list_to_clipboard_data(files, atoms); - data.extend(data_arr); - } - ClipboardContent::Other(format_name, buffer) => { - let atom = self.inner.server_for_write.get_atom(&format_name)?; - data.push(ClipboardData { - format: atom, - data: buffer, - }); - } - } - } - self.write(data) - } + fn available_formats(&self) -> Result> { + let ctx = &self.inner.server; + let atoms = ctx.atoms; + self.read(&atoms.TARGETS).map(|data| { + let mut formats = Vec::new(); + // 解析原子标识符列表 + let atom_list: Vec = data + .chunks(4) + .map(|chunk| { + let mut bytes = [0u8; 4]; + bytes.copy_from_slice(chunk); + u32::from_ne_bytes(bytes) + }) + .collect(); + for atom in atom_list { + if self.inner.ignore_formats.contains(&atom) { + continue; + } + let atom_name = ctx.get_atom_name(atom).unwrap_or("Unknown".to_string()); + formats.push(atom_name); + } + formats + }) + } + + fn has(&self, format: crate::ContentFormat) -> bool { + let ctx = &self.inner.server; + let atoms = ctx.atoms; + let atom_list = self.read(&atoms.TARGETS).map(|data| { + let atom_list: Vec = data + .chunks(4) + .map(|chunk| { + let mut bytes = [0u8; 4]; + bytes.copy_from_slice(chunk); + u32::from_ne_bytes(bytes) + }) + .collect(); + atom_list + }); + match atom_list { + Ok(formats) => match format { + ContentFormat::Text => formats.contains(&atoms.UTF8_STRING), + ContentFormat::Rtf => formats.contains(&atoms.RTF), + ContentFormat::Html => formats.contains(&atoms.HTML), + ContentFormat::Image => formats.contains(&atoms.PNG_MIME), + ContentFormat::Files => formats.contains(&atoms.FILE_LIST), + ContentFormat::Other(format_name) => { + let atom = ctx.get_atom(format_name.as_str()); + match atom { + Ok(atom) => formats.contains(&atom), + Err(_) => false, + } + } + }, + Err(_) => false, + } + } + + fn clear(&self) -> Result<()> { + self.write(vec![]) + } + + fn get_buffer(&self, format: &str) -> Result> { + let atom = self.inner.server.get_atom(format); + match atom { + Ok(atom) => self.read(&atom), + Err(_) => Err("Invalid format".into()), + } + } + + fn get_text(&self) -> Result { + let atoms = self.inner.server.atoms; + let text_data = self.read(&atoms.UTF8_STRING); + match text_data { + Ok(data) => { + let text = String::from_utf8_lossy(&data).to_string(); + Ok(text) + } + Err(_) => Ok("".to_string()), + } + } + + fn get_rich_text(&self) -> Result { + let atoms = self.inner.server.atoms; + let rtf_data = self.read(&atoms.RTF); + match rtf_data { + Ok(data) => { + let rtf = String::from_utf8_lossy(&data).to_string(); + Ok(rtf) + } + Err(_) => Ok("".to_string()), + } + } + + fn get_html(&self) -> Result { + let atoms = self.inner.server.atoms; + let html_data = self.read(&atoms.HTML); + match html_data { + Ok(data) => { + let html = String::from_utf8_lossy(&data).to_string(); + Ok(html) + } + Err(_) => Ok("".to_string()), + } + } + + fn get_image(&self) -> Result { + let atoms = self.inner.server.atoms; + let image_bytes = self.read(&atoms.PNG_MIME); + match image_bytes { + Ok(bytes) => { + let image = RustImageData::from_bytes(&bytes); + match image { + Ok(image) => Ok(image), + Err(_) => Err("Invalid image data".into()), + } + } + Err(_) => Err("No image data found".into()), + } + } + + fn set_buffer(&self, format: &str, buffer: Vec) -> Result<()> { + let atom = self.inner.server_for_write.get_atom(format)?; + let data = ClipboardData { + format: atom, + data: buffer, + }; + self.write(vec![data]) + } + + fn set_text(&self, text: String) -> Result<()> { + let atoms = self.inner.server_for_write.atoms; + let text_bytes = text.as_bytes().to_vec(); + + let data = ClipboardData { + format: atoms.UTF8_STRING, + data: text_bytes, + }; + self.write(vec![data]) + } + + fn set_rich_text(&self, text: String) -> Result<()> { + let atoms = self.inner.server_for_write.atoms; + let text_bytes = text.as_bytes().to_vec(); + + let data = ClipboardData { + format: atoms.RTF, + data: text_bytes, + }; + self.write(vec![data]) + } + + fn set_html(&self, html: String) -> Result<()> { + let atoms = self.inner.server_for_write.atoms; + let html_bytes = html.as_bytes().to_vec(); + + let data = ClipboardData { + format: atoms.HTML, + data: html_bytes, + }; + self.write(vec![data]) + } + + fn set_image(&self, image: crate::RustImageData) -> Result<()> { + let atoms = self.inner.server_for_write.atoms; + let image_png = image.to_png()?; + let data = ClipboardData { + format: atoms.PNG_MIME, + data: image_png.get_bytes().to_vec(), + }; + self.write(vec![data]) + } + + fn get_files(&self) -> Result> { + let atoms = self.inner.server.atoms; + let file_list_data = self.read(&atoms.FILE_LIST); + match file_list_data { + Ok(data) => { + let file_list_str = String::from_utf8_lossy(&data).to_string(); + let mut list = Vec::new(); + for line in file_list_str.lines() { + if !line.starts_with(FILE_PATH_PREFIX) { + continue; + } + list.push(line.to_string()) + } + Ok(list) + } + Err(_) => Ok(vec![]), + } + } + + fn get(&self, formats: &[ContentFormat]) -> Result> { + let mut contents = Vec::new(); + for format in formats { + match format { + ContentFormat::Text => match self.get_text() { + Ok(text) => contents.push(ClipboardContent::Text(text)), + Err(_) => continue, + }, + ContentFormat::Rtf => match self.get_rich_text() { + Ok(rtf) => contents.push(ClipboardContent::Rtf(rtf)), + Err(_) => continue, + }, + ContentFormat::Html => match self.get_html() { + Ok(html) => contents.push(ClipboardContent::Html(html)), + Err(_) => continue, + }, + ContentFormat::Image => match self.get_image() { + Ok(image) => contents.push(ClipboardContent::Image(image)), + Err(_) => continue, + }, + ContentFormat::Files => match self.get_files() { + Ok(files) => contents.push(ClipboardContent::Files(files)), + Err(_) => continue, + }, + ContentFormat::Other(format_name) => match self.get_buffer(format_name) { + Ok(buffer) => { + contents.push(ClipboardContent::Other(format_name.clone(), buffer)) + } + Err(_) => continue, + }, + } + } + Ok(contents) + } + + fn set_files(&self, files: Vec) -> Result<()> { + let atoms = self.inner.server_for_write.atoms; + let data = file_uri_list_to_clipboard_data(files, atoms); + self.write(data) + } + + fn set(&self, contents: Vec) -> Result<()> { + let mut data = Vec::new(); + let atoms = self.inner.server_for_write.atoms; + for content in contents { + match content { + ClipboardContent::Text(text) => { + data.push(ClipboardData { + format: atoms.UTF8_STRING, + data: text.as_bytes().to_vec(), + }); + } + ClipboardContent::Rtf(rtf) => { + data.push(ClipboardData { + format: atoms.RTF, + data: rtf.as_bytes().to_vec(), + }); + } + ClipboardContent::Html(html) => { + data.push(ClipboardData { + format: atoms.HTML, + data: html.as_bytes().to_vec(), + }); + } + ClipboardContent::Image(image) => { + let image_png = image.to_png()?; + data.push(ClipboardData { + format: atoms.PNG_MIME, + data: image_png.get_bytes().to_vec(), + }); + } + ClipboardContent::Files(files) => { + let data_arr = file_uri_list_to_clipboard_data(files, atoms); + data.extend(data_arr); + } + ClipboardContent::Other(format_name, buffer) => { + let atom = self.inner.server_for_write.get_atom(&format_name)?; + data.push(ClipboardData { + format: atom, + data: buffer, + }); + } + } + } + self.write(data) + } } pub struct ClipboardWatcherContext { - handlers: Vec, - stop_signal: Sender<()>, - stop_receiver: Receiver<()>, + handlers: Vec, + stop_signal: Sender<()>, + stop_receiver: Receiver<()>, } impl ClipboardWatcherContext { - pub fn new() -> Result { - let (tx, rx) = mpsc::channel(); - Ok(Self { - handlers: Vec::new(), - stop_signal: tx, - stop_receiver: rx, - }) - } + pub fn new() -> Result { + let (tx, rx) = mpsc::channel(); + Ok(Self { + handlers: Vec::new(), + stop_signal: tx, + stop_receiver: rx, + }) + } } impl ClipboardWatcher for ClipboardWatcherContext { - fn add_handler(&mut self, f: crate::CallBack) -> &mut Self { - self.handlers.push(f); - self - } - - fn start_watch(&mut self) { - let watch_server = XServerContext::new().expect("Failed to create X server context"); - let screen = watch_server - .conn - .setup() - .roots - .get(watch_server._screen) - .expect("Failed to get screen"); - - xfixes::query_version(&watch_server.conn, 5, 0) - .expect("Failed to query version xfixes is not available"); - let cookie = xfixes::select_selection_input( - &watch_server.conn, - screen.root, - watch_server.atoms.CLIPBOARD, - xfixes::SelectionEventMask::SET_SELECTION_OWNER - | xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE - | xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY, - ) - .expect("Failed to select selection input"); - - cookie.check().unwrap(); - - loop { - if self - .stop_receiver - .recv_timeout(Duration::from_millis(500)) - .is_ok() - { - break; - } - let event = match watch_server - .conn - .poll_for_event() - .expect("Failed to poll for event") - { - Some(event) => event, - None => { - continue; - } - }; - if let Event::XfixesSelectionNotify(_) = event { - self.handlers.iter().for_each(|f| f()); - } - } - } - - fn get_shutdown_channel(&self) -> WatcherShutdown { - WatcherShutdown { - sender: self.stop_signal.clone(), - } - } + fn add_handler(&mut self, f: crate::CallBack) -> &mut Self { + self.handlers.push(f); + self + } + + fn start_watch(&mut self) { + let watch_server = XServerContext::new().expect("Failed to create X server context"); + let screen = watch_server + .conn + .setup() + .roots + .get(watch_server._screen) + .expect("Failed to get screen"); + + xfixes::query_version(&watch_server.conn, 5, 0) + .expect("Failed to query version xfixes is not available"); + let cookie = xfixes::select_selection_input( + &watch_server.conn, + screen.root, + watch_server.atoms.CLIPBOARD, + xfixes::SelectionEventMask::SET_SELECTION_OWNER + | xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE + | xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY, + ) + .expect("Failed to select selection input"); + + cookie.check().unwrap(); + + loop { + if self + .stop_receiver + .recv_timeout(Duration::from_millis(500)) + .is_ok() + { + break; + } + let event = match watch_server + .conn + .poll_for_event() + .expect("Failed to poll for event") + { + Some(event) => event, + None => { + continue; + } + }; + if let Event::XfixesSelectionNotify(_) = event { + self.handlers.iter().for_each(|f| f()); + } + } + } + + fn get_shutdown_channel(&self) -> WatcherShutdown { + WatcherShutdown { + sender: self.stop_signal.clone(), + } + } } pub struct WatcherShutdown { - sender: Sender<()>, + sender: Sender<()>, } impl Drop for WatcherShutdown { - fn drop(&mut self) { - let _ = self.sender.send(()); - } + fn drop(&mut self) { + let _ = self.sender.send(()); + } } struct XServerContext { - conn: RustConnection, - win_id: u32, - _screen: usize, - atoms: Atoms, + conn: RustConnection, + win_id: u32, + _screen: usize, + atoms: Atoms, } impl XServerContext { - fn new() -> Result { - let (conn, screen) = x11rb::connect(None)?; - let win_id = conn.generate_id()?; - { - let screen = conn.setup().roots.get(screen).unwrap(); - conn.create_window( - COPY_DEPTH_FROM_PARENT, - win_id, - screen.root, - 0, - 0, - 1, - 1, - 0, - WindowClass::INPUT_OUTPUT, - screen.root_visual, - &CreateWindowAux::new() - .event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE), - )? - .check()?; - } - let atoms = Atoms::new(&conn)?.reply()?; - Ok(Self { - conn, - win_id, - _screen: screen, - atoms, - }) - } - - fn get_atom(&self, format: &str) -> Result { - let cookie = self.conn.intern_atom(false, format.as_bytes())?; - Ok(cookie.reply()?.atom) - } - - fn get_atom_name(&self, atom: Atom) -> Result { - let cookie = self.conn.get_atom_name(atom)?; - Ok(String::from_utf8_lossy(&cookie.reply()?.name).to_string()) - } + fn new() -> Result { + let (conn, screen) = x11rb::connect(None)?; + let win_id = conn.generate_id()?; + { + let screen = conn.setup().roots.get(screen).unwrap(); + conn.create_window( + COPY_DEPTH_FROM_PARENT, + win_id, + screen.root, + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_OUTPUT, + screen.root_visual, + &CreateWindowAux::new() + .event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE), + )? + .check()?; + } + let atoms = Atoms::new(&conn)?.reply()?; + Ok(Self { + conn, + win_id, + _screen: screen, + atoms, + }) + } + + fn get_atom(&self, format: &str) -> Result { + let cookie = self.conn.intern_atom(false, format.as_bytes())?; + Ok(cookie.reply()?.atom) + } + + fn get_atom_name(&self, atom: Atom) -> Result { + let cookie = self.conn.get_atom_name(atom)?; + Ok(String::from_utf8_lossy(&cookie.reply()?.name).to_string()) + } } fn file_uri_list_to_clipboard_data(file_list: Vec, atoms: Atoms) -> Vec { - let uri_list: Vec = file_list - .iter() - .map(|f| { - if f.starts_with(FILE_PATH_PREFIX) { - f.to_owned() - } else { - format!("{}{}", FILE_PATH_PREFIX, f) - } - }) - .collect(); - let uri_list = uri_list.join("\n"); - let text_uri_list_data = uri_list.as_bytes().to_vec(); - let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat(); - - vec![ - ClipboardData { - format: atoms.FILE_LIST, - data: text_uri_list_data, - }, - ClipboardData { - format: atoms.GNOME_COPY_FILES, - data: gnome_copied_files_data.clone(), - }, - ClipboardData { - format: atoms.NAUTILUS_FILE_LIST, - data: gnome_copied_files_data, - }, - ] + let uri_list: Vec = file_list + .iter() + .map(|f| { + if f.starts_with(FILE_PATH_PREFIX) { + f.to_owned() + } else { + format!("{}{}", FILE_PATH_PREFIX, f) + } + }) + .collect(); + let uri_list = uri_list.join("\n"); + let text_uri_list_data = uri_list.as_bytes().to_vec(); + let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat(); + + vec![ + ClipboardData { + format: atoms.FILE_LIST, + data: text_uri_list_data, + }, + ClipboardData { + format: atoms.GNOME_COPY_FILES, + data: gnome_copied_files_data.clone(), + }, + ClipboardData { + format: atoms.NAUTILUS_FILE_LIST, + data: gnome_copied_files_data, + }, + ] } diff --git a/tests/file_test.rs b/tests/file_test.rs index 7ef2596..9a654d0 100644 --- a/tests/file_test.rs +++ b/tests/file_test.rs @@ -5,47 +5,47 @@ const TMP_PATH: &str = "/tmp/"; #[cfg(target_os = "windows")] const TMP_PATH: &str = "C:\\Windows\\Temp\\"; #[cfg(all( - unix, - not(any( - target_os = "macos", - target_os = "ios", - target_os = "android", - target_os = "emscripten" - )) + unix, + not(any( + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "emscripten" + )) ))] const TMP_PATH: &str = "/tmp/"; #[test] fn test_file() { - let ctx = ClipboardContext::new().unwrap(); + let ctx = ClipboardContext::new().unwrap(); - let files = get_files(); + let files = get_files(); - ctx.set_files(files).unwrap(); + ctx.set_files(files).unwrap(); - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); - let has = ctx.has(ContentFormat::Files); - assert_eq!(has, true); + let has = ctx.has(ContentFormat::Files); + assert_eq!(has, true); - let files = ctx.get_files().unwrap(); - assert_eq!(files.len(), 2); + let files = ctx.get_files().unwrap(); + assert_eq!(files.len(), 2); - for file in files { - println!("{:?}", file); - } + for file in files { + println!("{:?}", file); + } - ctx.clear().unwrap(); + ctx.clear().unwrap(); - let has = ctx.has(ContentFormat::Files); - assert_eq!(has, false); + let has = ctx.has(ContentFormat::Files); + assert_eq!(has, false); } fn get_files() -> Vec { - let test_file1 = format!("{}clipboard_rs_test_file1.txt", TMP_PATH); - let test_file2 = format!("{}clipboard_rs_test_file2.txt", TMP_PATH); - std::fs::write(&test_file1, "hello world").unwrap(); - std::fs::write(&test_file2, "hello world").unwrap(); - vec![test_file1, test_file2] + let test_file1 = format!("{}clipboard_rs_test_file1.txt", TMP_PATH); + let test_file2 = format!("{}clipboard_rs_test_file2.txt", TMP_PATH); + std::fs::write(&test_file1, "hello world").unwrap(); + std::fs::write(&test_file2, "hello world").unwrap(); + vec![test_file1, test_file2] } diff --git a/tests/image_test.rs b/tests/image_test.rs index f860cf1..1785844 100644 --- a/tests/image_test.rs +++ b/tests/image_test.rs @@ -1,34 +1,34 @@ use clipboard_rs::{ - common::{RustImage, RustImageData}, - Clipboard, ClipboardContext, ContentFormat, + common::{RustImage, RustImageData}, + Clipboard, ClipboardContext, ContentFormat, }; use image::{ImageBuffer, Rgba, RgbaImage}; use std::io::Cursor; #[test] fn test_image() { - let ctx = ClipboardContext::new().unwrap(); + let ctx = ClipboardContext::new().unwrap(); - // 创建一个 100x100 大小的纯红色图像 - let width = 100; - let height = 100; - let image_buffer: RgbaImage = - ImageBuffer::from_fn(width, height, |_x, _y| Rgba([255u8, 0u8, 0u8, 255u8])); - let mut buf = Cursor::new(Vec::new()); - image_buffer - .write_to(&mut buf, image::ImageOutputFormat::Png) - .expect("Failed to encode image as PNG"); + // 创建一个 100x100 大小的纯红色图像 + let width = 100; + let height = 100; + let image_buffer: RgbaImage = + ImageBuffer::from_fn(width, height, |_x, _y| Rgba([255u8, 0u8, 0u8, 255u8])); + let mut buf = Cursor::new(Vec::new()); + image_buffer + .write_to(&mut buf, image::ImageOutputFormat::Png) + .expect("Failed to encode image as PNG"); - let rust_img = RustImageData::from_bytes(&buf.clone().into_inner()).unwrap(); + let rust_img = RustImageData::from_bytes(&buf.clone().into_inner()).unwrap(); - ctx.set_image(rust_img).unwrap(); + ctx.set_image(rust_img).unwrap(); - assert!(ctx.has(ContentFormat::Image)); + assert!(ctx.has(ContentFormat::Image)); - let clipboard_img = ctx.get_image().unwrap(); + let clipboard_img = ctx.get_image().unwrap(); - assert_eq!( - clipboard_img.to_png().unwrap().get_bytes(), - &buf.into_inner() - ); + assert_eq!( + clipboard_img.to_png().unwrap().get_bytes(), + &buf.into_inner() + ); } diff --git a/tests/string_test.rs b/tests/string_test.rs index ff2959a..9bc389a 100644 --- a/tests/string_test.rs +++ b/tests/string_test.rs @@ -1,54 +1,54 @@ use clipboard_rs::{ - common::ContentData, Clipboard, ClipboardContent, ClipboardContext, ContentFormat, + common::ContentData, Clipboard, ClipboardContent, ClipboardContext, ContentFormat, }; #[test] fn test_string() { - let ctx = ClipboardContext::new().unwrap(); - - let types = ctx.available_formats().unwrap(); - println!("{:?}", types); - - let test_plain_txt = "hell@$#%^&U都98好的😊o Rust!!!"; - ctx.set_text(test_plain_txt.to_string()).unwrap(); - assert!(ctx.has(ContentFormat::Text)); - assert_eq!(ctx.get_text().unwrap(), test_plain_txt); - - let test_rich_txt = "\x1b[1m\x1b[4m\x1b[31mHello, Rust!\x1b[0m"; - ctx.set_rich_text(test_rich_txt.to_string()).unwrap(); - assert!(ctx.has(ContentFormat::Rtf)); - assert_eq!(ctx.get_rich_text().unwrap(), test_rich_txt); - - let test_html = "

Hello, Rust!

"; - ctx.set_html(test_html.to_string()).unwrap(); - assert!(ctx.has(ContentFormat::Html)); - assert_eq!(ctx.get_html().unwrap(), test_html); - - let contents: Vec = vec![ - ClipboardContent::Text(test_plain_txt.to_string()), - ClipboardContent::Rtf(test_rich_txt.to_string()), - ClipboardContent::Html(test_html.to_string()), - ]; - ctx.set(contents).unwrap(); - assert!(ctx.has(ContentFormat::Text)); - assert!(ctx.has(ContentFormat::Rtf)); - assert!(ctx.has(ContentFormat::Html)); - assert_eq!(ctx.get_text().unwrap(), test_plain_txt); - assert_eq!(ctx.get_rich_text().unwrap(), test_rich_txt); - assert_eq!(ctx.get_html().unwrap(), test_html); - - let content_arr = ctx - .get(&[ContentFormat::Text, ContentFormat::Rtf, ContentFormat::Html]) - .unwrap(); - - assert_eq!(content_arr.len(), 3); - for c in content_arr { - let content_str = c.as_str().unwrap(); - match c.get_format() { - ContentFormat::Text => assert_eq!(content_str, test_plain_txt), - ContentFormat::Rtf => assert_eq!(content_str, test_rich_txt), - ContentFormat::Html => assert_eq!(content_str, test_html), - _ => panic!("unexpected format"), - } - } + let ctx = ClipboardContext::new().unwrap(); + + let types = ctx.available_formats().unwrap(); + println!("{:?}", types); + + let test_plain_txt = "hell@$#%^&U都98好的😊o Rust!!!"; + ctx.set_text(test_plain_txt.to_string()).unwrap(); + assert!(ctx.has(ContentFormat::Text)); + assert_eq!(ctx.get_text().unwrap(), test_plain_txt); + + let test_rich_txt = "\x1b[1m\x1b[4m\x1b[31mHello, Rust!\x1b[0m"; + ctx.set_rich_text(test_rich_txt.to_string()).unwrap(); + assert!(ctx.has(ContentFormat::Rtf)); + assert_eq!(ctx.get_rich_text().unwrap(), test_rich_txt); + + let test_html = "

Hello, Rust!

"; + ctx.set_html(test_html.to_string()).unwrap(); + assert!(ctx.has(ContentFormat::Html)); + assert_eq!(ctx.get_html().unwrap(), test_html); + + let contents: Vec = vec![ + ClipboardContent::Text(test_plain_txt.to_string()), + ClipboardContent::Rtf(test_rich_txt.to_string()), + ClipboardContent::Html(test_html.to_string()), + ]; + ctx.set(contents).unwrap(); + assert!(ctx.has(ContentFormat::Text)); + assert!(ctx.has(ContentFormat::Rtf)); + assert!(ctx.has(ContentFormat::Html)); + assert_eq!(ctx.get_text().unwrap(), test_plain_txt); + assert_eq!(ctx.get_rich_text().unwrap(), test_rich_txt); + assert_eq!(ctx.get_html().unwrap(), test_html); + + let content_arr = ctx + .get(&[ContentFormat::Text, ContentFormat::Rtf, ContentFormat::Html]) + .unwrap(); + + assert_eq!(content_arr.len(), 3); + for c in content_arr { + let content_str = c.as_str().unwrap(); + match c.get_format() { + ContentFormat::Text => assert_eq!(content_str, test_plain_txt), + ContentFormat::Rtf => assert_eq!(content_str, test_rich_txt), + ContentFormat::Html => assert_eq!(content_str, test_html), + _ => panic!("unexpected format"), + } + } }