use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering};
use std::sync::{Condvar, Mutex, OnceLock};
use std::time::Duration;
use windows::core::w;
use windows::Win32::Foundation::{HANDLE, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::DataExchange::{
CloseClipboard, EmptyClipboard, GetClipboardOwner, OpenClipboard, SetClipboardData,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, RegisterClassW,
TranslateMessage, CW_USEDEFAULT, MSG, WM_DESTROYCLIPBOARD, WM_RENDERALLFORMATS,
WM_RENDERFORMAT, WNDCLASSW, WS_OVERLAPPED,
};
use crate::clipboard::{alloc_global_public, utf16_bytes_public, CF_UNICODETEXT_PUBLIC};
use crate::Error;
const CF_TEXT: u32 = 1;
const CF_OEMTEXT: u32 = 7;
struct State {
pending: Option<String>,
rendered: bool,
render_count: u32,
last_render: Option<std::time::Instant>,
baseline: u32,
}
fn state() -> &'static (Mutex<State>, Condvar) {
static S: OnceLock<(Mutex<State>, Condvar)> = OnceLock::new();
S.get_or_init(|| {
(
Mutex::new(State {
pending: None,
rendered: false,
render_count: 0,
last_render: None,
baseline: 0,
}),
Condvar::new(),
)
})
}
static OWNER_HWND: AtomicIsize = AtomicIsize::new(0);
static THREAD_STARTED: AtomicBool = AtomicBool::new(false);
unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
match msg {
WM_RENDERFORMAT => {
render(wp.0 as u32);
LRESULT(0)
}
WM_RENDERALLFORMATS => {
if unsafe { OpenClipboard(Some(hwnd)) }.is_ok() {
render(CF_UNICODETEXT_PUBLIC);
let _ = unsafe { CloseClipboard() };
}
LRESULT(0)
}
WM_DESTROYCLIPBOARD => {
let (lock, _) = state();
if let Ok(mut s) = lock.lock() {
s.pending = None;
}
LRESULT(0)
}
_ => unsafe { DefWindowProcW(hwnd, msg, wp, lp) },
}
}
fn render(format: u32) {
if !matches!(format, CF_UNICODETEXT_PUBLIC | CF_TEXT | CF_OEMTEXT) {
return;
}
let (lock, cvar) = state();
let Ok(mut s) = lock.lock() else { return };
let Some(text) = s.pending.clone() else {
return;
};
if let Ok(handle) = alloc_global_public(&utf16_bytes_public(&text)) {
let _ = unsafe { SetClipboardData(CF_UNICODETEXT_PUBLIC, Some(HANDLE(handle.0))) };
}
s.rendered = true;
s.render_count += 1;
s.last_render = Some(std::time::Instant::now());
cvar.notify_all();
}
fn ensure_owner() -> Result<HWND, Error> {
if let 0 = OWNER_HWND.load(Ordering::SeqCst) {
} else {
return Ok(HWND(OWNER_HWND.load(Ordering::SeqCst) as *mut _));
}
if THREAD_STARTED.swap(true, Ordering::SeqCst) {
for _ in 0..200 {
let h = OWNER_HWND.load(Ordering::SeqCst);
if h != 0 {
return Ok(HWND(h as *mut _));
}
std::thread::sleep(Duration::from_millis(5));
}
return Err(Error::OwnerWindowFailed);
}
std::thread::Builder::new()
.name("win-text-inject-clipboard-owner".into())
.spawn(|| unsafe {
let instance = match GetModuleHandleW(None) {
Ok(i) => i,
Err(_) => return,
};
let class = w!("WinTextInjectClipboardOwner");
let wc = WNDCLASSW {
lpfnWndProc: Some(wndproc),
hInstance: instance.into(),
lpszClassName: class,
..Default::default()
};
RegisterClassW(&wc);
let hwnd = match CreateWindowExW(
Default::default(),
class,
w!("win-text-inject clipboard owner"),
WS_OVERLAPPED,
CW_USEDEFAULT,
CW_USEDEFAULT,
0,
0,
None,
None,
Some(instance.into()),
None,
) {
Ok(h) => h,
Err(_) => return,
};
OWNER_HWND.store(hwnd.0 as isize, Ordering::SeqCst);
let mut msg = MSG::default();
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
})
.map_err(|_| Error::OwnerWindowFailed)?;
for _ in 0..200 {
let h = OWNER_HWND.load(Ordering::SeqCst);
if h != 0 {
return Ok(HWND(h as *mut _));
}
std::thread::sleep(Duration::from_millis(5));
}
Err(Error::OwnerWindowFailed)
}
pub struct Offer;
impl Offer {
pub fn publish(text: &str) -> Result<Self, Error> {
let hwnd = ensure_owner()?;
{
let (lock, _) = state();
let mut s = lock.lock().map_err(|_| Error::OwnerWindowFailed)?;
s.pending = Some(text.to_owned());
s.rendered = false;
s.render_count = 0;
s.last_render = None;
s.baseline = 0;
}
{
let _guard = crate::clipboard::ClipboardGuard::open_owned_by(hwnd)?;
let result = unsafe {
(|| {
EmptyClipboard().map_err(Error::Clipboard)?;
if let Err(e) = SetClipboardData(CF_UNICODETEXT_PUBLIC, None) {
if e.code().0 != 0 {
return Err(Error::Clipboard(e));
}
}
crate::clipboard::attach_privacy_formats();
Ok::<(), Error>(())
})()
};
result?;
if unsafe { GetClipboardOwner() }.unwrap_or_default() != hwnd {
return Err(Error::OwnerWindowFailed);
}
}
Ok(Self)
}
pub fn wait_for_read(&self, timeout: Duration) -> bool {
let (lock, cvar) = state();
let Ok(guard) = lock.lock() else { return false };
let Ok((guard, _)) = cvar.wait_timeout_while(guard, timeout, |s| !s.rendered) else {
return false;
};
guard.rendered
}
pub fn wait_for_reads_to_settle(&self, timeout: Duration, quiet: Duration) -> Option<u32> {
if !self.wait_for_read(timeout) {
return None;
}
loop {
let last = {
let (lock, _) = state();
let s = lock.lock().ok()?;
s.last_render?
};
let elapsed = last.elapsed();
if elapsed >= quiet {
break;
}
std::thread::sleep(quiet - elapsed);
}
let (lock, _) = state();
let s = lock.lock().ok()?;
Some(s.render_count)
}
pub fn read_count(&self) -> u32 {
state().0.lock().map(|s| s.render_count).unwrap_or(0)
}
pub fn mark_paste_sent(&self) {
if let Ok(mut s) = state().0.lock() {
s.baseline = s.render_count;
}
}
pub fn consumed_before_paste(&self) -> bool {
state().0.lock().map(|s| s.baseline > 0).unwrap_or(false)
}
pub fn wait_for_target_read(&self, timeout: Duration, quiet: Duration) -> Option<u32> {
let (lock, cvar) = state();
{
let guard = lock.lock().ok()?;
let (guard, timed_out) = cvar
.wait_timeout_while(guard, timeout, |s| s.render_count <= s.baseline)
.ok()?;
if timed_out.timed_out() && guard.render_count <= guard.baseline {
return None;
}
}
loop {
let last = { lock.lock().ok()?.last_render? };
let elapsed = last.elapsed();
if elapsed >= quiet {
break;
}
std::thread::sleep(quiet - elapsed);
}
let s = lock.lock().ok()?;
Some(s.render_count - s.baseline)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn owner_window_starts_and_is_reused() {
let a = ensure_owner().expect("owner window");
let b = ensure_owner().expect("owner window again");
assert_eq!(a.0, b.0);
assert!(!a.0.is_null());
}
#[test]
fn non_text_formats_are_not_rendered() {
let (lock, _) = state();
{
let mut s = lock.lock().unwrap();
s.pending = Some("x".into());
s.rendered = false;
}
render(2);
assert!(!lock.lock().unwrap().rendered);
}
}