use windows::{
Win32::Foundation::HWND,
Win32::System::DataExchange::{
CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard, SetClipboardData,
},
Win32::System::Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GHND},
};
const CF_UNICODETEXT: u32 = 13;
#[derive(Debug)]
pub enum ClipboardError {
OpenFailed,
CloseFailed,
ClearFailed,
MemoryAllocationFailed,
MemoryLockFailed,
MemoryUnlockFailed,
SetDataFailed,
GetDataFailed,
InvalidFormat,
StringConversionError,
}
impl std::fmt::Display for ClipboardError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClipboardError::OpenFailed => write!(f, "Failed to open clipboard"),
ClipboardError::CloseFailed => write!(f, "Failed to close clipboard"),
ClipboardError::ClearFailed => write!(f, "Failed to clear clipboard"),
ClipboardError::MemoryAllocationFailed => {
write!(f, "Failed to allocate global memory")
}
ClipboardError::MemoryLockFailed => write!(f, "Failed to lock global memory"),
ClipboardError::MemoryUnlockFailed => write!(f, "Failed to unlock global memory"),
ClipboardError::SetDataFailed => write!(f, "Failed to set clipboard data"),
ClipboardError::GetDataFailed => write!(f, "Failed to get clipboard data"),
ClipboardError::InvalidFormat => write!(f, "Clipboard data is not in text format"),
ClipboardError::StringConversionError => {
write!(f, "String conversion error (contains null bytes)")
}
}
}
}
impl std::error::Error for ClipboardError {}
pub fn set_text(text: &str) -> Result<(), ClipboardError> {
unsafe {
if OpenClipboard(Some(HWND::default())).is_err() {
return Err(ClipboardError::OpenFailed);
}
}
unsafe {
if EmptyClipboard().is_err() {
CloseClipboard().ok();
return Err(ClipboardError::ClearFailed);
}
}
let utf16: Vec<u16> = text.encode_utf16().chain(std::iter::once(0)).collect();
let byte_size = utf16.len() * std::mem::size_of::<u16>();
let h_mem = unsafe { GlobalAlloc(GHND, byte_size) };
if h_mem.is_err() || h_mem.as_ref().unwrap().0.is_null() {
unsafe {
CloseClipboard().ok();
}
return Err(ClipboardError::MemoryAllocationFailed);
}
let h_mem = h_mem.unwrap();
unsafe {
let locked_ptr = GlobalLock(h_mem);
if locked_ptr.is_null() {
CloseClipboard().ok();
return Err(ClipboardError::MemoryLockFailed);
}
std::ptr::copy_nonoverlapping(utf16.as_ptr(), locked_ptr as *mut u16, utf16.len());
if GlobalUnlock(h_mem).is_err() {
let last_error = windows::Win32::Foundation::GetLastError().0;
if last_error != 0 && last_error != 6 { CloseClipboard().ok();
return Err(ClipboardError::MemoryUnlockFailed);
}
}
}
unsafe {
let handle = windows::Win32::Foundation::HANDLE(h_mem.0);
if SetClipboardData(CF_UNICODETEXT, Some(handle)).is_err() {
CloseClipboard().ok();
return Err(ClipboardError::SetDataFailed);
}
}
unsafe {
if CloseClipboard().is_err() {
return Err(ClipboardError::CloseFailed);
}
}
Ok(())
}
pub fn get_text() -> Result<String, ClipboardError> {
unsafe {
if OpenClipboard(Some(HWND::default())).is_err() {
return Err(ClipboardError::OpenFailed);
}
}
let h_data = unsafe { GetClipboardData(CF_UNICODETEXT) };
if h_data.is_err() || h_data.as_ref().unwrap().0.is_null() {
unsafe {
CloseClipboard().ok();
}
return Err(ClipboardError::GetDataFailed);
}
let h_data_handle = h_data.unwrap();
let h_data = windows::Win32::Foundation::HGLOBAL(h_data_handle.0);
let text = unsafe {
let locked_ptr = GlobalLock(h_data);
if locked_ptr.is_null() {
CloseClipboard().ok();
return Err(ClipboardError::MemoryLockFailed);
}
let mut utf16_chars = Vec::new();
let mut ptr = locked_ptr as *const u16;
loop {
let ch = *ptr;
if ch == 0 {
break;
}
utf16_chars.push(ch);
ptr = ptr.add(1);
}
if GlobalUnlock(h_data).is_err() {
CloseClipboard().ok();
return Err(ClipboardError::MemoryUnlockFailed);
}
match String::from_utf16(&utf16_chars) {
Ok(s) => s,
Err(_) => {
CloseClipboard().ok();
return Err(ClipboardError::StringConversionError);
}
}
};
unsafe {
if CloseClipboard().is_err() {
return Err(ClipboardError::CloseFailed);
}
}
Ok(text)
}
pub fn clear() -> Result<(), ClipboardError> {
unsafe {
if OpenClipboard(Some(HWND::default())).is_err() {
return Err(ClipboardError::OpenFailed);
}
}
unsafe {
if EmptyClipboard().is_err() {
CloseClipboard().ok();
return Err(ClipboardError::ClearFailed);
}
}
unsafe {
if CloseClipboard().is_err() {
return Err(ClipboardError::CloseFailed);
}
}
Ok(())
}
pub fn has_text() -> bool {
unsafe {
if OpenClipboard(Some(HWND::default())).is_err() {
return false;
}
let has_text = GetClipboardData(CF_UNICODETEXT)
.map(|h| !h.0.is_null())
.unwrap_or(false);
CloseClipboard().ok();
has_text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_set_and_get_text() {
let test_text = "Hello, Clipboard!";
let result = set_text(test_text);
assert!(result.is_ok(), "Failed to set text: {:?}", result);
let retrieved = get_text().expect("Failed to get text");
assert_eq!(retrieved, test_text);
}
#[test]
fn test_clear_clipboard() {
set_text("Test text").ok();
assert!(clear().is_ok(), "Failed to clear clipboard");
}
#[test]
fn test_unicode_text() {
let unicode_text = "你好世界!🌍 こんにちは";
let result = set_text(unicode_text);
assert!(result.is_ok(), "Failed to set unicode text: {:?}", result);
let retrieved = get_text().expect("Failed to get unicode text");
assert_eq!(retrieved, unicode_text);
}
#[test]
fn test_empty_string() {
let result = set_text("");
assert!(result.is_ok(), "Failed to set empty string: {:?}", result);
let retrieved = get_text().expect("Failed to get empty string");
assert_eq!(retrieved, "");
}
#[test]
fn test_long_text() {
let long_text = "A".repeat(10000);
let result = set_text(&long_text);
assert!(result.is_ok(), "Failed to set long text: {:?}", result);
let retrieved = get_text().expect("Failed to get long text");
assert_eq!(retrieved, long_text);
}
}