diff --git a/src-tauri/src/commands/paste.rs b/src-tauri/src/commands/paste.rs index 8ba9bf0..4c513b5 100644 --- a/src-tauri/src/commands/paste.rs +++ b/src-tauri/src/commands/paste.rs @@ -15,9 +15,17 @@ //! window does not. The frontend surfaces this caveat next to the toggle. use std::process::Command; +use std::time::Duration; use arboard::Clipboard; use serde::Serialize; +use tauri::Manager; + +/// Compositor settle time after hiding the preview overlay before firing +/// the paste keystroke. Empirically ~80ms is enough on KWin + Mutter +/// Wayland for focus to return to the previously-focused app; shorter +/// risks the keystroke still landing on the (now-invisible) overlay. +const PREVIEW_HIDE_SETTLE_MS: u64 = 80; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -35,8 +43,15 @@ pub struct PasteOutcome { /// Copy `text` to the clipboard, then trigger a paste keystroke in the /// focused window. Returns a structured result so the frontend can surface /// partial success (clipboard set, paste failed). +/// +/// Wayland compositor quirk: if Kon's always-on-top preview overlay is +/// visible when the keystroke fires, KWin / Mutter may resolve the keystroke +/// against the overlay (even with `focused: false` set at build time) and +/// the paste lands inside Kon instead of the previously-focused app. We hide +/// the preview window and give the compositor a beat to re-focus the real +/// target before dispatching. Matches OpenWhispr's PR #246 fix on GNOME. #[tauri::command] -pub async fn paste_text(text: String) -> Result { +pub async fn paste_text(app: tauri::AppHandle, text: String) -> Result { let mut outcome = PasteOutcome { backend: None, pasted: false, @@ -52,6 +67,8 @@ pub async fn paste_text(text: String) -> Result { } } + hide_preview_overlay_for_paste(&app).await; + match trigger_paste_keystroke() { Ok(backend) => { outcome.backend = Some(backend); @@ -63,6 +80,24 @@ pub async fn paste_text(text: String) -> Result { Ok(outcome) } +/// Hide the transcription-preview window if it's currently visible, then +/// sleep a short beat so the compositor can recompute focus. No-ops when +/// the window isn't registered yet (user never enabled the overlay) or +/// isn't currently shown. +async fn hide_preview_overlay_for_paste(app: &tauri::AppHandle) { + let Some(window) = app.get_webview_window("transcription-preview") else { + return; + }; + let visible = window.is_visible().unwrap_or(false); + if !visible { + return; + } + if window.hide().is_err() { + return; + } + tokio::time::sleep(Duration::from_millis(PREVIEW_HIDE_SETTLE_MS)).await; +} + /// Report which paste backends the OS has available right now. Pure probe — /// does not paste. Used by Settings to tell the user "install wtype" when /// nothing is available on their session.