fix(paste): hide preview overlay before Ctrl+V to avoid Wayland focus race
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<PasteOutcome, String> {
|
||||
pub async fn paste_text(app: tauri::AppHandle, text: String) -> Result<PasteOutcome, String> {
|
||||
let mut outcome = PasteOutcome {
|
||||
backend: None,
|
||||
pasted: false,
|
||||
@@ -52,6 +67,8 @@ pub async fn paste_text(text: String) -> Result<PasteOutcome, String> {
|
||||
}
|
||||
}
|
||||
|
||||
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<PasteOutcome, String> {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user