Files
openless/openless-all/scripts/linux-fcitx5-plugin/openless.cpp
T
aeoformandClaude Opus 4.7 9cd8b9a8fc fix(linux): clear previous hotkey trigger when switching between custom combo and preset modifier
setHotkey/setHotkeyRaw now clear hasCustomDictationKey_ to prevent stale custom
combo from persisting after switching to a preset modifier. setCustomDictationTrigger
now persists TriggerRawSym=TriggerRawStates=0 to raw config so the old raw
trigger doesn't reload after fcitx5 restart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 09:53:25 +08:00

433 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-FileCopyrightText: 2025 OpenLess Contributors
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
* fcitx5 插件 — 供 OpenLess 听写文字提交 + 快捷键监听。
*
* DBus 接口: org.fcitx.Fcitx.OpenLess1 (对象路径 /openless)
* 方法:
* CommitText(s: text) — 将文字提交到当前焦点输入上下文
* 安全性:本接口在会话总线(session bus)上对同用户
* 所有进程开放,此为 fcitx5/IBus 体系的标准安全模型
* (非特权进程隔离)。
* SetHotkey(as: keys) — 设置听写触发快捷键 (Key::parse 格式)
* SetHotkeyRaw(uu: sym, states) — 直接设听写触发 sym+states (不走 parse)
* SetCustomDictationTrigger(s: keyString) — 设置自定义组合键 (Key::parse 格式)
* SetQaHotkeyRaw(uu: sym, states) — 直接设 QA 面板触发 sym+states
* SetTranslationHotkeyRaw(uu: sym, states) — 直接设翻译模式触发 sym+states
* 信号:
* DictationKeyEvent(uub: sym, states, isPress) — 听写热键按下/抬起
* QaShortcutEvent(uub: sym, states, isPress) — QA 快捷键按下/抬起
* TranslationModifierEvent(uub: sym, states, isPress) — 翻译修饰键按下/抬起
*/
#include <memory>
#include <string>
#include <vector>
#include <fcitx-config/configuration.h>
#include <fcitx-config/iniparser.h>
#include <fcitx-config/option.h>
#include <fcitx-utils/dbus/bus.h>
#include <fcitx-utils/dbus/objectvtable.h>
#include <fcitx-utils/handlertable.h>
#include <fcitx-utils/i18n.h>
#include <fcitx-utils/key.h>
#include <fcitx-utils/log.h>
#include <fcitx/addonfactory.h>
#include <fcitx/addoninstance.h>
#include <fcitx/addonmanager.h>
#include <fcitx/event.h>
#include <fcitx/inputcontext.h>
#include <fcitx/inputcontextmanager.h>
#include <fcitx/instance.h>
#include <fcitx-module/dbus/dbus_public.h>
FCITX_DEFINE_LOG_CATEGORY(openless, "openless");
namespace fcitx {
FCITX_CONFIGURATION(OpenLessConfig,
KeyListOption triggerKey{this,
"TriggerKey",
_("Dictation trigger key"),
{},
KeyListConstrain()};
);
class OpenLess final : public AddonInstance,
public dbus::ObjectVTable<OpenLess> {
public:
OpenLess(Instance *instance)
: instance_(instance),
triggerRawSym_(0),
triggerRawStates_(0),
qaRawSym_(0),
qaRawStates_(0),
translationRawSym_(0),
translationRawStates_(0),
hasCustomDictationKey_(false),
savedIc_(nullptr) {
// 1. 读取配置
reloadConfig();
// 2. 注册 DBus 接口
auto *dbusMod = instance_->addonManager().addon("dbus", true);
if (dbusMod) {
auto *bus = dbusMod->call<IDBusModule::bus>();
if (bus) {
bus->addObjectVTable(
"/openless",
"org.fcitx.Fcitx.OpenLess1",
*this);
FCITX_LOGC(openless, Info)
<< "DBus interface registered at /openless";
} else {
FCITX_LOGC(openless, Warn)
<< "Failed to get DBus bus";
}
} else {
FCITX_LOGC(openless, Warn)
<< "DBus module not available";
}
// 3. 注册快捷键事件监听
eventHandlers_.push_back(
instance_->watchEvent(
EventType::InputContextKeyEvent,
EventWatcherPhase::PreInputMethod,
[this](Event &event) {
auto &keyEvent = static_cast<KeyEvent &>(event);
// 保存当前输入上下文:快捷键按下时用户在目标 app 中,
// 此后胶囊窗口可能抢走焦点,但 commitText 仍能用此 IC 提交文字。
if (!keyEvent.isRelease()) {
savedIc_ = keyEvent.inputContext();
}
auto sym = static_cast<uint32_t>(keyEvent.key().sym());
auto states = static_cast<uint32_t>(keyEvent.key().states());
bool isPress = !keyEvent.isRelease();
// 检查自定义组合键(优先级最高)
if (hasCustomDictationKey_ &&
keyEvent.key().sym() == customDictationKey_.sym() &&
keyEvent.key().states() == customDictationKey_.states()) {
FCITX_LOGC(openless, Debug)
<< "Custom dictation combo: sym=" << sym
<< " states=" << states
<< " isPress=" << isPress;
dictationKeyEvent(
static_cast<uint32_t>(customDictationKey_.sym()),
static_cast<uint32_t>(customDictationKey_.states()),
isPress);
keyEvent.filterAndAccept();
return;
}
// 检查听写触发键(raw + keylist 双路径)
if ((triggerRawSym_ != 0 &&
sym == triggerRawSym_ &&
states == triggerRawStates_) ||
(triggerRawSym_ == 0 && [&]() {
for (const auto &hk : triggerKeyList_) {
if (sym == static_cast<uint32_t>(hk.sym()) &&
states == static_cast<uint32_t>(hk.states()))
return true;
}
return false;
}())) {
auto dsym = triggerRawSym_ != 0
? triggerRawSym_
: static_cast<uint32_t>(triggerKeyList_[0].sym());
auto dstates = triggerRawStates_ != 0
? triggerRawStates_
: static_cast<uint32_t>(triggerKeyList_[0].states());
FCITX_LOGC(openless, Debug)
<< "Dictation hotkey: sym=" << dsym
<< " states=" << dstates
<< " isPress=" << isPress;
dictationKeyEvent(dsym, dstates, isPress);
keyEvent.filterAndAccept();
return;
}
// 检查 QA 快捷键
if (qaRawSym_ != 0 &&
sym == qaRawSym_ &&
states == qaRawStates_) {
FCITX_LOGC(openless, Debug)
<< "QA shortcut: sym=" << qaRawSym_
<< " states=" << qaRawStates_
<< " isPress=" << isPress;
qaShortcutEvent(qaRawSym_, qaRawStates_, isPress);
keyEvent.filterAndAccept();
return;
}
// 检查翻译模式修饰键(自定义 + 内置 Shift)
bool translationMatched = false;
if (translationRawSym_ != 0 &&
sym == translationRawSym_ &&
states == translationRawStates_) {
translationMatched = true;
}
// 内置 Shift 修饰键
if (sym == 0xffe1 || sym == 0xffe2) {
translationMatched = true;
}
if (translationMatched) {
FCITX_LOGC(openless, Debug)
<< "Translation modifier: sym=" << sym
<< " states=" << states
<< " isPress=" << isPress;
translationModifierEvent(sym, states, isPress);
keyEvent.filterAndAccept();
return;
}
}));
// 4. 监听 InputContext 销毁事件,自动清空 savedIc_ 避免野指针
eventHandlers_.push_back(
instance_->watchEvent(
EventType::InputContextDestroyed,
EventWatcherPhase::Default,
[this](Event &event) {
auto &icEvent = static_cast<InputContextEvent &>(event);
if (icEvent.inputContext() == savedIc_) {
savedIc_ = nullptr;
}
}));
FCITX_LOGC(openless, Info) << "OpenLess plugin loaded";
}
~OpenLess() = default;
// ---- DBus 方法 ----
// 返回 void 而非 std::tuple<>,以匹配 FCITX_OBJECT_VTABLE_METHOD 的 RET("")
void commitText(const std::string &text) {
// 优先使用快捷键按下时保存的输入上下文(savedIc_),
// 此时用户在目标 app 中,此后胶囊窗口抢焦点不影响提交。
// 若 savedIc_ 为空则兜底用 foreachFocused。
auto *ic = savedIc_;
if (!ic) {
FCITX_LOGC(openless, Warn)
<< "CommitText: savedIc_ is null, trying foreachFocused";
auto &mgr = instance_->inputContextManager();
mgr.foreachFocused([&](InputContext *focusedIc) {
ic = focusedIc;
return false;
});
}
if (!ic) {
FCITX_LOGC(openless, Warn)
<< "CommitText: no input context available";
throw std::runtime_error("no focused input context");
}
FCITX_LOGC(openless, Debug) << "CommitText: " << text;
ic->commitString(text);
}
void setHotkey(const std::vector<std::string> &keys) {
// 切换预设修饰键时清空自定义组合键,避免双发
hasCustomDictationKey_ = false;
KeyList keyList;
for (const auto &s : keys) {
Key key(s);
if (key.isValid()) {
keyList.push_back(key);
} else {
FCITX_LOGC(openless, Warn)
<< "SetHotkey: invalid key '" << s << "'";
}
}
config_.triggerKey.setValue(keyList);
// KeyList 路径激活时清空 raw 路径,避免优先级冲突
triggerRawSym_ = 0;
triggerRawStates_ = 0;
safeSaveAsIni(config_, configFile());
// 同时清除磁盘上残留的 TriggerRawSym/TriggerRawStates(旧 raw 模式的持久化值),
// 防止下次 fcitx5 重启 reloadConfig 重新加载旧 raw 热键覆盖新配置。
{
RawConfig raw;
readAsIni(raw, configFile());
raw.setValueByPath("TriggerRawSym", "0");
raw.setValueByPath("TriggerRawStates", "0");
safeSaveAsIni(raw, configFile());
}
rebuildTriggerKeys();
}
void setHotkeyRaw(uint32_t sym, uint32_t states) {
// 切换预设修饰键时清空自定义组合键,避免双发
hasCustomDictationKey_ = false;
triggerRawSym_ = sym;
triggerRawStates_ = states;
// 同时尝试维护 KeyList(如果 sym 可转为有效 key)
Key key(static_cast<KeySym>(sym),
static_cast<KeyStates>(states));
if (key.isValid()) {
KeyList keys = {key};
config_.triggerKey.setValue(keys);
} else {
// 修饰键无法用 KeyList 表达,清空 KeyList 避免误匹配
config_.triggerKey.setValue(KeyList{});
}
// 合并写入 config 和 raw sym/states
RawConfig raw;
raw.setValueByPath("TriggerRawSym", std::to_string(sym));
raw.setValueByPath("TriggerRawStates", std::to_string(states));
config_.save(raw);
safeSaveAsIni(raw, configFile());
rebuildTriggerKeys();
}
void setCustomDictationTrigger(const std::string &keyString) {
Key key(keyString);
if (!key.isValid()) {
FCITX_LOGC(openless, Warn)
<< "SetCustomDictationTrigger: invalid key '" << keyString << "'";
hasCustomDictationKey_ = false;
return;
}
customDictationKey_ = key;
hasCustomDictationKey_ = true;
// 有自定义键时清空已有 raw+keylist 路径,避免双发
triggerRawSym_ = 0;
triggerRawStates_ = 0;
config_.triggerKey.setValue(KeyList{});
// 同时持久化清空 TriggerRawSym/TriggerRawStates,防止 fcitx5 重启后从 INI 加载旧值
{
RawConfig raw;
readAsIni(raw, configFile());
config_.save(raw);
raw.setValueByPath("TriggerRawSym", "0");
raw.setValueByPath("TriggerRawStates", "0");
safeSaveAsIni(raw, configFile());
}
FCITX_LOGC(openless, Info)
<< "SetCustomDictationTrigger: '" << keyString << "'"
<< " sym=" << static_cast<uint32_t>(key.sym())
<< " states=" << static_cast<uint32_t>(key.states());
}
void setQaHotkeyRaw(uint32_t sym, uint32_t states) {
qaRawSym_ = sym;
qaRawStates_ = states;
RawConfig raw;
readAsIni(raw, configFile());
raw.setValueByPath("QaRawSym", std::to_string(sym));
raw.setValueByPath("QaRawStates", std::to_string(states));
safeSaveAsIni(raw, configFile());
FCITX_LOGC(openless, Info)
<< "SetQaHotkeyRaw: sym=" << sym << " states=" << states;
}
void setTranslationHotkeyRaw(uint32_t sym, uint32_t states) {
translationRawSym_ = sym;
translationRawStates_ = states;
RawConfig raw;
readAsIni(raw, configFile());
raw.setValueByPath("TranslationRawSym", std::to_string(sym));
raw.setValueByPath("TranslationRawStates", std::to_string(states));
safeSaveAsIni(raw, configFile());
FCITX_LOGC(openless, Info)
<< "SetTranslationHotkeyRaw: sym=" << sym << " states=" << states;
}
FCITX_OBJECT_VTABLE_METHOD(commitText, "CommitText", "s", "");
FCITX_OBJECT_VTABLE_METHOD(setHotkey, "SetHotkey", "as", "");
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(setTranslationHotkeyRaw, "SetTranslationHotkeyRaw", "uu", "");
FCITX_OBJECT_VTABLE_SIGNAL(dictationKeyEvent, "DictationKeyEvent", "uub");
FCITX_OBJECT_VTABLE_SIGNAL(qaShortcutEvent, "QaShortcutEvent", "uub");
FCITX_OBJECT_VTABLE_SIGNAL(translationModifierEvent, "TranslationModifierEvent", "uub");
Instance *instance() { return instance_; }
void reloadConfig() override {
readAsIni(config_, configFile());
// 加载原始 sym/states(由 SetHotkeyRaw / SetQaHotkeyRaw / SetTranslationHotkeyRaw 写入的持久化键值)
RawConfig raw;
readAsIni(raw, configFile());
{
auto *v = raw.valueByPath("TriggerRawSym");
triggerRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("TriggerRawStates");
triggerRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("QaRawSym");
qaRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("QaRawStates");
qaRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("TranslationRawSym");
translationRawSym_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
{
auto *v = raw.valueByPath("TranslationRawStates");
translationRawStates_ = v ? std::stoul(*v, nullptr, 0) : 0;
}
rebuildTriggerKeys();
}
const Configuration *getConfig() const override {
return &config_;
}
void setConfig(const RawConfig &rawConfig) override {
config_.load(rawConfig, true);
safeSaveAsIni(config_, configFile());
rebuildTriggerKeys();
}
private:
static constexpr const char *configFile() {
return "conf/openless.conf";
}
void rebuildTriggerKeys() {
triggerKeyList_ = config_.triggerKey.value();
}
Instance *instance_;
OpenLessConfig config_;
KeyList triggerKeyList_;
uint32_t triggerRawSym_;
uint32_t triggerRawStates_;
uint32_t qaRawSym_;
uint32_t qaRawStates_;
uint32_t translationRawSym_;
uint32_t translationRawStates_;
Key customDictationKey_;
bool hasCustomDictationKey_;
/// 快捷键按下时保存的输入上下文指针,用于 commitText 在失焦后仍能提交文字。
/// 事件处理线程和 DBus 处理线程都是 fcitx5 主事件循环,无竞态。
/// 通过 InputContextDestroyed 事件监听 IC 销毁时自动清空指针。
InputContext *savedIc_;
std::vector<std::unique_ptr<HandlerTableEntry<EventHandler>>>
eventHandlers_;
};
class OpenLessFactory : public AddonFactory {
public:
AddonInstance *create(AddonManager *manager) override {
return new OpenLess(manager->instance());
}
};
} // namespace fcitx
FCITX_ADDON_FACTORY(fcitx::OpenLessFactory);