diff --git a/issue-420-wayland-plan.md b/issue-420-wayland-plan.md new file mode 100644 index 00000000..1bcd729d --- /dev/null +++ b/issue-420-wayland-plan.md @@ -0,0 +1,317 @@ +# #420 Wayland 支持方案说明 + +> 适用范围:`/home/chris233/openless` +> 关联 issue:[#420](https://github.com/Open-Less/openless/issues/420) +> 目标:给 OpenLess 在 Linux / Wayland 下补一条可靠、与当前仓库决策一致的实现路径,而不是继续把 X11 思路硬套过去。 + +## 1. 当前问题拆分 + +#420 现在实际上混了三类问题: + +1. **Wayland 下全局快捷键不可用** + - 这是因为 Wayland 安全模型不允许普通应用像 X11 那样监听全局键盘事件。 + - 当前仓库已经把 CLI + single-instance 路径做成 Wayland 下的正式可交付方案;portal 仍属于后续研究方向,而不是现阶段已落定的主实现。 + +2. **Wayland 下文本输出不可靠** + - 流式输出路径:`unicode_keystroke.rs` 在 Linux 仍走 `enigo.text(...)`。 + - 一次性输出路径:`insertion.rs` 仍走 `clipboard + simulate_paste(enigo)`。 + - 这两条路径本质都还是 X11 风格假设,在 Wayland 下可能“调用成功但没真正落字”。 + +3. **Wayland 下设置页快捷键录制 / UI 黑屏闪烁** + - 这更像 WebKitGTK / 合成器 / 输入录制 UI 的独立问题。 + - 不应继续和“Wayland 全局快捷键”或“Wayland 文本输出”混成一个修复面。 + +## 2. 关键判断 + +### 2.1 Wayland 有多层可行路径,但不能把尚未验证的 portal 能力写成既定主路线 + +必须分开看: + +- **全局快捷键触发**: + - 从协议方向看,portal / compositor 能力值得研究; + - 但从**当前仓库已落地实现**与跨桌面可交付性看,正式支持路径已经是 `CLI + single-instance 转发`; + - `xdg-desktop-portal` 的 `GlobalShortcuts` 现阶段更适合作为 research track,而不是直接写成产品承诺。 +- **文本插入**:没有 X11 那种“应用可随意向其他应用发键”的通用能力。 + - 剪贴板有现实可行路。 + - 自动输入只能走 **受权限控制** 的 portal / libei / compositor 能力。 + - 不存在一个对所有 Wayland 桌面都等价、无感、无授权的统一注入接口。 + +### 2.2 现阶段最高优先级不是“自动输入一步到位”,而是“用户文本不能丢” + +当前最危险的问题不是“Wayland 下体验不够自动化”,而是: + +- 日志显示成功 +- OpenLess 认为已经插入 +- 用户实际输入框里没有字 + +这个行为会直接破坏产品的核心承诺:**用户的话不能丢**。 + +## 3. 建议总方案 + +按三个阶段推进,而不是一口气追求全自动。 + +--- + +## Phase 1:先止血,确保文本不丢 + +### 目标 + +在 Wayland 下,即使没有自动输入能力,也必须保证: + +- 听写结果至少可靠进入剪贴板 +- UI / 日志明确告诉用户当前走的是哪条 fallback +- 不再出现“代码认为成功,屏幕实际没字”的假成功状态 + +### 建议改动 + +#### 3.1 禁用 Wayland 下的“streaming insert 成功语义” + +当前逻辑里,Linux 流式路径一旦 `type_unicode_chunk()` 返回成功,就会: + +- 累积 `typed_text` +- 标记 `already_streamed=true` +- 跳过后续 inserter + +这在 Wayland 下不可靠。 + +**建议:** +- 检测 `Linux + Wayland` 时,不让 `enigo.text(...)` 的返回值直接成为“已成功插入”的依据。 +- Wayland 下默认不要走 `already_streamed=true` 的成功短路。 + +#### 3.2 Wayland 下默认降级为 copy-only + +当前非流式路径是: + +- 写入剪贴板 +- 再用 `simulate_paste()` 发粘贴快捷键 + +Wayland 下第二步不可靠。 + +**建议:** +- 检测到 Wayland 时,默认走 **copy-only fallback**。 +- 把文本留在剪贴板里,不要立即 restore。 +- 明确给用户提示:`已复制到剪贴板,请手动粘贴`。 + +#### 3.3 把状态文案改成真话 + +需要避免如下误导: + +- “已插入”但实际上没插入 +- “已尝试粘贴”但用户无从判断文本是否已落到目标应用 + +**建议:** +- Wayland fallback 时统一使用明确状态: + - `已复制到剪贴板,请手动粘贴` + - `Wayland 当前未启用自动输入` + - `剪贴板写入失败` + +### Phase 1 接受标准 + +- Wayland 下听写后,文本不会 silently disappear。 +- 即使自动输入失败,用户也总能从剪贴板找回文本。 +- 日志和 UI 状态与真实行为一致。 + +--- + +## Phase 2:巩固当前 Wayland 触发路径 + +### 目标 + +把 Wayland 下已经落地的 `CLI + single-instance` 方案补齐到真正稳定、清晰、可交付,而不是在文档里把尚未验证的 portal 能力提前写成主路线。 + +### 建议改动 + +#### 3.4 明确把 CLI 路径当作当前正式支持方案 + +当前仓库已采用的路径是: + +1. 启动时检测 Wayland session +2. 不安装 `rdev` 全局监听 +3. 通过桌面环境快捷键执行: + - `openless --toggle-dictation` + - `openless --toggle-qa` + - `openless --cancel-dictation` +4. 由 `tauri-plugin-single-instance` 把第二实例 argv 转发给主实例 coordinator + +这里要做的不是推翻,而是补齐: + +- Settings / README / Linux 指南里统一说明这是当前正式支持方式; +- 保证 GNOME / KDE / Hyprland / sway 等示例文案一致; +- 保证“有快捷键可触发”这件事在 Wayland 上可复现、可说明、可排障。 + +#### 3.5 portal 研究保留为后续增强方向 + +`xdg-desktop-portal` `GlobalShortcuts` 可以继续研究,但在仓库明确验证下面几点之前,不应写成主承诺: + +- GNOME / KDE / 其他桌面上的真实可用范围 +- 权限/交互模型是否符合产品心智 +- 回退链路是否比当前 CLI 方案更简单而不是更碎 + +### 为什么这一层应该单独做 + +- 这是当前仓库已经落地的 Wayland 触发方案; +- 它能解决 #420 最核心的“如何触发听写”问题; +- 维护成本和跨桌面稳定性目前都优于贸然切 portal 主路线。 + +### Phase 2 接受标准 + +- Wayland 用户按文档/设置页说明配置后,能稳定触发 Dictation / QA / Cancel。 +- 设置页、README、日志三处对 Wayland 触发方式的表述一致。 +- 不把 `GlobalShortcuts portal` 写成已交付能力;如继续研究,应另开 research issue / PR。 + +--- + +## Phase 3:研究受权限控制的 Wayland 自动输入能力 + +### 目标 + +探索 Wayland 下真正的“自动把文本发到其他应用”能力,但只在 **有 compositor 支持 + 有用户授权** 的情况下启用。 + +### 候选路径 + +#### 3.5 `RemoteDesktop` portal + keyboard events + +优点: +- 有官方 portal 文档 +- 权限模型明确 + +缺点: +- 会话 / 授权交互更重 +- 行为更像“远程控制权限”,不一定适合所有用户心智 + +#### 3.6 `RemoteDesktop` / `InputCapture` + `ConnectToEIS` + `libei` + +优点: +- 这是 Wayland / compositor 体系里更现代的输入模拟路径 +- 比直接赌 `enigo` / XTest 靠谱 + +缺点: +- 实现复杂度高 +- compositor / backend 支持碎片化 +- 仍然不是“全桌面无感通吃”的方案 + +#### 3.7 不建议把主方案押在 `virtual-keyboard-unstable-v1` + +原因: +- 协议本身就标明不适合当通用稳定能力依赖 +- compositor 是否开放给第三方应用不可控 +- 产品层面碎片化风险太高 + +### Phase 3 的产品策略 + +自动输入必须是: + +- **能力探测通过** 才启用 +- **授权成功** 才启用 +- 失败时明确回退到剪贴板方案 + +换句话说: + +> Wayland 自动输入应该是“可选增强能力”,不是默认基本能力。 + +--- + +## 4. 对 #420 的建议拆单 + +建议把后续工作拆成三个 issue / PR 方向: + +### 4.1 `wayland-output-safety` +范围: +- Wayland 下禁用假成功 streaming insert +- Wayland 下默认 copy-only +- 状态文案 / 日志对齐真实行为 + +这是最高优先级。 + +### 4.2 `wayland-trigger-path-hardening` +范围: +- 巩固 `CLI + single-instance` 触发链路 +- Settings / README / Linux 文档统一 +- GNOME / KDE / Hyprland / sway 示例与排障说明对齐 + +这是第二优先级。 + +### 4.3 `wayland-global-shortcuts-portal-research` +范围: +- 评估 `GlobalShortcuts` portal 的真实桌面支持面 +- 验证是否值得从 research 升级为产品能力 +- 只产出调研/原型,不提前改写当前支持承诺 + +这是后续研究方向,不应与当前可交付方案混写。 + +### 4.4 `wayland-hotkey-editor-flicker` +范围: +- 设置页快捷键录制时的闪烁 / 黑屏 +- 只针对 UI / WebKitGTK / 输入录制链路处理 + +这个不要再跟“文本输出”绑一起看。 + +--- + +## 5. 我建议的实际落地顺序 + +### 第一刀(应先做) +- 修 `Wayland 文本输出不可靠` +- 核心目标:**不丢文本** + +### 第二刀 +- 巩固 `CLI + single-instance` 触发链路 +- 核心目标:**让当前 Wayland 方案真正稳定、清晰、可交付** + +### 第三刀 +- 研究 `GlobalShortcuts portal` / `portal + libei` 能力 +- 核心目标:**评估哪些能力值得升级成未来增强项** + +### 第四刀 +- 单独处理设置页闪烁 / 黑屏 + +--- + +## 6. 不建议做的事 + +### 6.1 不建议继续把 `enigo` 返回值当 Wayland 成功依据 + +因为这会继续制造: +- 日志成功 +- UI 成功 +- 用户实际没看到字 + +### 6.2 不建议把未验证的 portal 方案直接写成当前主实现 + +在仓库已经正式落地 CLI 路径的前提下,把 portal 提前写成“既定正路”,会让文档、代码与用户预期再次脱节。 + +### 6.3 不建议把 `virtual-keyboard-unstable-v1` 直接当主实现 + +它更像 compositor 特定能力,不适合直接做成发行版通用路径。 + +--- + +## 7. 结论 + +Wayland 下当然应该走一条“属于 Wayland 的路”,但这条路在当前仓库里应分成两层: + +1. **当前正式触发路径** → `CLI + single-instance` +2. **剪贴板保底** → Wayland-native clipboard / copy-only fallback +3. **未来增强候选** → `GlobalShortcuts portal`、`RemoteDesktop` / `InputCapture` + `libei/EIS`(能力探测 + 用户授权) + +如果只能先做一件事,优先级一定是: + +> **先修文本输出链路,保证用户的话不会丢。** + +--- + +## 8. 参考资料(用于后续实现,不是最终用户文案) + +- XDG Portal GlobalShortcuts + https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html +- XDG Portal RemoteDesktop + https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html +- XDG Portal InputCapture + https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html +- XDG Portal Clipboard + https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Clipboard.html +- libei 文档 + https://libinput.pages.freedesktop.org/libei/ +- Wayland core / data transfer model + https://wayland.pages.freedesktop.org/wayland.freedesktop.org/docs/html/ch04.html + https://wayland.freedesktop.org/docs/html/apa.html diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index b8e064cb..cd553354 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -3583,7 +3583,7 @@ mod tests { #[test] fn focus_restore_failure_uses_specific_error_code_when_insert_fails() { assert_eq!( - dictation_error_code(InsertStatus::Failed, false, false, false), + dictation_error_code(InsertStatus::Failed, false, false, false, false), Some("focusRestoreFailed") ); } @@ -3600,7 +3600,7 @@ mod tests { #[cfg(target_os = "windows")] fn tsf_required_failure_keeps_tsf_error_when_focus_was_ready() { assert_eq!( - dictation_error_code(InsertStatus::Failed, false, true, false), + dictation_error_code(InsertStatus::Failed, false, true, false, false), Some("windowsImeTsfRequired") ); } diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index 50f1338b..c7c07fd7 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -324,6 +324,49 @@ fn finalize_polished_text( } } +fn streaming_insert_eligible( + streaming_insert_enabled: bool, + translation_active: bool, + mode: PolishMode, + raw_uses_llm: bool, + wayland_session: bool, +) -> bool { + streaming_insert_enabled + && !translation_active + && (mode != PolishMode::Raw || raw_uses_llm) + && !wayland_session +} + +fn wayland_done_message(status: InsertStatus, polish_failed: bool) -> Option { + match status { + InsertStatus::Inserted | InsertStatus::PasteSent => None, + InsertStatus::CopiedFallback => Some(if polish_failed { + "Wayland 未启用自动输入,已复制原文到剪贴板,请手动粘贴".to_string() + } else { + "Wayland 未启用自动输入,已复制到剪贴板,请手动粘贴".to_string() + }), + InsertStatus::Failed => Some("Wayland 未启用自动输入,剪贴板写入失败".to_string()), + } +} + +fn default_done_message(status: InsertStatus, polish_failed: bool) -> Option { + if polish_failed { + // polish 失败优先告知用户,即使 insert 成功也要让用户知道这版是原文 + Some("润色失败,已插入原文".to_string()) + } else { + match status { + InsertStatus::Inserted => None, + InsertStatus::PasteSent => Some("已尝试粘贴".to_string()), + InsertStatus::CopiedFallback => Some(if cfg!(target_os = "windows") { + "已复制,请 Ctrl+V".to_string() + } else { + "已复制,请粘贴".to_string() + }), + InsertStatus::Failed => Some("插入失败".to_string()), + } + } +} + pub(super) async fn handle_pressed_edge(inner: &Arc) { let was_held = inner.hotkey_trigger_held.swap(true, Ordering::SeqCst); if !was_held { @@ -1338,10 +1381,16 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { }; // 流式插入 opt-in 路径:开关打开 + 非翻译 + 非 Raw 模式 → 进入流式分支。 // 任何不满足都走原一次性 polish_or_passthrough 路径,行为跟历史完全一致。 - let streaming_eligible = - prefs.streaming_insert && !translation_active && (mode != PolishMode::Raw || raw_uses_llm); + let wayland_session = crate::hotkey::is_wayland_session(); + let streaming_eligible = streaming_insert_eligible( + prefs.streaming_insert, + translation_active, + mode, + raw_uses_llm, + wayland_session, + ); log::info!( - "[coord] polish dispatch: translation={translation_active} mode={mode:?} streaming_eligible={streaming_eligible}" + "[coord] polish dispatch: translation={translation_active} mode={mode:?} wayland_session={wayland_session} streaming_eligible={streaming_eligible}" ); let (polished, polish_error, already_streamed) = if translation_active { @@ -1445,6 +1494,24 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { polish_error ); InsertStatus::Inserted + } else if wayland_session { + log::info!( + "[coord] Wayland session detected; skipping synthetic paste and attempting copy-only fallback ({} chars)", + polished.chars().count() + ); + let status = inner.inserter.copy_fallback(&polished); + match status { + InsertStatus::CopiedFallback => { + log::info!("[coord] Wayland copy-only fallback succeeded") + } + InsertStatus::Failed => { + log::error!("[coord] Wayland copy-only fallback failed: clipboard write failed") + } + other => log::warn!( + "[coord] Wayland copy-only fallback returned unexpected status: {other:?}" + ), + } + status } else if focus_ready_for_paste { #[cfg(target_os = "windows")] { @@ -1503,6 +1570,7 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { polish_error.is_some(), focus_ready_for_paste, allow_non_tsf_insertion_fallback, + wayland_session, ) .map(str::to_string); let tsf_required_insert_failed = error_code.as_deref() == Some("windowsImeTsfRequired"); @@ -1532,20 +1600,10 @@ pub(super) async fn end_session(inner: &Arc) -> Result<(), String> { } let done_message = if tsf_required_insert_failed { Some("TSF 未上屏,已禁止非 TSF 兜底".to_string()) - } else if polish_error.is_some() { - // polish 失败优先告知用户,即使 insert 成功也要让用户知道这版是原文 - Some("润色失败,已插入原文".to_string()) + } else if wayland_session { + wayland_done_message(status, polish_error.is_some()) } else { - match status { - InsertStatus::Inserted => None, - InsertStatus::PasteSent => Some("已尝试粘贴".to_string()), - InsertStatus::CopiedFallback => Some(if cfg!(target_os = "windows") { - "已复制,请 Ctrl+V".to_string() - } else { - "已复制,请粘贴".to_string() - }), - InsertStatus::Failed => Some("插入失败".to_string()), - } + default_done_message(status, polish_error.is_some()) }; emit_capsule( @@ -1572,8 +1630,11 @@ pub(super) fn dictation_error_code( polish_failed: bool, focus_ready_for_paste: bool, allow_non_tsf_insertion_fallback: bool, + wayland_session: bool, ) -> Option<&'static str> { - if !focus_ready_for_paste && status == InsertStatus::Failed { + if wayland_session && status == InsertStatus::Failed { + Some("waylandClipboardWriteFailed") + } else if !focus_ready_for_paste && status == InsertStatus::Failed { Some("focusRestoreFailed") } else if cfg!(target_os = "windows") && focus_ready_for_paste @@ -1628,8 +1689,11 @@ fn append_typed_prefix(target: &mut String, delta: &str, typed_chars: usize) -> #[cfg(test)] mod tests { - use super::{append_typed_prefix, finalize_polished_text}; - use crate::types::{ChineseScriptPreference, CorrectionRule, PolishMode}; + use super::{ + append_typed_prefix, default_done_message, dictation_error_code, finalize_polished_text, + streaming_insert_eligible, wayland_done_message, + }; + use crate::types::{ChineseScriptPreference, CorrectionRule, InsertStatus, PolishMode}; fn correction_rule(pattern: &str, replacement: &str) -> CorrectionRule { CorrectionRule { @@ -1712,4 +1776,62 @@ mod tests { assert_eq!(appended, 1); assert_eq!(typed, "好"); } + + #[test] + fn wayland_disables_streaming_insert_even_when_pref_enabled() { + assert!(!streaming_insert_eligible( + true, + false, + PolishMode::Light, + false, + true + )); + } + + #[test] + fn x11_linux_can_still_use_streaming_insert_when_other_gates_pass() { + assert!(streaming_insert_eligible( + true, + false, + PolishMode::Light, + false, + false + )); + } + + #[test] + fn wayland_done_message_tells_user_manual_paste_is_required() { + assert_eq!( + wayland_done_message(InsertStatus::CopiedFallback, false), + Some("Wayland 未启用自动输入,已复制到剪贴板,请手动粘贴".to_string()) + ); + assert_eq!( + wayland_done_message(InsertStatus::CopiedFallback, true), + Some("Wayland 未启用自动输入,已复制原文到剪贴板,请手动粘贴".to_string()) + ); + assert_eq!( + wayland_done_message(InsertStatus::Failed, false), + Some("Wayland 未启用自动输入,剪贴板写入失败".to_string()) + ); + } + + #[test] + fn default_done_message_keeps_existing_non_wayland_behavior() { + assert_eq!( + default_done_message(InsertStatus::PasteSent, false), + Some("已尝试粘贴".to_string()) + ); + assert_eq!( + default_done_message(InsertStatus::Inserted, true), + Some("润色失败,已插入原文".to_string()) + ); + } + + #[test] + fn wayland_clipboard_failure_uses_specific_error_code() { + assert_eq!( + dictation_error_code(InsertStatus::Failed, false, false, true, true), + Some("waylandClipboardWriteFailed") + ); + } } diff --git a/openless-all/app/src-tauri/src/types.rs b/openless-all/app/src-tauri/src/types.rs index a7525b85..626b16d9 100644 --- a/openless-all/app/src-tauri/src/types.rs +++ b/openless-all/app/src-tauri/src/types.rs @@ -1565,7 +1565,7 @@ impl HotkeyCapability { supports_side_specific_modifiers: true, explicit_fallback_available: false, status_hint: Some( - "Linux 仅 best-effort:X11 可尝试 rdev 监听;Wayland 会明确提示暂不支持全局热键。".into(), + "Linux 仅 best-effort:X11 可尝试 rdev 监听;Wayland 请在桌面环境中绑定 openless --toggle-dictation 等 CLI 命令。".into(), ), } } diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index f4e017e7..89124f41 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -519,7 +519,7 @@ export const en: typeof zhCN = { streamingInsertHintWindows: 'SendInput Unicode types directly, bypassing TSF / IME — no input-method switching needed.', streamingInsertHintLinux: - 'enigo + XTest synthesize keystrokes. Stable on X11; on Wayland it depends on the compositor — failures fall back automatically.', + 'Uses enigo + XTest on X11. On Wayland, streaming insertion is disabled and output is kept in the clipboard for manual paste.', streamingInsertSaveClipboardLabel: 'Copy to clipboard', streamingInsertSaveClipboardHint: 'After a successful insert, write the final text to the clipboard so Cmd+V can paste it again. Off = clipboard is never touched.', localAsrTitle: 'Local ASR models (experimental)', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 9f89fc58..dcb97b24 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -521,7 +521,7 @@ export const ja: typeof zhCN = { streamingInsertHintWindows: 'SendInput Unicode で TSF / IME を迂回。入力ソースの切替は不要です。', streamingInsertHintLinux: - 'enigo + XTest でキー合成。X11 は安定、Wayland は compositor 依存で失敗時は自動フォールバック。', + 'X11 では enigo + XTest でキー合成します。Wayland ではストリーミング入力を無効化し、出力をクリップボードに残して手動貼り付けします。', streamingInsertSaveClipboardLabel: 'クリップボードに保存', streamingInsertSaveClipboardHint: '挿入成功後に最終テキストをクリップボードへ書き込み、Cmd+V で再貼付け可能にします。OFF ではクリップボードに触れません。', localAsrTitle: 'ローカル ASR モデル(実験的)', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index 60461e87..2398003e 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -521,7 +521,7 @@ export const ko: typeof zhCN = { streamingInsertHintWindows: 'SendInput Unicode 로 TSF / IME 를 우회. 입력 소스 전환 불필요.', streamingInsertHintLinux: - 'enigo + XTest 로 키 합성. X11 안정; Wayland 는 compositor 의존이며 실패 시 자동 폴백.', + 'X11에서는 enigo + XTest로 키를 합성합니다. Wayland에서는 스트리밍 입력을 비활성화하고 출력을 클립보드에 남겨 수동 붙여넣기를 사용합니다.', streamingInsertSaveClipboardLabel: '클립보드에 저장', streamingInsertSaveClipboardHint: '삽입 성공 후 최종 텍스트를 클립보드에 기록하여 Cmd+V 로 다시 붙여넣을 수 있게 합니다. 끄면 클립보드를 건드리지 않습니다.', localAsrTitle: '로컬 ASR 모델 (실험적)', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index 1789698e..fdc862b2 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -517,7 +517,7 @@ export const zhCN = { streamingInsertHintWindows: 'SendInput Unicode 直接送字符,绕过 TSF / IME,不切输入法。', streamingInsertHintLinux: - 'enigo + XTest 合成按键。X11 稳定;Wayland 取决于 compositor,失败自动回落。', + 'X11 使用 enigo + XTest 合成按键;Wayland 下会自动关闭流式输入,并保留到剪贴板供手动粘贴。', streamingInsertSaveClipboardLabel: '同步到剪贴板', streamingInsertSaveClipboardHint: '插入成功后把最终文本写入剪贴板,方便 Cmd+V 再次粘贴;关闭后流式过程不动剪贴板。', localAsrTitle: '本地 ASR 模型(实验性)', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index d3820b8a..1fa5800e 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -519,7 +519,7 @@ export const zhTW: typeof zhCN = { streamingInsertHintWindows: 'SendInput Unicode 直接送字元,繞過 TSF / IME,不切輸入法。', streamingInsertHintLinux: - 'enigo + XTest 合成按鍵。X11 穩定;Wayland 取決於 compositor,失敗自動回落。', + 'X11 使用 enigo + XTest 合成按鍵;Wayland 下會自動關閉串流輸入,並保留到剪貼簿供手動貼上。', streamingInsertSaveClipboardLabel: '同步到剪貼簿', streamingInsertSaveClipboardHint: '插入成功後把最終文字寫入剪貼簿,方便 Cmd+V 再次貼上;關閉後流式過程不動剪貼簿。', localAsrTitle: '本地 ASR 模型(實驗性)', diff --git a/openless-all/app/src/pages/settings/AdvancedSection.tsx b/openless-all/app/src/pages/settings/AdvancedSection.tsx index ce999819..54955307 100644 --- a/openless-all/app/src/pages/settings/AdvancedSection.tsx +++ b/openless-all/app/src/pages/settings/AdvancedSection.tsx @@ -114,7 +114,7 @@ export function AdvancedSection() { 时延显著降低,但有几个限制(不满足时自动回落原一次性插入路径): - macOS:CGEvent Unicode + 临时切到 ABC 输入源(CJK / 日文 IME 拦截兜底) - Windows:SendInput Unicode,绕过 TSF / IME,不需要切输入法 - - Linux(实验):enigo XTest;Wayland compositor 拒绝 libei 时失败回落 + - Linux(实验):X11 走 enigo + XTest;Wayland 下禁用流式输入并回落剪贴板 - 仅 OpenAI-compatible provider 实装;Gemini / Codex 透明降级 - 密码框 / 1Password / SSH prompt 等 Secure Input 框拒绝合成按键 → 失败回落 每个平台用各自的 hint key,互相不显示对方平台的细节。 */}