Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1711,14 +1711,15 @@ 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(
fcitx_tx,
combo_tx_for_fcitx,
fcitx_binding.clone(),
qa_trigger,
selection_polish_trigger,
translation_trigger,
custom_key,
);
Expand Down
3 changes: 2 additions & 1 deletion openless-all/app/src-tauri/src/coordinator/hotkey_loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,15 @@ 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(
fcitx_tx,
combo_tx_for_fcitx,
fcitx_binding.clone(),
qa_trigger,
selection_polish_trigger,
translation_trigger,
custom_key,
);
Expand Down
6 changes: 3 additions & 3 deletions openless-all/app/src-tauri/src/hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
41 changes: 41 additions & 0 deletions openless-all/app/src-tauri/src/linux_fcitx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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>,
) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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);
});
}
Expand All @@ -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;
}
Expand Down
26 changes: 23 additions & 3 deletions openless-all/app/src-tauri/src/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
37 changes: 37 additions & 0 deletions openless-all/scripts/linux-fcitx5-plugin/openless.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) — 翻译修饰键按下/抬起
*/

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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_)
Expand Down Expand Up @@ -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;
Expand All @@ -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_; }
Expand All @@ -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;
Expand Down Expand Up @@ -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_;
Expand Down
Loading