From 5fd991ae1f2c6b9053456ddcf54a1b1ec26b5bf1 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 15 May 2026 07:41:08 +0800 Subject: [PATCH 1/2] Prevent Wayland dictation text loss Wayland cannot prove synthetic typing or paste delivery, so Linux Wayland now bypasses streaming success semantics and keeps completed dictation text in the clipboard with explicit fallback status. The Phase 1 plan is recorded at repo root to keep follow-up work aligned with the current CLI trigger decision. Constraint: Wayland security model does not provide X11-style global key or cross-app text injection guarantees. Rejected: Treating enigo or simulated paste success as inserted on Wayland | It can report success while the target app receives no text. Confidence: high Scope-risk: narrow Directive: Keep Wayland automatic input as an explicitly probed future enhancement; do not silently restore fake insertion success. Tested: cargo fmt --manifest-path openless-all/app/src-tauri/Cargo.toml; cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib coordinator::dictation; cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib focus_restore_failure_uses_specific_error_code; git diff --check; cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml Not-tested: Real Wayland compositor clipboard behavior and Windows TSF runtime behavior on native hosts. --- issue-420-wayland-plan.md | 317 ++++++++++++++++++ openless-all/app/src-tauri/src/coordinator.rs | 4 +- .../src-tauri/src/coordinator/dictation.rs | 160 +++++++-- 3 files changed, 460 insertions(+), 21 deletions(-) create mode 100644 issue-420-wayland-plan.md 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") + ); + } } From c4cfad3632e81c1e0b25ebeb0b49bcb42642ac4b Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Fri, 15 May 2026 07:44:44 +0800 Subject: [PATCH 2/2] Align Wayland trigger guidance Wayland now uses CLI-triggered dictation and copy-only output safety, so user-visible Linux hints should not imply that Wayland streaming insertion or global hotkeys are still attempted inside OpenLess. Constraint: Wayland trigger support is currently delivered through desktop shortcuts invoking openless CLI flags. Rejected: Leaving old best-effort Wayland wording in Settings | It conflicts with Phase 1 copy-only behavior and the CLI trigger path. Confidence: high Scope-risk: narrow Directive: Keep future portal/libei language in research docs, not current-product Settings hints. Tested: npm run build; cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml; git diff --check Not-tested: Live Wayland Settings rendering on GNOME/KDE/Hyprland/sway. --- openless-all/app/src-tauri/src/types.rs | 2 +- openless-all/app/src/i18n/en.ts | 2 +- openless-all/app/src/i18n/ja.ts | 2 +- openless-all/app/src/i18n/ko.ts | 2 +- openless-all/app/src/i18n/zh-CN.ts | 2 +- openless-all/app/src/i18n/zh-TW.ts | 2 +- openless-all/app/src/pages/settings/AdvancedSection.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) 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,互相不显示对方平台的细节。 */}