From bc1ae3968e5117b76a06f540101c4322de490b71 Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 21 Apr 2026 09:23:22 +0100 Subject: [PATCH] fix(paste): hide preview overlay before Ctrl+V to avoid Wayland focus race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase H landed the transcription preview overlay. Phase C landed auto-paste. With both enabled the combo is broken on Wayland compositors (KWin, Mutter): the overlay is always_on_top + visible at the moment paste_text fires its Ctrl+V keystroke, and the compositor resolves the key to the topmost visible window — which is the overlay, even though we built it with focused=false. Net result: the transcript pastes into Kon instead of whatever app the user was actually dictating into. Fix, mirroring OpenWhispr's PR #246 shape: before trigger_paste_keystroke, hide the transcription-preview window if it exists and is visible, then sleep PREVIEW_HIDE_SETTLE_MS (80ms) so the compositor recomputes focus onto the previously-focused app. No reshow — the user's confirmation is the text appearing in the target app, not a fading overlay. The 80ms is enough on KWin and Mutter; tunable if it shows up differently on other compositors. paste_text now takes the tauri::AppHandle so it can reach the preview window. Frontend invocation signature is unchanged (Tauri injects the handle; the JS call site still passes { text }). Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/src/commands/paste.rs | 37 ++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) 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.