diff --git a/src-tauri/src/commands/paste.rs b/src-tauri/src/commands/paste.rs index 02d0b20..5703ae8 100644 --- a/src-tauri/src/commands/paste.rs +++ b/src-tauri/src/commands/paste.rs @@ -72,18 +72,7 @@ pub async fn paste_text(app: tauri::AppHandle, text: String) -> Result = match Clipboard::new() { - Ok(mut cb) => cb.get_text().ok(), - Err(_) => None, - }; + let prior_clipboard = snapshot_clipboard_text(); match Clipboard::new().and_then(|mut cb| cb.set_text(&text)) { Ok(()) => outcome.copied = true, @@ -121,25 +110,40 @@ pub async fn paste_text(app: tauri::AppHandle, text: String) -> Result outcome.message = Some(err), } - // Fire-and-forget: restore the prior clipboard in the background - // after CLIPBOARD_RESTORE_MS, regardless of whether the paste - // succeeded. If paste failed, the transcript is still on the - // clipboard and Kon's own "Copy" button remains the user's - // recovery path; overwriting it with the restore would undo that. - // So only restore when the keystroke actually fired. if outcome.pasted { - if let Some(prior) = prior_clipboard.clone() { - let transcript_clone = text.clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(CLIPBOARD_RESTORE_MS)).await; - restore_prior_clipboard(&prior, &transcript_clone); - }); - } + schedule_clipboard_restore(prior_clipboard, text); } Ok(outcome) } +/// Snapshot the clipboard's current text, or `None` when the clipboard +/// holds non-text content (images, files, empty). Shared between +/// `paste_text` and `paste_text_replacing` so both halves of the +/// dictation flow obey the "never silently clobber the user's +/// clipboard" contract from Handy #921 (brief item #3). +fn snapshot_clipboard_text() -> Option { + match Clipboard::new() { + Ok(mut cb) => cb.get_text().ok(), + Err(_) => None, + } +} + +/// Fire-and-forget: restore `prior` to the clipboard after +/// `CLIPBOARD_RESTORE_MS`, but only if the clipboard still holds the +/// `transcript` we set (i.e., the user hasn't copied something new in +/// the window). No-op when `prior` is `None` — nothing safe to +/// restore. +fn schedule_clipboard_restore(prior: Option, transcript: String) { + let Some(prior) = prior else { + return; + }; + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(CLIPBOARD_RESTORE_MS)).await; + restore_prior_clipboard(&prior, &transcript); + }); +} + /// Write `prior` back to the clipboard only if the current clipboard /// content is still the transcript we just set — i.e. the user hasn't /// already copied something new themselves in the last 300 ms. This @@ -189,6 +193,14 @@ pub async fn paste_text_replacing( message: None, }; + // Same clipboard-preservation contract as `paste_text`: snapshot + // the user's prior clipboard before we write the raw transcript, + // restore it in the background after the paste keystroke fires. + // Without this, a successful revert leaves the raw transcript on + // the clipboard permanently — stomping whatever the user had + // copied before invoking replace-with-raw. + let prior_clipboard = snapshot_clipboard_text(); + match Clipboard::new().and_then(|mut cb| cb.set_text(&text)) { Ok(()) => outcome.copied = true, Err(err) => { @@ -214,6 +226,10 @@ pub async fn paste_text_replacing( Err(err) => outcome.message = Some(err), } + if outcome.pasted { + schedule_clipboard_restore(prior_clipboard, text); + } + Ok(outcome) }