//! Win32 message pumps stay off GPUI's main thread. #![allow(unsafe_code)] use crate::background::{Action, State}; use std::{ ptr, sync::{ Arc, OnceLock, atomic::{AtomicBool, Ordering}, mpsc::{self, Sender}, }, thread, time::Duration, }; use windows_sys::Win32::{ Foundation::{ CloseHandle, ERROR_ALREADY_EXISTS, GetLastError, HANDLE, HWND, LPARAM, LRESULT, WPARAM, }, System::{LibraryLoader::GetModuleHandleW, Threading::CreateMutexW}, UI::WindowsAndMessaging::{ CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, FindWindowW, GetMessageW, PM_REMOVE, PeekMessageW, PostMessageW, PostQuitMessage, RegisterClassW, TranslateMessage, UnregisterClassW, WM_APP, WM_CLOSE, WM_DESTROY, WNDCLASSW, }, }; fn wide(text: &str) -> Vec { text.encode_utf16().chain(Some(0)).collect() } static ACTIONS: OnceLock> = OnceLock::new(); const OPEN: u32 = WM_APP + 1; unsafe extern "system" fn procedure( window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { if message == OPEN { if let Some(actions) = ACTIONS.get() { let _ = actions.send(Action::Open); } return 0; } // SAFETY: this is a registered window procedure with Win32-provided arguments. unsafe { if message == WM_CLOSE { DestroyWindow(window); return 0; } if message == WM_DESTROY { PostQuitMessage(0); return 0; } DefWindowProcW(window, message, wparam, lparam) } } pub struct Instance { mutex: HANDLE, window: usize, worker: Option>, } impl Instance { pub fn acquire(actions: Sender) -> Result, String> { let name = wide(&format!( "Local\\Clipbin-{}", std::env::var("USERNAME").unwrap_or_default() )); let class = wide("ClipbinSingleInstance"); // SAFETY: names are NUL-terminated, no security attributes or initial ownership. let mutex = unsafe { CreateMutexW(ptr::null(), 0, name.as_ptr()) }; if mutex.is_null() { return Err(std::io::Error::last_os_error().to_string()); } // SAFETY: GetLastError is read immediately after CreateMutexW. if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { // Startup can race the first instance's window registration. for _ in 0..100 { // SAFETY: class string is live; FindWindow and PostMessage accept borrowed handles. let window = unsafe { FindWindowW(class.as_ptr(), ptr::null()) }; if !window.is_null() { // SAFETY: send a private message to the live endpoint window. let sent = unsafe { PostMessageW(window, OPEN, 0, 0) }; let error = std::io::Error::last_os_error(); // SAFETY: release this launch's mutex reference exactly once. unsafe { CloseHandle(mutex); } return if sent != 0 { Ok(None) } else { Err(error.to_string()) }; } thread::sleep(Duration::from_millis(50)); } // SAFETY: release our reference, leaving the first instance's mutex intact. unsafe { CloseHandle(mutex); } return Err("Running Clipbin did not expose its reopen endpoint".into()); } let _ = ACTIONS.set(actions); let (ready, result) = mpsc::sync_channel(1); let worker = thread::spawn(move || { // SAFETY: window and message loop are owned by this thread; the class // and instance handle remain valid through DestroyWindow. unsafe { let instance = GetModuleHandleW(ptr::null()); let definition = WNDCLASSW { lpfnWndProc: Some(procedure), hInstance: instance, lpszClassName: class.as_ptr(), ..std::mem::zeroed() }; RegisterClassW(&definition); let window = CreateWindowExW( 0, class.as_ptr(), class.as_ptr(), 0, 0, 0, 0, 0, ptr::null_mut(), ptr::null_mut(), instance, ptr::null(), ); let _ = ready.send(if window.is_null() { Err(std::io::Error::last_os_error().to_string()) } else { Ok(window as usize) }); if window.is_null() { return; } let mut message = std::mem::zeroed(); while GetMessageW(&mut message, ptr::null_mut(), 0, 0) > 0 { TranslateMessage(&message); DispatchMessageW(&message); } UnregisterClassW(class.as_ptr(), instance); } }); match result.recv().map_err(|e| e.to_string())? { Ok(window) => Ok(Some(Self { mutex, window, worker: Some(worker), })), Err(error) => { // SAFETY: endpoint creation failed; release the acquired mutex. unsafe { CloseHandle(mutex); } let _ = worker.join(); Err(error) } } } } impl Drop for Instance { fn drop(&mut self) { // SAFETY: the endpoint thread owns this HWND; posting close lets it clean up. unsafe { PostMessageW(self.window as HWND, WM_CLOSE, 0, 0); } if let Some(worker) = self.worker.take() { let _ = worker.join(); } // SAFETY: release the mutex reference acquired above. unsafe { CloseHandle(self.mutex); } } } enum Command { Update(State), Stop, } pub struct Tray { commands: Sender, available: Arc, worker: Option>, } impl Tray { pub fn start(actions: Sender) -> Result { let (commands, receiver) = mpsc::channel(); let (ready, result) = mpsc::sync_channel(1); let available = Arc::new(AtomicBool::new(true)); let alive = available.clone(); let worker = thread::spawn(move || { use tray_icon::{ Icon, TrayIconBuilder, TrayIconEvent, menu::{Menu, MenuEvent, MenuItem}, }; let run = || -> Result<(), String> { let menu = Menu::new(); let entries: Vec<_> = [ ("Open Clipbin", Action::Open), ("Save clip", Action::Clip), ("Add bookmark", Action::Bookmark), ("Quit", Action::Quit), ] .into_iter() .map(|(label, action)| (MenuItem::new(label, true, None), action)) .collect(); for (item, _) in &entries { menu.append(item).map_err(|e| e.to_string())?; } let icon = |state: &State| { let [r, g, b] = state.colour(); Icon::from_rgba([r, g, b, 255].repeat(32 * 32), 32, 32) .map_err(|e| e.to_string()) }; let state = State { status: crate::RecordingStatus::Idle, game: None, }; let tray = TrayIconBuilder::new() .with_menu(Box::new(menu)) .with_tooltip(state.tooltip()) .with_icon(icon(&state)?) .build() .map_err(|e| e.to_string())?; let _ = ready.send(Ok(())); loop { // SAFETY: the tray is created and dispatched on this thread. unsafe { let mut message = std::mem::zeroed(); while PeekMessageW(&mut message, ptr::null_mut(), 0, 0, PM_REMOVE) != 0 { TranslateMessage(&message); DispatchMessageW(&message); } } while let Ok(event) = MenuEvent::receiver().try_recv() { if let Some((_, action)) = entries.iter().find(|(item, _)| item.id() == &event.id) { let _ = actions.send(*action); } } while let Ok(event) = TrayIconEvent::receiver().try_recv() { if matches!(event, TrayIconEvent::DoubleClick { .. }) { let _ = actions.send(Action::Open); } } match receiver.recv_timeout(Duration::from_millis(20)) { Ok(Command::Update(state)) => { tray.set_tooltip(Some(state.tooltip())) .map_err(|e| e.to_string())?; tray.set_icon(Some(icon(&state)?)) .map_err(|e| e.to_string())?; } Ok(Command::Stop) | Err(mpsc::RecvTimeoutError::Disconnected) => break, Err(mpsc::RecvTimeoutError::Timeout) => {} } } Ok(()) }; if let Err(error) = run() { let _ = ready.send(Err(error.clone())); eprintln!("Windows tray unavailable: {error}"); let _ = actions.send(Action::Open); } alive.store(false, Ordering::Relaxed); }); result.recv().map_err(|e| e.to_string())??; Ok(Self { commands, available, worker: Some(worker), }) } pub fn available(&self) -> bool { self.available.load(Ordering::Relaxed) && self .worker .as_ref() .is_some_and(|worker| !worker.is_finished()) } pub fn update(&self, state: State) { let _ = self.commands.send(Command::Update(state)); } } impl Drop for Tray { fn drop(&mut self) { let _ = self.commands.send(Command::Stop); if let Some(worker) = self.worker.take() { let _ = worker.join(); } } }