fix(insertion): macOS 插入后恢复用户剪贴板 (#525)
此前 macOS 的 insert() 只写剪贴板 + post Cmd+V,完全忽略 restore_clipboard_after_paste 参数;整套剪贴板恢复机制被 #[cfg(not(target_os = "macos"))] 排除,导致设置里的 「恢复剪贴板」开关在 macOS 上无效(issue #525,实测 macOS 26.5 / M4 Pro)。 - 把剪贴板恢复机制(保存原内容 → 延迟回写 → 仅当剪贴板仍是刚插入文字时才恢复)改为 跨平台可用,去掉 cfg(not macos) 门控;insert_with_clipboard_restore 仍仅 Windows/ Linux 使用(依赖 PasteShortcut 模拟),故保留其门控。 - macOS insert() 改为:保存原剪贴板 → 写转写文字 → Cmd+V → 粘贴成功且开关开启时按 CLIPBOARD_RESTORE_DELAY 延迟恢复;粘贴失败则保留转写文字供手动粘贴、不恢复。 - CLIPBOARD_RESTORE_DELAY 合并为统一常量;删除随之失效的 macos_insert_status_after_paste。 - should_restore_clipboard 守卫测试改为跨平台运行,新增 macOS 成功路径测试。
This commit is contained in:
1 parent
19ae8641d6
commit
4b4d21912b
1 file changed
+33
-45
@@ -5,22 +5,15 @@
|
||||
//! - macOS:用 CoreGraphics CGEvent 直接 post Cmd+V。
|
||||
//! - Windows / Linux:用 enigo 按 `PasteShortcut` 模拟。
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use once_cell::sync::Lazy;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::types::{InsertStatus, PasteShortcut};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const CLIPBOARD_RESTORE_DELAY: Duration = Duration::from_millis(750);
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
|
||||
/// 粘贴完成到尝试恢复剪贴板之间的延迟,给目标应用读取剪贴板留出时间。
|
||||
const CLIPBOARD_RESTORE_DELAY: Duration = Duration::from_millis(750);
|
||||
|
||||
pub struct TextInserter;
|
||||
@@ -90,21 +83,37 @@ impl TextInserter {
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS 路径:写剪贴板 + post Cmd+V。两个 `_` 参数仅为对齐跨平台签名。
|
||||
/// macOS 路径:保存原剪贴板 → 写转写文字 → post Cmd+V → 按需恢复原剪贴板。
|
||||
/// `paste_shortcut` 在 macOS 不使用(固定 Cmd+V),仅为对齐跨平台签名。
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn insert(
|
||||
&self,
|
||||
text: &str,
|
||||
restore_clipboard_after_paste: bool,
|
||||
paste_shortcut: PasteShortcut,
|
||||
_paste_shortcut: PasteShortcut,
|
||||
) -> InsertStatus {
|
||||
if text.is_empty() {
|
||||
return InsertStatus::CopiedFallback;
|
||||
}
|
||||
if !copy_to_clipboard(text) {
|
||||
return InsertStatus::Failed;
|
||||
// issue #525:先记下用户原剪贴板,粘贴成功后再恢复,避免覆盖用户手动复制的内容。
|
||||
// 此前 macOS 完全不实现恢复(恢复机制曾被 cfg(not macos) 排除),导致设置里的
|
||||
// 「恢复剪贴板」开关在 macOS 上无效。
|
||||
let restore_plan = match copy_to_clipboard_with_restore_plan(text) {
|
||||
Ok(plan) => plan,
|
||||
Err(err) => {
|
||||
log::error!("[insertion] clipboard write failed: {}", err);
|
||||
return InsertStatus::Failed;
|
||||
}
|
||||
};
|
||||
if let Err(err) = simulate_paste() {
|
||||
log::warn!("[insertion] simulated paste failed: {}", err);
|
||||
// 粘贴失败:把转写文字留在剪贴板供用户手动粘贴,不恢复。
|
||||
return InsertStatus::CopiedFallback;
|
||||
}
|
||||
macos_insert_status_after_paste(simulate_paste())
|
||||
if restore_clipboard_after_paste {
|
||||
schedule_clipboard_restore(restore_plan);
|
||||
}
|
||||
insertion_success_status()
|
||||
}
|
||||
|
||||
/// 只写剪贴板、不模拟粘贴。用于目标控件活跃状态无法验证时的兜底路径。
|
||||
@@ -146,41 +155,26 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_insert_status_after_paste(result: Result<(), String>) -> InsertStatus {
|
||||
match result {
|
||||
Ok(()) => insertion_success_status(),
|
||||
Err(err) => {
|
||||
log::warn!("[insertion] simulated paste failed: {}", err);
|
||||
InsertStatus::CopiedFallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextInserter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[derive(Debug)]
|
||||
struct ClipboardRestorePlan {
|
||||
inserted_text: String,
|
||||
previous_text: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[derive(Debug, Clone)]
|
||||
struct PendingClipboardRestore {
|
||||
latest_restore_id: u64,
|
||||
original_text: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
static NEXT_CLIPBOARD_RESTORE_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
static PENDING_CLIPBOARD_RESTORE: Lazy<Mutex<Option<PendingClipboardRestore>>> =
|
||||
Lazy::new(|| Mutex::new(None));
|
||||
|
||||
@@ -199,7 +193,6 @@ fn copy_to_clipboard(text: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn copy_to_clipboard_with_restore_plan(text: &str) -> Result<ClipboardRestorePlan, String> {
|
||||
let mut clipboard = arboard::Clipboard::new().map_err(|e| e.to_string())?;
|
||||
let previous_text = match clipboard.get_text() {
|
||||
@@ -246,7 +239,6 @@ fn insert_with_clipboard_restore(
|
||||
insertion_success_status()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn schedule_clipboard_restore(plan: ClipboardRestorePlan) {
|
||||
let (restore_id, original_text) =
|
||||
remember_pending_clipboard_restore(plan.previous_text.clone());
|
||||
@@ -255,7 +247,6 @@ fn schedule_clipboard_restore(plan: ClipboardRestorePlan) {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn remember_pending_clipboard_restore(previous_text: Option<String>) -> (u64, Option<String>) {
|
||||
let restore_id = NEXT_CLIPBOARD_RESTORE_ID.fetch_add(1, Ordering::SeqCst);
|
||||
let original_text = {
|
||||
@@ -273,7 +264,6 @@ fn remember_pending_clipboard_restore(previous_text: Option<String>) -> (u64, Op
|
||||
(restore_id, original_text)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn restore_clipboard_after_delay(
|
||||
plan: ClipboardRestorePlan,
|
||||
original_text: Option<String>,
|
||||
@@ -324,7 +314,6 @@ fn restore_clipboard_after_delay(
|
||||
clear_pending_clipboard_restore(restore_id);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn is_latest_clipboard_restore(restore_id: u64) -> bool {
|
||||
matches!(
|
||||
PENDING_CLIPBOARD_RESTORE.lock().as_ref(),
|
||||
@@ -332,7 +321,6 @@ fn is_latest_clipboard_restore(restore_id: u64) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn clear_pending_clipboard_restore(restore_id: u64) {
|
||||
let mut pending = PENDING_CLIPBOARD_RESTORE.lock();
|
||||
if matches!(pending.as_ref(), Some(batch) if batch.latest_restore_id == restore_id) {
|
||||
@@ -340,7 +328,6 @@ fn clear_pending_clipboard_restore(restore_id: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn should_restore_clipboard(current_text: Option<&str>, inserted_text: &str) -> bool {
|
||||
matches!(current_text, Some(current) if current == inserted_text)
|
||||
}
|
||||
@@ -561,7 +548,6 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn restore_only_when_clipboard_still_holds_inserted_text() {
|
||||
assert!(should_restore_clipboard(
|
||||
Some("dictated text"),
|
||||
@@ -728,15 +714,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_direct_write_or_paste_failure_keeps_copied_fallback_available() {
|
||||
assert_eq!(
|
||||
macos_insert_status_after_paste(Ok(())),
|
||||
InsertStatus::Inserted
|
||||
);
|
||||
assert_eq!(
|
||||
macos_insert_status_after_paste(Err("AX direct write unavailable".to_string())),
|
||||
InsertStatus::CopiedFallback
|
||||
);
|
||||
fn macos_paste_success_reports_inserted_and_guards_restore() {
|
||||
// 粘贴成功 → Inserted;恢复仅在剪贴板仍是刚插入的转写文字时进行(issue #525)。
|
||||
assert_eq!(insertion_success_status(), InsertStatus::Inserted);
|
||||
assert!(should_restore_clipboard(
|
||||
Some("dictated text"),
|
||||
"dictated text"
|
||||
));
|
||||
assert!(!should_restore_clipboard(
|
||||
Some("user changed clipboard"),
|
||||
"dictated text"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user