From 2f6f4c53ec4a6f91a0e26b77c790ae73b9e67f48 Mon Sep 17 00:00:00 2001 From: sim Date: Thu, 6 Aug 2026 05:09:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(selection-polish):=20Linux=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E2=80=94=E2=80=94fcitx5=20=E6=8F=92=E4=BB=B6=E7=83=AD?= =?UTF-8?q?=E9=94=AE=E8=B7=AF=E7=94=B1=20+=20PRIMARY=20=E9=80=89=E5=8C=BA?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - linux_fcitx.rs:新增 SetSelectionPolishHotkeyRaw DBus 方法 + sync_selection_polish_binding; start_dictation_signal_listener 增加 selection_polish_trigger 参数,监听 SelectionPolishEvent 信号 → HotkeyEvent::SelectionPolishShortcutPressed, 初始同步与 fcitx5 重启重同步均含选区润色触发键 - hotkey.rs Linux adapter:update_modifier_shortcuts 转发 selection_polish_trigger (移除 disabled on Linux 注释) - hotkey_loops.rs:Linux 启动 fcitx5 监听时传入 selection_polish_trigger - selection.rs Linux 分支:is_captured = PRIMARY 读到选区即可;validate 重读 PRIMARY 与捕获文本比较(用户改选/清空即拒绝粘贴);selection_text_matches cfg 覆盖 linux - fcitx5 插件 openless.cpp:新增 SelectionPolishEvent 信号 + SetSelectionPolishHotkeyRaw 方法(INI 持久化 SelectionPolishRawSym/States), 按键分发仿 QA 模式;需重装插件(scripts/inject-fcitx5-plugin.sh)生效 - 注:Linux 代码由 Linux CI 验证编译;fcitx 插件 .so 需 Linux 环境重编译安装 --- .../src-tauri/src/coordinator/hotkey_loops.rs | 3 +- openless-all/app/src-tauri/src/hotkey.rs | 6 +-- openless-all/app/src-tauri/src/linux_fcitx.rs | 41 +++++++++++++++++++ openless-all/app/src-tauri/src/selection.rs | 26 ++++++++++-- .../scripts/linux-fcitx5-plugin/openless.cpp | 37 +++++++++++++++++ 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs b/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs index ffa24b829..57a752c0a 100644 --- a/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs +++ b/openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs @@ -147,7 +147,7 @@ pub(super) fn hotkey_supervisor_loop(inner: Arc) { // Linux: 启动 fcitx5 插件信号监听作为热键源。 #[cfg(target_os = "linux")] { - let (qa_trigger, _selection_polish_trigger, translation_trigger) = + let (qa_trigger, selection_polish_trigger, translation_trigger) = modifier_shortcut_triggers(&inner); let custom_key = custom_dictation_key_string(&inner); crate::linux_fcitx::start_dictation_signal_listener( @@ -155,6 +155,7 @@ pub(super) fn hotkey_supervisor_loop(inner: Arc) { combo_tx_for_fcitx, fcitx_binding.clone(), qa_trigger, + selection_polish_trigger, translation_trigger, custom_key, ); diff --git a/openless-all/app/src-tauri/src/hotkey.rs b/openless-all/app/src-tauri/src/hotkey.rs index ef4bd034c..8a4901515 100644 --- a/openless-all/app/src-tauri/src/hotkey.rs +++ b/openless-all/app/src-tauri/src/hotkey.rs @@ -1737,9 +1737,9 @@ mod platform { translation_trigger: Option, ) { crate::linux_fcitx::sync_qa_binding(qa_trigger); - // Selection Polish ships disabled on Linux for now; the fcitx plugin has - // no corresponding signal route yet. - let _ = selection_polish_trigger; + // 选区润色触发键:fcitx5 插件通过 SelectionPolishEvent 信号回传 + //(插件端需 `scripts/inject-fcitx5-plugin.sh` 重装新版 .so)。 + crate::linux_fcitx::sync_selection_polish_binding(selection_polish_trigger); crate::linux_fcitx::sync_translation_binding(translation_trigger); } diff --git a/openless-all/app/src-tauri/src/linux_fcitx.rs b/openless-all/app/src-tauri/src/linux_fcitx.rs index 5c9aeecbb..08fb1d33c 100644 --- a/openless-all/app/src-tauri/src/linux_fcitx.rs +++ b/openless-all/app/src-tauri/src/linux_fcitx.rs @@ -69,6 +69,18 @@ pub fn set_qa_hotkey_raw(sym: u32, states: u32) -> Result<(), String> { Ok(()) } +/// 通过 fcitx5 插件设置选区润色触发键 sym + states。 +pub fn set_selection_polish_hotkey_raw(sym: u32, states: u32) -> Result<(), String> { + let conn = + dbus::blocking::Connection::new_session().map_err(|e| format!("dbus session: {e}"))?; + let msg = dbus::Message::new_method_call(DEST, PATH, IFACE, "SetSelectionPolishHotkeyRaw") + .map_err(|e| format!("build msg: {e}"))? + .append2(sym, states); + conn.send_with_reply_and_block(msg, TIMEOUT) + .map_err(|e| format!("SetSelectionPolishHotkeyRaw: {e}"))?; + Ok(()) +} + /// 通过 fcitx5 插件设置翻译模式修饰键 sym + states。 pub fn set_translation_hotkey_raw(sym: u32, states: u32) -> Result<(), String> { let conn = @@ -226,6 +238,26 @@ pub fn sync_translation_binding(trigger: Option) { } } +/// 将选区润色触发键同步到 fcitx5 插件。 +pub fn sync_selection_polish_binding(trigger: Option) { + let Some(trigger) = trigger else { + // 无选区润色快捷键时清空插件端配置 + let _ = set_selection_polish_hotkey_raw(0, 0); + return; + }; + if trigger == crate::types::HotkeyTrigger::MediaPlayPause { + return; + } + let sym = trigger_to_keysym(trigger); + let name = trigger_name(trigger); + match set_selection_polish_hotkey_raw(sym, 0) { + Ok(()) => { + log::info!("[fcitx] Synced selection polish hotkey {name} (sym={sym}) to plugin via SetSelectionPolishHotkeyRaw") + } + Err(e) => log::warn!("[fcitx] Failed to sync selection polish hotkey to plugin: {e}"), + } +} + /// 通过 fcitx5 插件在候选词列表下方显示状态文本(不干扰输入法预编辑)。 pub fn set_aux_down(text: &str) -> Result<(), String> { let conn = @@ -279,6 +311,7 @@ pub fn start_dictation_signal_listener( combo_tx: std::sync::mpsc::Sender, binding: crate::types::HotkeyBinding, qa_trigger: Option, + selection_polish_trigger: Option, translation_trigger: Option, custom_trigger_key: Option, ) { @@ -346,6 +379,10 @@ pub fn start_dictation_signal_listener( if is_press { let _ = tx2.send(crate::hotkey::HotkeyEvent::TranslationModifierPressed); } + } else if member == "SelectionPolishEvent" { + if is_press { + let _ = tx2.send(crate::hotkey::HotkeyEvent::SelectionPolishShortcutPressed); + } } } true @@ -379,6 +416,7 @@ pub fn start_dictation_signal_listener( let binding_for_name = binding.clone(); let custom_for_name = custom_trigger_key.clone(); let qa_for_name = qa_trigger; + let polish_for_name = selection_polish_trigger; let trans_for_name = translation_trigger; let _name_match = match conn.add_match(fcitx_rule, move |args: (String, String, String), _conn, _msg| { let (name, _old_owner, new_owner) = args; @@ -391,11 +429,13 @@ pub fn start_dictation_signal_listener( let b = binding_for_name.clone(); let c = custom_for_name.clone(); let q = qa_for_name; + let s = polish_for_name; let t = trans_for_name; std::thread::spawn(move || { std::thread::sleep(Duration::from_secs(1)); // 等插件完全加载 resync_main_binding(&b, c.as_deref()); sync_qa_binding(q); + sync_selection_polish_binding(s); sync_translation_binding(t); }); } @@ -414,6 +454,7 @@ pub fn start_dictation_signal_listener( log::info!("[fcitx-hotkey] fcitx5 available, syncing initial bindings (attempt {attempt})"); resync_main_binding(&binding, custom_trigger_key.as_deref()); sync_qa_binding(qa_trigger); + sync_selection_polish_binding(selection_polish_trigger); sync_translation_binding(translation_trigger); break; } diff --git a/openless-all/app/src-tauri/src/selection.rs b/openless-all/app/src-tauri/src/selection.rs index 6c5f07bbf..1d6ecf0c4 100644 --- a/openless-all/app/src-tauri/src/selection.rs +++ b/openless-all/app/src-tauri/src/selection.rs @@ -157,8 +157,11 @@ pub(crate) fn selection_insertion_target_is_captured( #[cfg(not(any(target_os = "windows", target_os = "macos")))] { + // Linux:无前台窗口校验,靠「选区文本一致性」兜底——capture 时能读到 + // PRIMARY selection(run_selection_polish 已挡掉无选区),validate 时 + // 重读 PRIMARY 比较,变了就拒绝粘贴。 let _ = target; - false + true } } @@ -223,7 +226,19 @@ pub(crate) fn validate_selection_insertion_target( #[cfg(not(any(target_os = "windows", target_os = "macos")))] { - let _ = (target, expected_selection); + // Linux:重读 PRIMARY selection 与捕获文本比较——用户改了选区 / 清空 + // PRIMARY 就拒绝粘贴(fcitx CommitText 直接写焦点输入上下文,无需 + // 恢复窗口焦点,所以这里不需要窗口级校验)。 + let current_selection = match linux_selection::read_selected_text() { + linux_selection::LinuxSelectionRead::Text(text) => { + let trimmed = text.trim(); + (!trimmed.is_empty()).then(|| truncate_selection(trimmed)) + } + _ => None, + }; + if !selection_text_matches(expected_selection, current_selection.as_deref()) { + return SelectionInsertionTargetValidation::SelectionChanged; + } SelectionInsertionTargetValidation::Valid } } @@ -477,7 +492,12 @@ fn selected_text_for_validation() -> Option { (!trimmed.is_empty()).then(|| truncate_selection(trimmed)) } -#[cfg(any(target_os = "windows", target_os = "macos", test))] +#[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "linux", + test +))] fn selection_text_matches(expected: &str, actual: Option<&str>) -> bool { actual.is_some_and(|actual| actual == expected) } diff --git a/openless-all/scripts/linux-fcitx5-plugin/openless.cpp b/openless-all/scripts/linux-fcitx5-plugin/openless.cpp index 5f7c585d7..d97c1361d 100644 --- a/openless-all/scripts/linux-fcitx5-plugin/openless.cpp +++ b/openless-all/scripts/linux-fcitx5-plugin/openless.cpp @@ -21,6 +21,7 @@ * 信号: * DictationKeyEvent(uub: sym, states, isPress) — 听写热键按下/抬起 * QaShortcutEvent(uub: sym, states, isPress) — QA 快捷键按下/抬起 + * SelectionPolishEvent(uub: sym, states, isPress) — 选区润色快捷键按下/抬起 * TranslationModifierEvent(uub: sym, states, isPress) — 翻译修饰键按下/抬起 */ @@ -68,6 +69,8 @@ class OpenLess final : public AddonInstance, triggerRawStates_(0), qaRawSym_(0), qaRawStates_(0), + selectionPolishRawSym_(0), + selectionPolishRawStates_(0), translationRawSym_(0), translationRawStates_(0), hasCustomDictationKey_(false), @@ -180,6 +183,16 @@ class OpenLess final : public AddonInstance, keyEvent.filterAndAccept(); return; } + if (selectionPolishRawSym_ != 0 && + sym == selectionPolishRawSym_ && + states == selectionPolishRawStates_) { + FCITX_LOGC(openless, Debug) + << "Selection polish shortcut"; + selectionPolishEvent(selectionPolishRawSym_, + selectionPolishRawStates_, isPress); + keyEvent.filterAndAccept(); + return; + } bool translationMatched = false; if (translationRawSym_ != 0 && sym == translationRawSym_ && states == translationRawStates_) @@ -417,6 +430,18 @@ class OpenLess final : public AddonInstance, << "SetQaHotkeyRaw: sym=" << sym << " states=" << states; } + void setSelectionPolishHotkeyRaw(uint32_t sym, uint32_t states) { + selectionPolishRawSym_ = sym; + selectionPolishRawStates_ = states; + RawConfig raw; + readAsIni(raw, configFile()); + raw.setValueByPath("SelectionPolishRawSym", std::to_string(sym)); + raw.setValueByPath("SelectionPolishRawStates", std::to_string(states)); + safeSaveAsIni(raw, configFile()); + FCITX_LOGC(openless, Info) + << "SetSelectionPolishHotkeyRaw: sym=" << sym << " states=" << states; + } + void setTranslationHotkeyRaw(uint32_t sym, uint32_t states) { translationRawSym_ = sym; translationRawStates_ = states; @@ -436,10 +461,12 @@ class OpenLess final : public AddonInstance, FCITX_OBJECT_VTABLE_METHOD(setHotkeyRaw, "SetHotkeyRaw", "uu", ""); FCITX_OBJECT_VTABLE_METHOD(setCustomDictationTrigger, "SetCustomDictationTrigger", "s", ""); FCITX_OBJECT_VTABLE_METHOD(setQaHotkeyRaw, "SetQaHotkeyRaw", "uu", ""); + FCITX_OBJECT_VTABLE_METHOD(setSelectionPolishHotkeyRaw, "SetSelectionPolishHotkeyRaw", "uu", ""); FCITX_OBJECT_VTABLE_METHOD(setTranslationHotkeyRaw, "SetTranslationHotkeyRaw", "uu", ""); FCITX_OBJECT_VTABLE_SIGNAL(dictationKeyEvent, "DictationKeyEvent", "uub"); FCITX_OBJECT_VTABLE_SIGNAL(dictationKeyCombined, "DictationKeyCombined", "uub"); FCITX_OBJECT_VTABLE_SIGNAL(qaShortcutEvent, "QaShortcutEvent", "uub"); + FCITX_OBJECT_VTABLE_SIGNAL(selectionPolishEvent, "SelectionPolishEvent", "uub"); FCITX_OBJECT_VTABLE_SIGNAL(translationModifierEvent, "TranslationModifierEvent", "uub"); Instance *instance() { return instance_; } @@ -466,6 +493,14 @@ class OpenLess final : public AddonInstance, auto *v = raw.valueByPath("QaRawStates"); qaRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0; } + { + auto *v = raw.valueByPath("SelectionPolishRawSym"); + selectionPolishRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0; + } + { + auto *v = raw.valueByPath("SelectionPolishRawStates"); + selectionPolishRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0; + } { auto *v = raw.valueByPath("TranslationRawSym"); translationRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0; @@ -515,6 +550,8 @@ class OpenLess final : public AddonInstance, uint32_t triggerRawStates_; uint32_t qaRawSym_; uint32_t qaRawStates_; + uint32_t selectionPolishRawSym_; + uint32_t selectionPolishRawStates_; uint32_t translationRawSym_; uint32_t translationRawStates_; Key customDictationKey_; From 2cb7df33396bddb30333b7043a53656cc66d3342 Mon Sep 17 00:00:00 2001 From: sim Date: Thu, 6 Aug 2026 17:42:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(selection-polish):=20=E8=A1=A5=E4=BC=A0?= =?UTF-8?q?=20selection=5Fpolish=5Ftrigger=20=E5=88=B0=20fcitx=20=E7=9B=91?= =?UTF-8?q?=E5=90=AC=E5=99=A8=EF=BC=88=E7=AC=AC=E4=BA=8C=E4=B8=AA=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E7=82=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coordinator.rs 的 hotkey 安装路径调 start_dictation_signal_listener 时漏传 selection_polish_trigger(签名 7 参数只传 6 个),Linux CI E0061。hotkey_loops.rs 已传,此处遗漏导致 Linux 编译失败。 --- openless-all/app/src-tauri/src/coordinator.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index fd7a7481f..0ab48a0d6 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -1711,7 +1711,7 @@ impl Coordinator { // Linux: 启动 fcitx5 插件信号监听作为热键源。 #[cfg(target_os = "linux")] { - let (qa_trigger, _selection_polish_trigger, translation_trigger) = + let (qa_trigger, selection_polish_trigger, translation_trigger) = modifier_shortcut_triggers(&self.inner); let custom_key = custom_dictation_key_string(&self.inner); crate::linux_fcitx::start_dictation_signal_listener( @@ -1719,6 +1719,7 @@ impl Coordinator { combo_tx_for_fcitx, fcitx_binding.clone(), qa_trigger, + selection_polish_trigger, translation_trigger, custom_key, );