fix(windows): reduce SendInput pressure during insert
This commit is contained in:
1 parent
52ea7f5983
commit
6ae3bfc452
9 files changed
+245
-52
No files matched your search
@@ -2029,13 +2029,118 @@ fn insert_via_non_tsf_fallback(
|
||||
restore_clipboard: bool,
|
||||
paste_shortcut: PasteShortcut,
|
||||
) -> InsertStatus {
|
||||
if inner.inserter.insert_via_unicode_keystrokes(polished) == InsertStatus::Inserted {
|
||||
log::info!("[windows-ime] TSF unavailable; inserted via Unicode SendInput");
|
||||
InsertStatus::Inserted
|
||||
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)
|
||||
},
|
||||
);
|
||||
|
||||
if retried_unicode {
|
||||
if 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"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
inner
|
||||
.inserter
|
||||
.insert_via_clipboard_fallback(polished, restore_clipboard, paste_shortcut)
|
||||
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 => {}
|
||||
}
|
||||
}
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
fn finish_non_tsf_insertion_fallback<C, U>(
|
||||
mut clipboard_fallback: C,
|
||||
mut unicode_fallback: U,
|
||||
) -> InsertStatus
|
||||
where
|
||||
C: FnMut() -> InsertStatus,
|
||||
U: 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
|
||||
}
|
||||
},
|
||||
status => status,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod non_tsf_fallback_tests {
|
||||
use super::finish_non_tsf_insertion_fallback;
|
||||
use crate::types::InsertStatus;
|
||||
|
||||
#[test]
|
||||
fn clipboard_copy_stops_before_unicode_retry() {
|
||||
let mut unicode_called = false;
|
||||
let status = finish_non_tsf_insertion_fallback(
|
||||
|| InsertStatus::CopiedFallback,
|
||||
|| {
|
||||
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;
|
||||
InsertStatus::CopiedFallback
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(status, InsertStatus::Failed);
|
||||
assert!(unicode_called);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,39 +114,7 @@ async fn run_streaming_polish(
|
||||
// from what the user actually sees\"。
|
||||
let (tx, rx) = std::sync::mpsc::channel::<String>();
|
||||
let typer_handle = tokio::task::spawn_blocking(move || {
|
||||
let mut typed_text = String::new();
|
||||
let mut first_failure: Option<String> = None;
|
||||
let mut pending = String::new();
|
||||
while let Ok(delta) = rx.recv() {
|
||||
pending.push_str(&delta);
|
||||
let flush_at = std::time::Instant::now() + STREAMING_INSERT_FLUSH_INTERVAL;
|
||||
loop {
|
||||
let now = std::time::Instant::now();
|
||||
if now >= flush_at {
|
||||
break;
|
||||
}
|
||||
match rx.recv_timeout(flush_at.duration_since(now)) {
|
||||
Ok(delta) => pending.push_str(&delta),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => break,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
|
||||
first_failure =
|
||||
flush_streaming_insert_buffer(&mut pending, &mut typed_text);
|
||||
return (typed_text, first_failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
first_failure = flush_streaming_insert_buffer(&mut pending, &mut typed_text);
|
||||
if first_failure.is_some() {
|
||||
// 一旦类型链路出错(如 Secure Input 启用),后续 delta 全部丢弃,但仍
|
||||
// 把 mpsc drain 完,避免发送端阻塞。
|
||||
while rx.recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if first_failure.is_none() {
|
||||
first_failure = flush_streaming_insert_buffer(&mut pending, &mut typed_text);
|
||||
}
|
||||
(typed_text, first_failure)
|
||||
drain_streaming_insert_deltas(rx, STREAMING_INSERT_FLUSH_INTERVAL)
|
||||
});
|
||||
|
||||
// 3. 调流式润色,on_delta 塞 mpsc;should_cancel 检查 dictation 取消旗。
|
||||
@@ -283,13 +251,77 @@ async fn run_streaming_polish(
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_streaming_insert_deltas(
|
||||
rx: std::sync::mpsc::Receiver<String>,
|
||||
flush_interval: std::time::Duration,
|
||||
) -> (String, Option<String>) {
|
||||
drain_streaming_insert_deltas_with(rx, flush_interval, flush_streaming_insert_buffer)
|
||||
}
|
||||
|
||||
fn drain_streaming_insert_deltas_with<F>(
|
||||
rx: std::sync::mpsc::Receiver<String>,
|
||||
flush_interval: std::time::Duration,
|
||||
mut flush_pending: F,
|
||||
) -> (String, Option<String>)
|
||||
where
|
||||
F: FnMut(&mut String, &mut String) -> Option<String>,
|
||||
{
|
||||
let mut typed_text = String::new();
|
||||
let mut first_failure: Option<String> = None;
|
||||
let mut pending = String::new();
|
||||
while let Ok(delta) = rx.recv() {
|
||||
pending.push_str(&delta);
|
||||
let flush_at = std::time::Instant::now() + flush_interval;
|
||||
loop {
|
||||
let now = std::time::Instant::now();
|
||||
if now >= flush_at {
|
||||
break;
|
||||
}
|
||||
match rx.recv_timeout(flush_at.duration_since(now)) {
|
||||
Ok(delta) => pending.push_str(&delta),
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => break,
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
|
||||
first_failure = flush_pending(&mut pending, &mut typed_text);
|
||||
return (typed_text, first_failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
first_failure = flush_pending(&mut pending, &mut typed_text);
|
||||
if first_failure.is_some() {
|
||||
// 一旦类型链路出错(如 Secure Input 启用),后续 delta 全部丢弃,但仍
|
||||
// 把 mpsc drain 完,避免发送端阻塞。
|
||||
while rx.recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if first_failure.is_none() {
|
||||
first_failure = flush_pending(&mut pending, &mut typed_text);
|
||||
}
|
||||
(typed_text, first_failure)
|
||||
}
|
||||
|
||||
fn flush_streaming_insert_buffer(pending: &mut String, typed_text: &mut String) -> Option<String> {
|
||||
flush_streaming_insert_buffer_with(
|
||||
pending,
|
||||
typed_text,
|
||||
crate::unicode_keystroke::type_unicode_chunk,
|
||||
)
|
||||
}
|
||||
|
||||
fn flush_streaming_insert_buffer_with<F>(
|
||||
pending: &mut String,
|
||||
typed_text: &mut String,
|
||||
mut type_chunk: F,
|
||||
) -> Option<String>
|
||||
where
|
||||
F: FnMut(&str) -> Result<usize, crate::unicode_keystroke::TypeError>,
|
||||
{
|
||||
if pending.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let delta = std::mem::take(pending);
|
||||
let delta_chars = delta.chars().count();
|
||||
match crate::unicode_keystroke::type_unicode_chunk(&delta) {
|
||||
match type_chunk(&delta) {
|
||||
Ok(typed_chars) => {
|
||||
let appended = append_typed_prefix(typed_text, &delta, typed_chars);
|
||||
if appended < delta_chars {
|
||||
@@ -362,9 +394,7 @@ fn streaming_insert_eligible(
|
||||
mode: PolishMode,
|
||||
raw_uses_llm: bool,
|
||||
) -> bool {
|
||||
streaming_insert_enabled
|
||||
&& !translation_active
|
||||
&& (mode != PolishMode::Raw || raw_uses_llm)
|
||||
streaming_insert_enabled && !translation_active && (mode != PolishMode::Raw || raw_uses_llm)
|
||||
}
|
||||
|
||||
fn default_done_message(status: InsertStatus, polish_failed: bool) -> Option<String> {
|
||||
@@ -1728,8 +1758,8 @@ fn append_typed_prefix(target: &mut String, delta: &str, typed_chars: usize) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
append_typed_prefix, default_done_message, dictation_error_code,
|
||||
finalize_polished_text, streaming_insert_eligible,
|
||||
append_typed_prefix, default_done_message, drain_streaming_insert_deltas_with,
|
||||
finalize_polished_text, flush_streaming_insert_buffer_with, streaming_insert_eligible,
|
||||
};
|
||||
use crate::types::{ChineseScriptPreference, CorrectionRule, InsertStatus, PolishMode};
|
||||
|
||||
@@ -1836,4 +1866,61 @@ mod tests {
|
||||
Some("润色失败,已插入原文".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_insert_batches_queued_deltas_before_flush() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
tx.send("你".to_string()).unwrap();
|
||||
tx.send("好".to_string()).unwrap();
|
||||
tx.send("🙂".to_string()).unwrap();
|
||||
drop(tx);
|
||||
|
||||
let mut flushed = Vec::new();
|
||||
let (typed, failure) = drain_streaming_insert_deltas_with(
|
||||
rx,
|
||||
std::time::Duration::from_millis(50),
|
||||
|pending, typed_text| {
|
||||
flushed.push(pending.clone());
|
||||
typed_text.push_str(pending);
|
||||
pending.clear();
|
||||
None
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(flushed, vec!["你好🙂".to_string()]);
|
||||
assert_eq!(typed, "你好🙂");
|
||||
assert_eq!(failure, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush_streaming_insert_buffer_keeps_partial_unicode_prefix() {
|
||||
let mut pending = "a你🙂b".to_string();
|
||||
let mut typed = String::new();
|
||||
|
||||
let failure = flush_streaming_insert_buffer_with(&mut pending, &mut typed, |_| {
|
||||
Err(crate::unicode_keystroke::TypeError::Partial {
|
||||
typed_chars: 3,
|
||||
source: Box::new(platform_type_error()),
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(typed, "a你🙂");
|
||||
assert!(pending.is_empty());
|
||||
assert!(failure.is_some());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn platform_type_error() -> crate::unicode_keystroke::TypeError {
|
||||
crate::unicode_keystroke::TypeError::EventAllocFailed
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn platform_type_error() -> crate::unicode_keystroke::TypeError {
|
||||
crate::unicode_keystroke::TypeError::SendInputFailed("fail".into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn platform_type_error() -> crate::unicode_keystroke::TypeError {
|
||||
crate::unicode_keystroke::TypeError::EnigoText("fail".into())
|
||||
}
|
||||
}
|
||||
@@ -544,7 +544,8 @@ pub struct UserPreferences {
|
||||
/// 行为一致,不破坏既有用户。
|
||||
#[serde(default)]
|
||||
pub paste_shortcut: PasteShortcut,
|
||||
/// Windows: 是否允许 TSF 失败后继续使用 SendInput / 粘贴类非 TSF 兜底。
|
||||
/// Windows: 是否允许 TSF 失败后继续使用快捷键粘贴 / 剪贴板兜底。
|
||||
/// 仅在剪贴板写入失败时才再试 Unicode SendInput,避免长文本注入卡顿。
|
||||
/// 默认开启以保持可用性;关闭后可验证文本是否真正由 TSF 上屏。
|
||||
#[serde(default = "default_true")]
|
||||
pub allow_non_tsf_insertion_fallback: bool,
|
||||
|
||||
@@ -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, allow Unicode SendInput / shortcut paste.',
|
||||
allowNonTsfFallbackDesc: 'Windows: when TSF insertion fails, try shortcut paste first; only fall back to Unicode SendInput if clipboard write fails.',
|
||||
historyGroupTitle: 'History & context',
|
||||
historyRetentionLabel: 'History retention (days)',
|
||||
historyRetentionDesc: 'Entries older than this are pruned on new writes; 0 = no time-based pruning.',
|
||||
|
||||
@@ -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 = 時間で削除しない。',
|
||||
|
||||
@@ -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 = 시간 기반 정리 비활성화.',
|
||||
|
||||
@@ -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 = 不按时间清理。',
|
||||
|
||||
@@ -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 = 不按時間清理。',
|
||||
|
||||
@@ -230,7 +230,7 @@ export interface UserPreferences {
|
||||
* 等终端只接受 Ctrl+Shift+V,硬编码 Ctrl+V 会被吞掉,听写文本只剩在剪贴板里。
|
||||
* macOS 走 AX 直写不受影响。默认 'ctrlV' 与历史行为一致。 */
|
||||
pasteShortcut: PasteShortcut;
|
||||
/** Windows:TSF 失败后是否允许 SendInput / 粘贴类非 TSF 兜底。关闭后可验证是否真实 TSF 上屏。 */
|
||||
/** Windows:TSF 失败后是否允许快捷键粘贴 / 剪贴板兜底。仅在剪贴板写失败时才再试 SendInput。关闭后可验证是否真实 TSF 上屏。 */
|
||||
allowNonTsfInsertionFallback: boolean;
|
||||
/** 用户的工作语言(多选,原生名);作为前提注入 LLM polish/translate prompt 头部。 */
|
||||
workingLanguages: string[];
|
||||
|
||||
Reference in new issue
Block a user