From b69412335ab9c8436f94af50fee76f0fa2ac54a9 Mon Sep 17 00:00:00 2001 From: omercelikdev Date: Tue, 21 Jul 2026 00:10:58 +0300 Subject: [PATCH] fix(windows): make auto-paste actually restore the target window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, simulate_paste hid QlipLab's own window and then called a bare SetForegroundWindow on the previously-focused window. By that point QlipLab is no longer the foreground process, so Windows' foreground lock denies the call and merely flashes the taskbar button — the synthesized Ctrl+V then lands nowhere, making paste look broken. Add restore_foreground_window(), which brings the target back reliably the way Ditto does: - IsWindow guard against a stale handle - SW_RESTORE if the target was minimized - AllowSetForegroundWindow + AttachThreadInput to both the outgoing foreground thread and the target's thread, so the foreground-lock check treats the request as cooperating - verify via GetForegroundWindow and retry once, then settle before Ctrl+V macOS/Linux paths are unchanged. Win32 signatures verified against windows 0.61.3 (AttachThreadInput lives in Win32::System::Threading, already enabled — no new crate feature needed). Co-Authored-By: Claude Opus 4.8 --- src-tauri/src/lib.rs | 93 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 10d72df..d7d36eb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -261,6 +261,73 @@ fn save_frontmost_app() -> Result<(), String> { Ok(()) } +/// Bring a previously-focused window back to the foreground reliably. +/// +/// A plain `SetForegroundWindow` from QlipLab fails here: by the time we paste, +/// QlipLab has already hidden its own window and is no longer the foreground +/// process, so Windows' foreground lock denies the call and merely flashes the +/// taskbar button — the paste then lands nowhere. We work around it the way +/// Ditto does: restore the target if it was minimized, then attach our input +/// queue to both the outgoing foreground thread and the target's thread so the +/// foreground-lock check treats the request as coming from the active app. +/// Returns whether the target actually ended up in the foreground. +#[cfg(target_os = "windows")] +fn restore_foreground_window(hwnd_val: isize) -> bool { + use windows::Win32::Foundation::HWND; + use windows::Win32::System::Threading::{AttachThreadInput, GetCurrentThreadId}; + use windows::Win32::UI::WindowsAndMessaging::{ + AllowSetForegroundWindow, BringWindowToTop, GetForegroundWindow, GetWindowThreadProcessId, + IsIconic, IsWindow, SetForegroundWindow, ShowWindow, ASFW_ANY, SW_RESTORE, + }; + + let target = HWND(hwnd_val as *mut std::ffi::c_void); + unsafe { + if !IsWindow(Some(target)).as_bool() { + qlip_log(&format!("restore_foreground_window: stale/invalid hwnd={}", hwnd_val)); + return false; + } + + // Un-minimize so the paste target is actually able to receive input. + if IsIconic(target).as_bool() { + let _ = ShowWindow(target, SW_RESTORE); + } + + // Opt out of the foreground lock for this attempt. + let _ = AllowSetForegroundWindow(ASFW_ANY); + + let this_thread = GetCurrentThreadId(); + let fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None); + let target_thread = GetWindowThreadProcessId(target, None); + + // Attach to the outgoing foreground thread and the target thread so + // Windows sees a "cooperating" request rather than a background grab. + let att_fg = fg_thread != 0 + && fg_thread != this_thread + && AttachThreadInput(this_thread, fg_thread, true).as_bool(); + let att_tgt = target_thread != 0 + && target_thread != this_thread + && target_thread != fg_thread + && AttachThreadInput(this_thread, target_thread, true).as_bool(); + + let _ = SetForegroundWindow(target); + let _ = BringWindowToTop(target); + + if att_tgt { + let _ = AttachThreadInput(this_thread, target_thread, false); + } + if att_fg { + let _ = AttachThreadInput(this_thread, fg_thread, false); + } + + let ok = GetForegroundWindow() == target; + qlip_log(&format!( + "restore_foreground_window: hwnd={} foreground={}", + hwnd_val, ok + )); + ok + } +} + /// Get current frontmost app name (for source tracking and ignore list) #[cfg(target_os = "macos")] #[tauri::command] @@ -535,21 +602,25 @@ fn simulate_paste() -> Result<(), String> { qlip_log("simulate_paste [non-mac]: starting"); #[cfg(target_os = "windows")] { - use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow; - use windows::Win32::Foundation::HWND; - if let Ok(guard) = PREVIOUS_HWND.lock() { - if let Some(hwnd_val) = *guard { - qlip_log(&format!("simulate_paste [win]: activating hwnd={}", hwnd_val)); - unsafe { - let _ = SetForegroundWindow(HWND(hwnd_val as *mut std::ffi::c_void)); - } - } else { - qlip_log("simulate_paste [win]: no previous hwnd saved"); + let hwnd_val = PREVIOUS_HWND.lock().ok().and_then(|guard| *guard); + if let Some(hwnd_val) = hwnd_val { + qlip_log(&format!("simulate_paste [win]: activating hwnd={}", hwnd_val)); + // The foreground grab can lose a race with our own window + // finishing its hide, so give it a second attempt. + if !restore_foreground_window(hwnd_val) { + thread::sleep(Duration::from_millis(60)); + restore_foreground_window(hwnd_val); } + // Let the newly-activated window settle before we type into it. + thread::sleep(Duration::from_millis(60)); + } else { + qlip_log("simulate_paste [win]: no previous hwnd saved"); + thread::sleep(Duration::from_millis(100)); } } - + #[cfg(not(target_os = "windows"))] thread::sleep(Duration::from_millis(100)); + if let Ok(mut enigo) = Enigo::new(&Settings::default()) { qlip_log("simulate_paste [non-mac]: enigo created, pressing Ctrl+V"); let _ = enigo.key(Key::Control, Direction::Press);