Skip to content
Open
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
96 changes: 52 additions & 44 deletions openless-all/app/src-tauri/src/insertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
//! - macOS:用 CoreGraphics CGEvent 直接 post Cmd+V。
//! - Windows / Linux:用 enigo 按 `PasteShortcut` 模拟。

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use std::time::Duration;

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use once_cell::sync::Lazy;
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use parking_lot::Mutex;

use crate::types::{InsertStatus, PasteShortcut};

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
const CLIPBOARD_RESTORE_DELAY: Duration = Duration::from_millis(750);

pub struct TextInserter;
Expand Down Expand Up @@ -87,21 +87,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 排除),导致设置里
// 的开关在 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()
}

/// Android:跨应用输入由 dictation 流程按用户策略处理;通用插入只写剪贴板兜底。
Expand Down Expand Up @@ -157,41 +173,30 @@ 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(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[derive(Debug)]
struct ClipboardRestorePlan {
inserted_text: String,
previous_text: Option<String>,
}

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[derive(Debug, Clone)]
struct PendingClipboardRestore {
latest_restore_id: u64,
original_text: Option<String>,
}

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
static NEXT_CLIPBOARD_RESTORE_ID: AtomicU64 = AtomicU64::new(1);

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
static PENDING_CLIPBOARD_RESTORE: Lazy<Mutex<Option<PendingClipboardRestore>>> =
Lazy::new(|| Mutex::new(None));

Expand Down Expand Up @@ -232,7 +237,7 @@ fn copy_to_clipboard(text: &str) -> bool {
}
}

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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 @@ -279,7 +284,7 @@ fn insert_with_clipboard_restore(
insertion_success_status()
}

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn schedule_clipboard_restore(plan: ClipboardRestorePlan) {
let (restore_id, original_text) =
remember_pending_clipboard_restore(plan.previous_text.clone());
Expand All @@ -288,7 +293,7 @@ fn schedule_clipboard_restore(plan: ClipboardRestorePlan) {
});
}

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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 @@ -306,7 +311,7 @@ fn remember_pending_clipboard_restore(previous_text: Option<String>) -> (u64, Op
(restore_id, original_text)
}

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn restore_clipboard_after_delay(
plan: ClipboardRestorePlan,
original_text: Option<String>,
Expand Down Expand Up @@ -357,23 +362,23 @@ fn restore_clipboard_after_delay(
clear_pending_clipboard_restore(restore_id);
}

#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
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 @@ -594,7 +599,7 @@ mod tests {
use std::time::Duration;

#[test]
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn restore_only_when_clipboard_still_holds_inserted_text() {
assert!(should_restore_clipboard(
Some("dictated text"),
Expand Down Expand Up @@ -724,7 +729,7 @@ mod tests {
}

#[test]
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn pending_clipboard_restore_keeps_first_original_until_latest_restore() {
*PENDING_CLIPBOARD_RESTORE.lock() = None;

Expand All @@ -746,7 +751,7 @@ mod tests {
}

#[test]
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn clipboard_restore_skips_when_clipboard_no_longer_matches_inserted_text() {
assert!(should_restore_clipboard(
Some("dictated text"),
Expand All @@ -761,15 +766,18 @@ 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),
// 即「恢复剪贴板」开关在 macOS 上真正生效。
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