Skip to content

feat(linux): fcitx5 plugin for Wayland input + deb packaging#451

Open
aeoform wants to merge 8 commits into
Open-Less:betafrom
aeoform:beta
Open

feat(linux): fcitx5 plugin for Wayland input + deb packaging#451
aeoform wants to merge 8 commits into
Open-Less:betafrom
aeoform:beta

Conversation

@aeoform
Copy link
Copy Markdown

@aeoform aeoform commented May 15, 2026

User description

Summary

  • fcitx5 C++ plugin (scripts/linux-fcitx5-plugin/): DBus interface with CommitText, SetHotkey/SetHotkeyRaw methods and DictationKeyEvent signal. Replaces enigo/XTest for Wayland/X11 compatibility.
  • linux_fcitx.rs: DBus client connecting Rust backend to the plugin.
  • Wayland: 胶囊窗口没了emit_capsule 在 Wayland 上跳过所有窗口操作(show/hide/reposition),目标 app 始终持有键盘焦点。文字通过 fcitx5 插件 commitString 直接进入目标 app —— 和输入法的行为一致。
  • X11: 保持胶囊窗口(首次 Idle→可见时 show 一次)。
  • Insertion: 插入优先走 fcitx5 commit_text(支持中文),失败降级到剪贴板拷贝。
  • 流式输入: streaming_insert_eligible 不再被 Wayland 阻断。
  • CI/打包: Linux CI 自动编译 fcitx5 插件并打进 deb/rpm;deb 依赖 fcitx5, fcitx5-module-dbus;安装后插件 .so 在 /usr/lib/fcitx5/,重启 fcitx5 即自动加载。
  • 修复: mod linux_fcitx 加了 #[cfg(target_os = "linux")],macOS/Windows CI 不再因为 dbus crate 找不到而编译失败。

使用方式

  1. 安装 deb 包(自动装上 fcitx5 依赖和插件)
  2. 确保 fcitx5 已设为系统输入法
  3. 重启 fcitx5(或重新登录)
  4. OpenLess 会自动通过 DBus 调用插件提交文字

Test plan

  • Linux/Wayland: 听写文字能通过 fcitx5 插件输入到目标 app,胶囊窗口不弹出
  • Linux/X11: 胶囊窗口正常显示,行为不变
  • macOS/Windows CI: cargo check 通过(cfg gate 验证)
  • Linux cargo check + unit tests: 通过
  • deb 安装后 /usr/lib/fcitx5/libopenless.so 存在
  • 重启 fcitx5 后插件自动加载:fcitx5 -rd 无报错

PR Type

Enhancement, Bug fix


Description

  • Add fcitx5 DBus plugin and client

  • Route Linux dictation through fcitx5

  • Preserve Wayland focus by skipping capsule

  • Bundle plugin in Linux packages

  • Refresh fallback messages and tests


Diagram Walkthrough

flowchart LR
  A["Coordinator / hotkey flow"] -- "sync hotkeys, listen signals" --> B["fcitx5 OpenLess plugin"]
  A -- "commit text on Linux" --> B
  B -- "deliver input context text" --> C["Focused app"]
  A -- "clipboard fallback" --> D["Clipboard"]
  E["GitHub Actions packaging"] -- "build and bundle plugin" --> B
Loading

File Walkthrough

Relevant files
Bug fix
2 files
coordinator.rs
Sync Linux hotkeys and capsule focus                                         
+64/-2   
unicode_keystroke.rs
Replace Linux enigo typing path                                                   
+9/-24   
Enhancement
4 files
dictation.rs
Commit Wayland text through fcitx5                                             
+48/-36 
insertion.rs
Prefer fcitx5 for Linux insertion                                               
+15/-0   
linux_fcitx.rs
Add Linux fcitx5 DBus client                                                         
+176/-0 
openless.cpp
Implement fcitx5 dictation plugin                                               
+277/-0 
Configuration changes
5 files
lib.rs
Gate Linux fcitx module loading                                                   
+2/-0     
build.sh
Add plugin build and install helper                                           
+29/-0   
release-tauri.yml
Package fcitx5 plugin in releases                                               
+56/-6   
CMakeLists.txt
Build and install fcitx5 addon                                                     
+45/-0   
openless.conf.in
Define plugin metadata and dependencies                                   
+15/-0   
Dependencies
1 files
Cargo.toml
Add Linux DBus dependency                                                               
+2/-0     

aeoform and others added 3 commits May 15, 2026 23:43
- fcitx5 C++ plugin (scripts/linux-fcitx5-plugin/) with DBus interface:
  CommitText, SetHotkey/SetHotkeyRaw, DictationKeyEvent signal
- linux_fcitx.rs: DBus client to call plugin from Rust
- coordinator/dictation.rs: Wayland insertion via fcitx5 commit_text,
  streaming insert enabled on Wayland
- insertion.rs: fcitx5 commit_text on Linux with clipboard fallback
- unicode_keystroke.rs: Linux path uses fcitx5 commit_text
- Capsule window show-once on Linux to avoid stealing focus

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add #[cfg(target_os = "linux")] to mod linux_fcitx to fix macOS/Windows
  CI compilation (dbus crate is Linux-only).
- Wayland: skip capsule window show/hide entirely in emit_capsule so the
  target app never loses keyboard focus. Text is committed via fcitx5
  plugin commit_string — no window means the compositor forwards the
  commit to the right app.
- X11 keeps existing behavior (show capsule window once per session).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add "Build fcitx5 plugin" step to release-tauri.yml: compiles
  the C++ plugin via cmake, copies .so + .conf to
  src-tauri/linux-fcitx5-plugin/ for the Tauri bundler.
- Override tauri build --config to include deb.files / rpm.files
  that place the plugin at /usr/lib/fcitx5/ so it's auto-detected
  after install.
- Add fcitx5, fcitx5-module-dbus as deb/rpm dependencies.
- Local builds unaffected (config is CI-only via --config flag).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

PR Reviewer Guide 🔍

(Review updated until commit dce03b6)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Lost DBus match

The signal listener registers a DBus match but does not keep the returned match token alive. In dbus-rs, dropping that token typically unregisters the match immediately, so DictationKeyEvent may never reach the coordinator and the Wayland hotkey path can stop working.

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";
Stale raw config

SetHotkeyRaw persists TriggerRawSym and TriggerRawStates, but SetHotkey only rewrites the structured config. If the plugin was previously in raw mode, those raw keys can remain in the config file and be reloaded on the next fcitx5 restart, so the old trigger keeps overriding the new hotkey.

void setHotkey(const std::vector<std::string> &keys) {
    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());
    rebuildTriggerKeys();
}

void setHotkeyRaw(uint32_t sym, uint32_t states) {
    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();

When ensure_modifier_hotkey_monitor finds an existing monitor and
updates the rdev/CGEventTap binding, it must also sync the new binding
to the fcitx5 plugin on Wayland. Previously the early return skipped
this, leaving plugin and coordinator out of sync after a hotkey change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aeoform
Copy link
Copy Markdown
Author

aeoform commented May 15, 2026

已修复 stale sync 问题:

ensure_modifier_hotkey_monitor 在 monitor 已存在、仅更新 binding 时,现在也会同步到 fcitx5 插件(sync_binding_to_plugin),确保设置里改热键后插件和 coordinator 热键一致。

其他两个 review 点:

  • DBus 安全 — session bus 本身有用户隔离,与 fcitx5 其他插件的安全模型一致。后续可加 sender PID 验证。
  • Linux fallback — 已删除 enigo/XTest,因为 Wayland 不支持,而 X11 用户有 fcitx5 插件(必装)。不插件的 Linux 流式输入会显示错误,非流式路径仍有剪贴板兜底。

@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 265babd

@aeoform
Copy link
Copy Markdown
Author

aeoform commented May 15, 2026

后续我会考虑加上其他的输入法版本。

FCITX_INSTALL_ADDONDIR varies by distro (multiarch Ubuntu →
/usr/lib/x86_64-linux-gnu/fcitx5/; Fedora → /usr/lib64/fcitx5/;
Arch → /usr/lib/fcitx5/). Extract from cmake cache at build time
via GITHUB_ENV instead of hardcoding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aeoform
Copy link
Copy Markdown
Author

aeoform commented May 15, 2026

已修复安装路径问题:cmake 运行时通过 FCITX_INSTALL_ADDONDIR 探测当前 distro 的实际插件目录(multiarch 感知),通过 $GITHUB_ENV 传递给 tauri --config JSON。

关于 savedIc_ 野指针

  • 事件处理(hotkey)和 DBus 处理(commitText)都在 fcitx5 主事件循环同线程执行,无并发问题
  • 听写会话通常很短(几个秒),用户在这期间不太可能切换/关闭目标 app
  • InputContext 在被激活使用时不会被 fcitx5 销毁
  • foreachFocused 兜底处理了 savedIc_ 为空的情况
  • 后续可以通过 InputContext 的生命周期信号(如 destroyed)加弱引用追踪器

@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit e7cfc14

Prevents savedIc_ from becoming a dangling pointer by connecting to the
InputContext::destroyed signal when the IC is saved. On destruction the
pointer is cleared automatically, and commitText falls through to
foreachFocused for the current focused IC.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 9e81339

…syntax

ic->connect(ic->destroyed, ...) is invalid — InputContext has no two-arg
connect overload. Use ic->destroyed.connect(callback) directly, which
calls Signal<void()>::connect. Also simplified ScopedConnection storage
from unique_ptr to direct member (ScopedConnection is move-assignable).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 1802ed7

fcitx5 插件 Wayland/X11 都可使用,不再为 X11 单独维护 enigo XTest。
Linux 统一走 fcitx5 CommitText 直写,插件不可用时降级到剪贴板拷贝。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aeoform
Copy link
Copy Markdown
Author

aeoform commented May 15, 2026

Linux 统一走 fcitx5 输入法的决策说明

最新提交(dce03b6)移除了 Linux 上的 enigo XTest 路径,X11 也和 Wayland 一样统一走 fcitx5 插件 CommitText 直写。

原因:

  • enigo XTest 在 Wayland 完全不可用,而 X11 上 fcitx5 也一样能用
  • XTest 合成键盘事件在不同输入法栈(ibus / fcitx5 / 无输入法)下行为不一致,容易出现"打了字母但输入法吞掉"、"中英文状态不对"等奇怪问题
  • fcitx5 CommitText 是输入法原生提交接口,直接送字上屏,不受当前输入法状态影响,也不会触发候选框
  • 维护一条路径比两条路径省心,出问题只需排查 fcitx5 插件(C++ DBus)和 Rust DBus 客户端

降级保障:

  • fcitx5 插件不可用时(fcitx5 未运行或插件未加载),自动降级到剪贴板拷贝,用户手动粘贴
  • deb 包的 Depends 已包含 fcitx5 + fcitx5-module-dbus,apt 安装时自动装上

AppImage:

  • AppImage 产物不含 fcitx5 插件(无法安装系统路径),用户需确保系统已装 fcitx5

@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit dce03b6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant