feat(selection-polish): Linux 支持——fcitx5 插件热键路由 + PRIMARY 选区校验 (#927)

* feat(selection-polish): Linux 支持——fcitx5 插件热键路由 + PRIMARY 选区校验

- 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 环境重编译安装

* fix(selection-polish): 补传 selection_polish_trigger 到 fcitx 监听器(第二个调用点)

coordinator.rs 的 hotkey 安装路径调 start_dictation_signal_listener 时漏传
selection_polish_trigger(签名 7 参数只传 6 个),Linux CI E0061。hotkey_loops.rs
已传,此处遗漏导致 Linux 编译失败。

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
TRIPandsim authored and GitHub committed 2026-08-06 19:32:39 +08:00
1 parent 7655dcc7fa
commit b68a5901c3
6 files changed
+108 -8

No files matched your search

@@ -1706,7 +1706,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(
@@ -1714,6 +1714,7 @@ impl Coordinator {
combo_tx_for_fcitx,
fcitx_binding.clone(),
qa_trigger,
selection_polish_trigger,
translation_trigger,
custom_key,
);
@@ -147,7 +147,7 @@ pub(super) fn hotkey_supervisor_loop(inner: Arc<Inner>) {
// 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<Inner>) {
combo_tx_for_fcitx,
fcitx_binding.clone(),
qa_trigger,
selection_polish_trigger,
translation_trigger,
custom_key,
);
+3 -3
View File
@@ -1737,9 +1737,9 @@ mod platform {
translation_trigger: Option<HotkeyTrigger>,
) {
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);
}
@@ -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<crate::types::HotkeyTrigger>) {
}
}
/// 将选区润色触发键同步到 fcitx5 插件。
pub fn sync_selection_polish_binding(trigger: Option<crate::types::HotkeyTrigger>) {
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<u64>,
binding: crate::types::HotkeyBinding,
qa_trigger: Option<crate::types::HotkeyTrigger>,
selection_polish_trigger: Option<crate::types::HotkeyTrigger>,
translation_trigger: Option<crate::types::HotkeyTrigger>,
custom_trigger_key: Option<String>,
) {
@@ -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;
}
+23 -3
View File
@@ -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<String> {
(!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)
}
@@ -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 @@ public:
triggerRawStates_(0),
qaRawSym_(0),
qaRawStates_(0),
selectionPolishRawSym_(0),
selectionPolishRawStates_(0),
translationRawSym_(0),
translationRawStates_(0),
hasCustomDictationKey_(false),
@@ -180,6 +183,16 @@ public:
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 @@ public:
<< "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 @@ public:
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 @@ public:
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 @@ private:
uint32_t triggerRawStates_;
uint32_t qaRawSym_;
uint32_t qaRawStates_;
uint32_t selectionPolishRawSym_;
uint32_t selectionPolishRawStates_;
uint32_t translationRawSym_;
uint32_t translationRawStates_;
Key customDictationKey_;