fix(windows): pace Unicode insertion fallback

This commit is contained in:
H-Chris233 committed 2026-05-20 16:00:17 +08:00
1 parent 5bf197c359
commit 358c8d41e2
8 files changed
+88 -89

No files matched your search

+63 -79
View File
@@ -2026,45 +2026,29 @@ fn should_try_non_tsf_insertion_fallback(
fn insert_via_non_tsf_fallback(
inner: &Arc<Inner>,
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<C, U>(
mut clipboard_fallback: C,
fn finish_non_tsf_insertion_fallback<U, C>(
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);
}
}
+18 -3
View File
@@ -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(())
}
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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.',
+1 -1
View File
@@ -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 = 時間で削除しない。',
+1 -1
View File
@@ -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 = 시간 기반 정리 비활성화.',
+1 -1
View File
@@ -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 = 不按时间清理。',
+1 -1
View File
@@ -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 = 不按時間清理。',