Skip to content

feat(linux): fcitx5 候选框听写状态提示 + AppImage 插件自动安装 + 热键启动容错#520

Open
aeoform wants to merge 8 commits into
Open-Less:betafrom
aeoform:feat/linux-fcitx5-status-popup
Open

feat(linux): fcitx5 候选框听写状态提示 + AppImage 插件自动安装 + 热键启动容错#520
aeoform wants to merge 8 commits into
Open-Less:betafrom
aeoform:feat/linux-fcitx5-status-popup

Conversation

@aeoform
Copy link
Copy Markdown
Contributor

@aeoform aeoform commented May 22, 2026

User description

概述

Linux 上通过 fcitx5 输入法候选框下方显示听写状态文字,替代 macOS/Windows 的胶囊窗口。同时修复 AppImage 不含 fcitx5 插件、热键启动时序等问题。

改动

fcitx5 插件 (openless.cpp)

  • 新增 SetAuxDown/ClearAuxDown DBus 接口
  • 6 种状态提示:🎤 收音中 → 🔄 识别中 → ✨ 润色中 → ✅ 已插入 / — 已取消 / ❌ 出错
  • InputContextFocusIn 监听:切窗口自动跟随焦点
  • 优先当前焦点 IC,失焦 IC 降级兜底
  • flushUI 先排空旧事件再设 auxDown,防止按键竞态覆盖

热键启动容错 (linux_fcitx.rs)

  • start_dictation_signal_listener 启动时等待 fcitx5 最多 30s
  • 监听 NameOwnerChanged,fcitx5 重启后自动重新同步快捷键

AppImage 插件自动安装

  • CI 将 libopenless.so 打入 AppImage 资源
  • 启动时检测插件缺失 → 自动安装到 ~/.local/lib/fcitx5/~/.local/share/fcitx5/addon/
  • deb/rpm 仍通过 files 映射安装到系统路径

其他

  • Linux 胶囊窗口不显示,状态完全走 fcitx5 输入面板
  • coordinator.rs / dictation.rs 移除冗余调用,统一文案

测试

  • fcitx5 候选框下方显示听写状态
  • 切换窗口状态跟随焦点
  • fcitx5 后启动时热键自动补同步
  • AppImage 首次启动自动安装插件到 ~/.local/

🤖 Generated with Claude Code


PR Type

Bug fix, Enhancement, Tests


Description

  • Show Linux status in fcitx5

    • Render dictation state via auxDown
    • Follow focus changes across windows
  • Auto-install bundled AppImage plugin

    • Copy plugin into ~/.local/
    • Avoid overwriting system installs
  • Resync hotkeys after fcitx5 restarts

    • Wait for fcitx5 startup
    • Watch NameOwnerChanged events
  • Fix duplicate DashScope transcripts

    • Track finals by sentence_id
    • Ignore interim end_time: 0

Diagram Walkthrough

flowchart LR
  ASR["DashScope realtime ASR"] -- "dedupe interim/final segments" --> Transcript["Clean final transcript"]
  AppImage["Linux AppImage bundle"] -- "auto-install plugin on startup" --> Local["~/.local/fcitx5 plugin files"]
  Coordinator["Linux coordinator"] -- "send auxDown status" --> Fcitx["fcitx5 plugin"]
  Fcitx -- "hotkey events and focus sync" --> Coordinator
Loading

File Walkthrough

Relevant files
Bug fix
1 files
bailian.rs
Deduplicate final ASR segments by sentence                             
+19/-5   
Enhancement
3 files
coordinator.rs
Route Linux status and hotkey syncing                                       
+98/-8   
linux_fcitx.rs
Add auxDown API and restart resync                                             
+204/-1 
openless.cpp
Add status text and focus handling                                             
+83/-8   
Documentation
1 files
dictation.rs
Remove redundant Linux polish status note                               
+3/-0     
Configuration changes
2 files
lib.rs
Install fcitx5 plugin during startup                                         
+5/-0     
release-tauri.yml
Bundle Linux plugin resource in AppImage                                 
+4/-3     

- fcitx5 插件新增 SetAuxDown/ClearAuxDown DBus 接口,在候选词列表下方
  显示听写状态文字(收音中→识别中→润色中→已插入),切窗口自动跟随焦点
- 优先使用当前焦点 IC 展示 auxDown,失焦 IC 降级兜底,避免面板不渲染
- flushUI 排空旧事件后再设 auxDown,防止按键事件竞态覆盖状态文字
- start_dictation_signal_listener 启动时等待 fcitx5 最多 30s,监听
  NameOwnerChanged 在 fcitx5 重启后自动重新同步快捷键绑定
- AppImage 打包 libopenless.so 为资源,启动时检测缺失自动安装到
  ~/.local/lib/fcitx5/ 和 ~/.local/share/fcitx5/addon/
- Linux 胶囊窗口不在 Wayland/X11 显示,状态完全走 fcitx5 输入面板

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

github-actions Bot commented May 22, 2026

PR Reviewer Guide 🔍

(Review updated until commit 1b0e41c)

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

Plugin Overwrite

Startup now always copies the bundled plugin into ~/.local/lib/fcitx5/ and rewrites the addon config whenever the resource exists. That means an existing user-installed copy in ~/.local gets replaced on every launch, which can downgrade or clobber a newer/local build instead of only installing when the plugin is missing.

if let Err(e) = std::fs::create_dir_all(&lib_dir) {
    log::warn!("[fcitx-install] Failed to create {:?}: {e}", lib_dir);
    return;
}
if let Err(e) = std::fs::create_dir_all(&addon_dir) {
    log::warn!("[fcitx-install] Failed to create {:?}: {e}", addon_dir);
    return;
}

let so_dest = lib_dir.join("libopenless.so");
if let Err(e) = std::fs::copy(&so_src, &so_dest) {
    log::warn!("[fcitx-install] Failed to copy plugin .so: {e}");
    return;
}
log::info!("[fcitx-install] Installed plugin .so to {:?}", so_dest);

let config_content = format!(
    concat!(
        "[Addon]\n",
        "Name=OpenLess\n",
        "Name[zh_CN]=OpenLess 听写辅助\n",
        "Comment=OpenLess dictation commit helper\n",
        "Comment[zh_CN]=供 OpenLess 听写提交文字的 DBus 接口及快捷键监听\n",
        "Category=Module\n",
        "Type=SharedLibrary\n",
        "Library={}\n",
        "Version=1.0.0\n",
        "OnDemand=False\n",
        "Configurable=False\n",
        "\n",
        "[Addon/Dependencies]\n",
        "0=core\n",
        "1=dbus\n",
    ),
    so_dest.display()
);

let conf_dest = addon_dir.join("openless.conf");
if let Err(e) = std::fs::write(&conf_dest, &config_content) {
Aux Race

Each capsule state change spawns a detached thread to call set_aux_down or clear_aux_down, but there is no ordering/cancellation between those calls. On quick state transitions, an older send can arrive after a later clear or update and reintroduce stale text in fcitx5, for example showing 🎤 收音中... after dictation has already ended.

    // 把 DBus I/O 移到独立线程:emit_capsule 会被音频回调线程
    // (cpal) 调用,同步阻塞可能导致录音卡顿或可闻杂音。
    let text = t.to_string();
    std::thread::spawn(move || {
        if let Err(e) = crate::linux_fcitx::set_aux_down(&text) {
            log::warn!("[capsule] set_aux_down failed: {e}");
        }
    });
    // 首次设置(从 None 转为有值)时,fcitx5 可能还在处理触发
    // 快捷键的按键事件(press/release),这些事件可能覆盖 auxDown。
    // 延迟 300ms 重设一次确保状态不被竞态覆盖。
    // 重设前检查 LAST_AUX:如果状态已经变了则跳过,避免旧文字覆盖新状态。
    if was_none {
        let text = t.to_string();
        std::thread::spawn(move || {
            std::thread::sleep(std::time::Duration::from_millis(300));
            let current = LAST_AUX.lock().unwrap().clone();
            if current.as_deref() != Some(&text) {
                log::info!("[capsule] set_aux_down retry skipped: state changed to {current:?}");
                return;
            }
            log::info!("[capsule] set_aux_down retry: {text}");
            if let Err(e) = crate::linux_fcitx::set_aux_down(&text) {
                log::warn!("[capsule] set_aux_down retry failed: {e}");
            }
        });
    }
}
None => {
    log::info!("[capsule] clear_aux_down");
    // 同样从音频线程挪走,避免阻塞。
    std::thread::spawn(|| {
        if let Err(e) = crate::linux_fcitx::clear_aux_down() {
            log::warn!("[capsule] clear_aux_down failed: {e}");
        }
    });

aeoform and others added 2 commits May 22, 2026 20:48
Linux 已从 NoActivate 列表移除(走 fcitx5 auxDown 状态提示),测试断言
需同步更新为 FallbackShow。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
两个根因:
1. end_time 判断用 is_some() 而非 > 0:API 对 interim 结果设 end_time:0,
   导致所有中间结果被当成 final 推入 segments
2. 累积文本拼接:同一句 API 多次发送("你"→"你好"→"你好吗"),
   每次作为新 segment push,join 后变成重复拼接

修复:
- end_time 改用 > 0 判断真正的句子结束
- 引入 sentence_id → BTreeMap 按序存储,同一 sentence_id 后到覆盖前到

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

Persistent review updated to latest commit 7635df5

1. clearAuxDown 找不到 IC 时也会清掉 lastAuxText_,避免 FocusIn 时
   重放旧状态(如"已插入"→切窗口→突然显示"已插入")
2. 300ms 延迟重试前检查 LAST_AUX,状态已变则跳过,避免旧文字覆盖

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

Persistent review updated to latest commit 7403597

1. set_aux_down/clear_aux_down 从 emit_capsule(被 cpal 音频回调调用)
   移到独立线程执行,避免同步 DBus I/O 阻塞录音导致卡顿
2. is_plugin_installed_on_disk 同时检查 .so 和 .conf,孤立的 .so
   没有 addon 配置 fcitx5 不会加载

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

Persistent review updated to latest commit c4cb3c9

1. start_dictation_signal_listener 新增 custom_trigger_key 参数,
   NameOwnerChanged 和初始同步都通过 resync_main_binding 分支处理
   自定义组合键 vs 预设修饰键
2. ensure_plugin_installed 去掉"已安装即跳过"检查,AppImage 每次启动
   都覆盖最新 .so + .conf,确保新 DBus 方法不会因旧插件丢失

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aeoform aeoform force-pushed the feat/linux-fcitx5-status-popup branch from b783115 to 163327e Compare May 22, 2026 13:21
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 163327e

1 similar comment
@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 163327e

aeoform and others added 2 commits May 22, 2026 21:29
…ux")]

函数体内引用了 crate::linux_fcitx,非 Linux 平台该模块不存在导致 E0433。

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

Persistent review updated to latest commit 77455cf

@github-actions
Copy link
Copy Markdown

Persistent review updated to latest commit 1b0e41c

@aeoform
Copy link
Copy Markdown
Contributor Author

aeoform commented May 22, 2026

在测试里我没发现相关问题

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