Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 33 additions & 45 deletions openless-all/app/src-tauri/src/insertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
if restore_clipboard_after_paste {
schedule_clipboard_restore(restore_plan);
}
macos_insert_status_after_paste(simulate_paste())
insertion_success_status()
}

/// 只写剪贴板、不模拟粘贴。用于目标控件活跃状态无法验证时的兜底路径。
Expand Down Expand Up @@ -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));

Expand All @@ -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() {
Expand Down Expand Up @@ -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());
Expand All @@ -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 = {
Expand All @@ -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>,
Expand Down Expand Up @@ -324,23 +314,20 @@ 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(),
Some(batch) if batch.latest_restore_id == restore_id
)
}

#[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) {
pending.take();
}
}

#[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)
}
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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]
Expand Down
Loading