diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index c3a8ca22..9b00270f 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -2026,45 +2026,29 @@ fn should_try_non_tsf_insertion_fallback( fn insert_via_non_tsf_fallback( inner: &Arc, polished: &str, - restore_clipboard: bool, - paste_shortcut: PasteShortcut, + _restore_clipboard: bool, + _paste_shortcut: PasteShortcut, ) -> InsertStatus { - let mut retried_unicode = false; let status = finish_non_tsf_insertion_fallback( - || { - inner.inserter.insert_via_clipboard_fallback( - polished, - restore_clipboard, - paste_shortcut, - ) - }, - || { - retried_unicode = true; - inner.inserter.insert_via_unicode_keystrokes(polished) - }, + || inner.inserter.insert_via_unicode_keystrokes(polished), + || inner.inserter.copy_fallback(polished), ); - if retried_unicode { - if status == InsertStatus::Inserted { + match status { + InsertStatus::Inserted => { log::warn!( - "[windows-ime] TSF unavailable; clipboard write failed, inserted via Unicode SendInput" - ); - } else { - log::warn!( - "[windows-ime] TSF unavailable; clipboard write failed and Unicode SendInput fallback also failed" + "[windows-ime] TSF unavailable; inserted via paced Unicode SendInput fallback" ); } - } else { - match status { - InsertStatus::PasteSent => { - log::info!("[windows-ime] TSF unavailable; attempted shortcut paste fallback"); - } - InsertStatus::CopiedFallback => { - log::warn!( - "[windows-ime] TSF unavailable; left text on clipboard, skipped Unicode SendInput retry" - ); - } - InsertStatus::Inserted | InsertStatus::Failed => {} + InsertStatus::CopiedFallback => { + log::warn!( + "[windows-ime] TSF unavailable; Unicode SendInput failed, left text on clipboard" + ); + } + InsertStatus::PasteSent | InsertStatus::Failed => { + log::warn!( + "[windows-ime] TSF unavailable; Unicode SendInput fallback failed and copy fallback failed" + ); } } @@ -2072,24 +2056,24 @@ fn insert_via_non_tsf_fallback( } #[cfg(any(target_os = "windows", test))] -fn finish_non_tsf_insertion_fallback( - mut clipboard_fallback: C, +fn finish_non_tsf_insertion_fallback( mut unicode_fallback: U, + mut copy_fallback: C, ) -> InsertStatus where - C: FnMut() -> InsertStatus, U: FnMut() -> InsertStatus, + C: FnMut() -> InsertStatus, { - match clipboard_fallback() { - InsertStatus::Failed => match unicode_fallback() { - InsertStatus::Inserted => InsertStatus::Inserted, - // 这里的 Unicode fallback 没有经过剪贴板复制;若它失败,不能再伪装成 - // "已复制,请 Ctrl+V"。 - InsertStatus::PasteSent | InsertStatus::CopiedFallback | InsertStatus::Failed => { - InsertStatus::Failed + match unicode_fallback() { + InsertStatus::Inserted => InsertStatus::Inserted, + InsertStatus::PasteSent | InsertStatus::CopiedFallback | InsertStatus::Failed => { + match copy_fallback() { + InsertStatus::CopiedFallback => InsertStatus::CopiedFallback, + InsertStatus::Inserted | InsertStatus::PasteSent | InsertStatus::Failed => { + InsertStatus::Failed + } } - }, - status => status, + } } } @@ -2099,48 +2083,48 @@ mod non_tsf_fallback_tests { use crate::types::InsertStatus; #[test] - fn clipboard_copy_stops_before_unicode_retry() { - let mut unicode_called = false; + fn unicode_fallback_runs_before_copy_fallback() { + let mut copy_called = false; let status = finish_non_tsf_insertion_fallback( - || InsertStatus::CopiedFallback, + || InsertStatus::Inserted, || { - unicode_called = true; - InsertStatus::Inserted - }, - ); - - assert_eq!(status, InsertStatus::CopiedFallback); - assert!(!unicode_called); - } - - #[test] - fn unicode_retry_runs_only_after_clipboard_failure() { - let mut unicode_called = false; - let status = finish_non_tsf_insertion_fallback( - || InsertStatus::Failed, - || { - unicode_called = true; - InsertStatus::Inserted - }, - ); - - assert_eq!(status, InsertStatus::Inserted); - assert!(unicode_called); - } - - #[test] - fn double_failure_does_not_pretend_text_was_copied() { - let mut unicode_called = false; - let status = finish_non_tsf_insertion_fallback( - || InsertStatus::Failed, - || { - unicode_called = true; + copy_called = true; InsertStatus::CopiedFallback }, ); + assert_eq!(status, InsertStatus::Inserted); + assert!(!copy_called); + } + + #[test] + fn copy_fallback_runs_after_unicode_failure() { + let mut copy_called = false; + let status = finish_non_tsf_insertion_fallback( + || InsertStatus::Failed, + || { + copy_called = true; + InsertStatus::CopiedFallback + }, + ); + + assert_eq!(status, InsertStatus::CopiedFallback); + assert!(copy_called); + } + + #[test] + fn double_failure_does_not_pretend_text_was_copied() { + let mut copy_called = false; + let status = finish_non_tsf_insertion_fallback( + || InsertStatus::Failed, + || { + copy_called = true; + InsertStatus::Failed + }, + ); + assert_eq!(status, InsertStatus::Failed); - assert!(unicode_called); + assert!(copy_called); } } diff --git a/openless-all/app/src-tauri/src/insertion.rs b/openless-all/app/src-tauri/src/insertion.rs index 9b1d6567..b5ca39b4 100644 --- a/openless-all/app/src-tauri/src/insertion.rs +++ b/openless-all/app/src-tauri/src/insertion.rs @@ -386,15 +386,30 @@ fn insertion_success_status() -> InsertStatus { #[cfg(target_os = "windows")] mod windows_unicode { + use std::time::Duration; + use windows::Win32::UI::Input::KeyboardAndMouse::{ SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYBD_EVENT_FLAGS, KEYEVENTF_KEYUP, KEYEVENTF_UNICODE, VIRTUAL_KEY, }; + const SENDINPUT_CHUNK_CHARS: usize = 16; + const SENDINPUT_CHUNK_DELAY: Duration = Duration::from_millis(12); + pub fn send_text(text: &str) -> Result<(), String> { - for unit in text.encode_utf16() { - send_utf16_unit(unit, false)?; - send_utf16_unit(unit, true)?; + let mut sent_in_chunk = 0usize; + let mut chars = text.chars().peekable(); + while let Some(ch) = chars.next() { + let mut buf = [0u16; 2]; + for unit in ch.encode_utf16(&mut buf) { + send_utf16_unit(*unit, false)?; + send_utf16_unit(*unit, true)?; + } + sent_in_chunk += 1; + if sent_in_chunk >= SENDINPUT_CHUNK_CHARS && chars.peek().is_some() { + std::thread::sleep(SENDINPUT_CHUNK_DELAY); + sent_in_chunk = 0; + } } Ok(()) } diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index 737c479a..0feb78de 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -544,8 +544,8 @@ pub struct UserPreferences { /// 行为一致,不破坏既有用户。 #[serde(default)] pub paste_shortcut: PasteShortcut, - /// Windows: 是否允许 TSF 失败后继续使用快捷键粘贴 / 剪贴板兜底。 - /// 仅在剪贴板写入失败时才再试 Unicode SendInput,避免长文本注入卡顿。 + /// Windows: 是否允许 TSF 失败后继续使用分批 Unicode SendInput / 剪贴板兜底。 + /// Unicode SendInput 失败时才复制到剪贴板,避免文本丢失。 /// 默认开启以保持可用性;关闭后可验证文本是否真正由 TSF 上屏。 #[serde(default = "default_true")] pub allow_non_tsf_insertion_fallback: bool, diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 943f9880..225a4537 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -546,7 +546,7 @@ export const en: typeof zhCN = { comboClear: 'Clear', comboConflict: 'This shortcut combination is not available', allowNonTsfFallbackLabel: 'Allow non-TSF fallback', - allowNonTsfFallbackDesc: 'Windows: when TSF insertion fails, try shortcut paste first; only fall back to Unicode SendInput if clipboard write fails.', + allowNonTsfFallbackDesc: 'Windows: when TSF insertion fails, use paced Unicode SendInput; if that still fails, copy the text to the clipboard.', historyGroupTitle: 'History & context', historyRetentionLabel: 'History retention (days)', historyRetentionDesc: 'Entries older than this are pruned on new writes; 0 = no time-based pruning.', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index fa688a09..760baa83 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -548,7 +548,7 @@ export const ja: typeof zhCN = { comboClear: 'クリア', comboConflict: 'このショートカットの組み合わせは使用できません', allowNonTsfFallbackLabel: '非 TSF フォールバックを許可', - allowNonTsfFallbackDesc: 'Windows:TSF 入力が失敗した時は先にショートカット貼り付けを試し、クリップボード書き込みも失敗した時だけ Unicode SendInput に切り替えます。', + allowNonTsfFallbackDesc: 'Windows:TSF 入力が失敗した時は分割した Unicode SendInput を使い、それも失敗した場合はクリップボードへコピーします。', historyGroupTitle: '履歴とコンテキスト', historyRetentionLabel: '履歴保持期間(日)', historyRetentionDesc: '保持日数を超えた履歴は新規書き込み時に削除されます。0 = 時間で削除しない。', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 50304f3d..c6d4de87 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -548,7 +548,7 @@ export const ko: typeof zhCN = { comboClear: '지우기', comboConflict: '이 단축키 조합은 사용할 수 없습니다', allowNonTsfFallbackLabel: '비 TSF 폴백 허용', - allowNonTsfFallbackDesc: 'Windows: TSF 입력이 실패하면 먼저 단축키 붙여넣기를 시도하고, 클립보드 쓰기까지 실패한 경우에만 Unicode SendInput으로 전환.', + allowNonTsfFallbackDesc: 'Windows: TSF 입력이 실패하면 분할된 Unicode SendInput을 사용하고, 그래도 실패하면 텍스트를 클립보드에 복사합니다.', historyGroupTitle: '기록 및 컨텍스트', historyRetentionLabel: '기록 보관 기간(일)', historyRetentionDesc: '보관 기간을 초과한 기록은 새 항목 작성 시 정리됩니다. 0 = 시간 기반 정리 비활성화.', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 11b42ead..b32d04b2 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -544,7 +544,7 @@ export const zhCN = { comboClear: '清除', comboConflict: '该快捷键组合不可用', allowNonTsfFallbackLabel: '允许非 TSF 兜底', - allowNonTsfFallbackDesc: 'Windows:TSF 失败时先改用快捷键粘贴;剪贴板写入失败时才退到 Unicode SendInput。', + allowNonTsfFallbackDesc: 'Windows:TSF 失败时使用分批 Unicode SendInput;如果仍失败,再复制到剪贴板。', historyGroupTitle: '历史与上下文', historyRetentionLabel: '历史保留天数', historyRetentionDesc: '超过保留天数的历史在写入新条目时被清理;0 = 不按时间清理。', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 40e3334a..8f31b52e 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -546,7 +546,7 @@ export const zhTW: typeof zhCN = { pasteShortcutCtrlShiftV: 'Ctrl+Shift+V(kitty / alacritty / wezterm / 多數終端)', pasteShortcutShiftInsert: 'Shift+Insert(xterm / urxvt)', allowNonTsfFallbackLabel: '允許非 TSF 兜底', - allowNonTsfFallbackDesc: 'Windows:TSF 失敗時先改用快捷鍵貼上;剪貼簿寫入失敗時才退到 Unicode SendInput。', + allowNonTsfFallbackDesc: 'Windows:TSF 失敗時使用分批 Unicode SendInput;如果仍失敗,再複製到剪貼簿。', historyGroupTitle: '歷史與上下文', historyRetentionLabel: '歷史保留天數', historyRetentionDesc: '超過保留天數的歷史在寫入新條目時被清理;0 = 不按時間清理。',