From 19ab7429cda6f47f40c4a3e4f67bddf193810e6c Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Thu, 28 May 2026 23:08:00 +0800 Subject: [PATCH 1/7] =?UTF-8?q?chore(linux):=20fcitx5=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=89=93=E5=8C=85=E6=B3=A8=E5=85=A5=20+=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=20rdev=20=E6=AE=8B=E7=95=99=20+=20=E5=A3=B0=E6=98=8E=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fcitx5 插件改为打包时注入系统路径(inject-fcitx5-plugin.sh),不再运行时复制 - ensure_plugin_installed 改为纯版本检查,不做文件 I/O - 清理 hotkey/permissions/unicode_keystroke 中 rdev 相关注释和日志 - tauri.linux.conf.json 声明 deb/rpm 运行时依赖及 fcitx5 推荐 - prebuild 脚本自动复制插件文件供 Tauri resources 打包 Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/app/.gitignore | 2 + openless-all/app/package.json | 1 + openless-all/app/src-tauri/src/hotkey.rs | 15 ++- openless-all/app/src-tauri/src/linux_fcitx.rs | 98 +++++++------------ openless-all/app/src-tauri/src/permissions.rs | 4 +- .../app/src-tauri/src/unicode_keystroke.rs | 3 +- .../app/src-tauri/tauri.linux.conf.json | 32 ++++++ openless-all/scripts/inject-fcitx5-plugin.sh | 87 ++++++++++++++++ 8 files changed, 165 insertions(+), 77 deletions(-) create mode 100755 openless-all/scripts/inject-fcitx5-plugin.sh diff --git a/openless-all/app/.gitignore b/openless-all/app/.gitignore index 625e21f7..8371a704 100644 --- a/openless-all/app/.gitignore +++ b/openless-all/app/.gitignore @@ -9,3 +9,5 @@ dist/ # Tauri src-tauri/target/ src-tauri/gen/ +src-tauri/linux-fcitx5-plugin/libopenless.so +src-tauri/linux-fcitx5-plugin/openless.conf diff --git a/openless-all/app/package.json b/openless-all/app/package.json index 5ec2d5eb..70d83121 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "prebuild": "mkdir -p src-tauri/linux-fcitx5-plugin && for f in libopenless.so openless.conf; do [ -f ../scripts/linux-fcitx5-plugin/build_release/$f ] && cp ../scripts/linux-fcitx5-plugin/build_release/$f src-tauri/linux-fcitx5-plugin/; done; true", "preview": "vite preview", "tauri": "tauri", "check:hotkey-injection": "node scripts/check-hotkey-injection.mjs" diff --git a/openless-all/app/src-tauri/src/hotkey.rs b/openless-all/app/src-tauri/src/hotkey.rs index 6075f8e4..845f15d5 100644 --- a/openless-all/app/src-tauri/src/hotkey.rs +++ b/openless-all/app/src-tauri/src/hotkey.rs @@ -1,11 +1,9 @@ //! 全局热键监听:发送按下 / 抬起 / 取消三类边沿事件。 //! //! - macOS:原生 CGEventTap(core-foundation + core-graphics FFI),与 Swift -//! `OpenLessHotkey/HotkeyMonitor.swift` 同源。**不能用 `rdev`**:rdev 在每个 -//! 事件回调里同步调 `TSMGetInputSourceProperty`,macOS 14+ 强制断言主线程, -//! 非主线程触发 `dispatch_assert_queue_fail` → SIGTRAP abort(已踩坑)。 +//! `OpenLessHotkey/HotkeyMonitor.swift` 同源。 //! - Windows:原生 `WH_KEYBOARD_LL` low-level keyboard hook,保留 modifier-only -//! trigger(如右 Control / 右 Alt)的真实语义,不再把平台能力藏在 `rdev` 抽象里。 +//! trigger(如右 Control / 右 Alt)的真实语义。 //! - Linux:fcitx5 插件提供热键事件(DBus 信号 `DictationKeyEvent`)。 //! //! 仅产出"边沿"事件,toggle vs hold 由 Coordinator 解释。 @@ -1189,19 +1187,18 @@ mod platform { use super::{HotkeyAdapter, HotkeyEvent}; use crate::types::{HotkeyAdapterKind, HotkeyBinding, HotkeyInstallError, HotkeyTrigger}; - /// Linux 统一使用 fcitx5 插件作为热键源(Wayland / X11 均可), - /// 不再启用 rdev 监听器。此处返回占位 adapter 让上层走 `Installed` 分支。 + /// Linux 统一使用 fcitx5 插件作为热键源(Wayland / X11 均可)。 /// /// 实际的热键事件由 `linux_fcitx::start_dictation_signal_listener` 接收 /// fcitx5 插件的 DBus 信号并转发到 `Sender`。 pub fn start_adapter( _binding: HotkeyBinding, - tx: Sender, + _tx: Sender, ) -> Result, HotkeyInstallError> { log::info!( - "[hotkey] Linux — fcitx5 plugin handles hotkeys; rdev listener skipped" + "[hotkey] Linux — fcitx5 plugin handles hotkeys" ); - Ok(Box::new(PlaceholderAdapter { _tx: tx })) + Ok(Box::new(PlaceholderAdapter { _tx })) } /// Linux 占位 adapter:实现接口但不监听键盘。 diff --git a/openless-all/app/src-tauri/src/linux_fcitx.rs b/openless-all/app/src-tauri/src/linux_fcitx.rs index d25c6b61..90ce7d9c 100644 --- a/openless-all/app/src-tauri/src/linux_fcitx.rs +++ b/openless-all/app/src-tauri/src/linux_fcitx.rs @@ -404,12 +404,11 @@ pub fn start_dictation_signal_listener( .ok(); } -/// AppImage / 便携版:每次启动时从 bundled resources 复制插件到 -/// `~/.local/lib/fcitx5/` 和 `~/.local/share/fcitx5/addon/`,始终覆盖已有文件。 +/// 检查 fcitx5 插件是否已安装到系统路径,与 bundled 版本比对。 /// -/// 这确保 AppImage 版本与插件版本一致——插件新增 DBus 方法时旧 .so 不会缺少符号。 -/// 系统路径(deb/rpm 安装)不会被覆盖。 -/// 安装后需要用户重启 fcitx5(`fcitx5 -r`)才能加载新插件。 +/// 所有 Linux 格式(deb/rpm/AppImage)的插件安装都在打包时完成 +///(`scripts/inject-fcitx5-plugin.sh`),此处仅做版本检查。 +/// 不做任何文件 I/O 写入——版本不匹配或未安装时仅输出警告。 #[cfg(target_os = "linux")] pub fn ensure_plugin_installed(app: &tauri::AppHandle) { use tauri::Manager; @@ -417,75 +416,46 @@ pub fn ensure_plugin_installed(app: &tauri::AppHandle) { let resource_dir = match app.path().resource_dir() { Ok(d) => d, Err(e) => { - log::warn!("[fcitx-install] Cannot resolve resource dir: {e}"); + log::warn!("[fcitx] Cannot resolve resource dir: {e}"); return; } }; - let so_src = resource_dir.join("linux-fcitx5-plugin").join("libopenless.so"); - if !so_src.exists() { - log::info!( - "[fcitx-install] Bundled plugin not found at {:?} — not an AppImage or plugin not bundled", - so_src - ); + let bundled_so = resource_dir.join("linux-fcitx5-plugin").join("libopenless.so"); + if !bundled_so.exists() { + // 非打包环境(dev build),跳过检查 return; } - let Ok(home) = std::env::var("HOME") else { - log::warn!("[fcitx-install] Cannot determine HOME dir"); - return; + let bundled_size = match bundled_so.metadata() { + Ok(m) => m.len(), + Err(_) => return, }; - let home = std::path::PathBuf::from(home); - - let lib_dir = home.join(".local").join("lib").join("fcitx5"); - let addon_dir = home.join(".local").join("share").join("fcitx5").join("addon"); - - 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) { - log::warn!("[fcitx-install] Failed to write addon config: {e}"); - return; + // fcitx5 标准系统路径(打包时由 inject-fcitx5-plugin.sh 注入) + let system_so = std::path::Path::new("/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so"); + let system_conf = std::path::Path::new("/usr/share/fcitx5/addon/openless.conf"); + + match (system_so.exists(), system_conf.exists()) { + (true, true) => { + if let Ok(m) = system_so.metadata() { + if m.len() != bundled_size { + log::warn!( + "[fcitx] Plugin version mismatch: system={}, bundled={}. Reinstall OpenLess to update.", + m.len(), + bundled_size + ); + } + } + } + (false, _) | (_, false) => { + log::warn!( + "[fcitx] fcitx5 plugin not installed at system paths ({:?}, {:?}). \ + The OpenLess package may be incomplete.", + system_so, system_conf + ); + } } - log::info!("[fcitx-install] Installed addon config to {:?}", conf_dest); - log::info!( - "[fcitx-install] Done. Run `fcitx5 -r` to load the plugin, then restart OpenLess." - ); } /// 同步主听写热键:自定义组合键走 SetCustomDictationTrigger,预设修饰键走 SetHotkeyRaw。 diff --git a/openless-all/app/src-tauri/src/permissions.rs b/openless-all/app/src-tauri/src/permissions.rs index d85702c0..04af3df4 100644 --- a/openless-all/app/src-tauri/src/permissions.rs +++ b/openless-all/app/src-tauri/src/permissions.rs @@ -6,7 +6,7 @@ //! - macOS Accessibility:`AXIsProcessTrusted` 检查; //! `AXIsProcessTrustedWithOptions({kAXTrustedCheckOptionPrompt: true})` 弹系统授权框。 //! - macOS Microphone:`AVAudioApplication.shared.recordPermission` + requestRecordPermission。 -//! - Windows:rdev / cpal 不需要 Accessibility 等价权限;麦克风首次使用时 Win10+ 弹一次系统提示。 +//! - Windows:cpal 不需要 Accessibility 等价权限;麦克风首次使用时 Win10+ 弹一次系统提示。 use serde::Serialize; @@ -256,7 +256,7 @@ mod platform { #[cfg(target_os = "windows")] use winreg::RegKey; - /// Windows / Linux 不存在 macOS 那种 Accessibility 概念;rdev 直接监听键盘。 + /// Windows / Linux 不存在 macOS 那种 Accessibility 概念。 pub fn check_accessibility() -> PermissionStatus { PermissionStatus::NotApplicable } diff --git a/openless-all/app/src-tauri/src/unicode_keystroke.rs b/openless-all/app/src-tauri/src/unicode_keystroke.rs index bed9d9d1..849d3f92 100644 --- a/openless-all/app/src-tauri/src/unicode_keystroke.rs +++ b/openless-all/app/src-tauri/src/unicode_keystroke.rs @@ -31,8 +31,7 @@ //! - `type_unicode_chunk`(CGEventPost)任意线程可调,对齐 `insertion.rs::macos:: //! simulate_paste` 现状。 //! - TIS(`switch_to_ascii` / `restore_input_source`)调度到主线程,规避 macOS 14+ -//! 对 TSM/TIS 主线程的 `dispatch_assert_queue_fail` SIGTRAP(与 -//! `feedback_rdev_macos_trap.md` 同款风险类别)。 +//! 对 TSM/TIS 主线程的 `dispatch_assert_queue_fail` SIGTRAP。 #[allow(unused_imports)] use tauri::{AppHandle, Runtime}; diff --git a/openless-all/app/src-tauri/tauri.linux.conf.json b/openless-all/app/src-tauri/tauri.linux.conf.json index c77074e2..39892ef4 100644 --- a/openless-all/app/src-tauri/tauri.linux.conf.json +++ b/openless-all/app/src-tauri/tauri.linux.conf.json @@ -51,5 +51,37 @@ "acceptFirstMouse": true } ] + }, + "bundle": { + "resources": [ + "linux-fcitx5-plugin/libopenless.so", + "linux-fcitx5-plugin/openless.conf" + ], + "linux": { + "deb": { + "depends": [ + "libasound2", + "libdbus-1-3", + "libssl3", + "libjavascriptcoregtk-4.1-0", + "libsoup-3.0-0" + ], + "recommends": [ + "fcitx5" + ] + }, + "rpm": { + "depends": [ + "alsa-lib", + "dbus-libs", + "openssl-libs", + "webkit2gtk4.1", + "libsoup3" + ], + "recommends": [ + "fcitx5" + ] + } + } } } diff --git a/openless-all/scripts/inject-fcitx5-plugin.sh b/openless-all/scripts/inject-fcitx5-plugin.sh new file mode 100755 index 00000000..c4b53d09 --- /dev/null +++ b/openless-all/scripts/inject-fcitx5-plugin.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Inject fcitx5 plugin files into Linux packages at system paths. +# Usage: ./inject-fcitx5-plugin.sh +# +# Supports: .deb, .rpm, AppImage (AppDir) +set -euo pipefail + +PKG="$1" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PLUGIN_DIR="$SCRIPT_DIR/linux-fcitx5-plugin/build_release" +SO_SRC="$PLUGIN_DIR/libopenless.so" +CONF_SRC="$PLUGIN_DIR/openless.conf" + +if [ ! -f "$SO_SRC" ] || [ ! -f "$CONF_SRC" ]; then + echo "[inject-fcitx5] Plugin not built — run build.sh first. Skipping." + exit 0 +fi + +TARGET_LIB="/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so" +TARGET_CONF="/usr/share/fcitx5/addon/openless.conf" + +case "$PKG" in + *.deb) + echo "[inject-fcitx5] Injecting into deb: $PKG" + TMPDIR=$(mktemp -d) + trap 'rm -rf "$TMPDIR"' EXIT + dpkg-deb -R "$PKG" "$TMPDIR" + mkdir -p "$TMPDIR/usr/lib/x86_64-linux-gnu/fcitx5" + mkdir -p "$TMPDIR/usr/share/fcitx5/addon" + cp "$SO_SRC" "$TMPDIR/$TARGET_LIB" + cp "$CONF_SRC" "$TMPDIR/$TARGET_CONF" + dpkg-deb -b "$TMPDIR" "$PKG" + echo "[inject-fcitx5] Done — deb updated" + ;; + *.rpm) + echo "[inject-fcitx5] Injecting into rpm: $PKG" + TMPDIR=$(mktemp -d) + trap 'rm -rf "$TMPDIR"' EXIT + cd "$TMPDIR" + rpm2cpio "$PKG" | cpio -idm 2>/dev/null || true + mkdir -p ".$TARGET_LIB" ".$TARGET_CONF" 2>/dev/null || true + rmdir ".$TARGET_LIB" ".$TARGET_CONF" 2>/dev/null || true + mkdir -p "$(dirname ".$TARGET_LIB")" + mkdir -p "$(dirname ".$TARGET_CONF")" + cp "$SO_SRC" ".$TARGET_LIB" + cp "$CONF_SRC" ".$TARGET_CONF" + # rpmrebuild is the safest way to repack + if command -v rpmrebuild &>/dev/null; then + rpmrebuild -np -d "$TMPDIR" "$PKG" 2>/dev/null || { + echo "[inject-fcitx5] rpmrebuild failed — rpm injection not available, skipping" + exit 0 + } + else + echo "[inject-fcitx5] rpmrebuild not found — install it for rpm injection support. Skipping." + exit 0 + fi + echo "[inject-fcitx5] Done — rpm updated" + ;; + *.AppImage|*/AppDir|*/appdir) + # AppImage / AppDir: add files to the AppDir before packaging + echo "[inject-fcitx5] Injecting into AppDir: $PKG" + if [ -d "$PKG" ]; then + APPDIR="$PKG" + else + # Extract AppImage to get AppDir + TMPDIR=$(mktemp -d) + trap 'rm -rf "$TMPDIR"' EXIT + cd "$TMPDIR" + "$PKG" --appimage-extract 2>/dev/null || { + echo "[inject-fcitx5] Cannot extract AppImage — skipping" + exit 0 + } + APPDIR="$TMPDIR/squashfs-root" + fi + mkdir -p "$APPDIR/$TARGET_LIB" 2>/dev/null || true + rmdir "$APPDIR/$TARGET_LIB" 2>/dev/null || true + mkdir -p "$APPDIR/$(dirname "$TARGET_LIB")" + mkdir -p "$APPDIR/$(dirname "$TARGET_CONF")" + cp "$SO_SRC" "$APPDIR/$TARGET_LIB" + cp "$CONF_SRC" "$APPDIR/$TARGET_CONF" + echo "[inject-fcitx5] Done — AppDir updated" + ;; + *) + echo "[inject-fcitx5] Unknown package format: $PKG — skipping" + exit 1 + ;; +esac From d426b9e7b878507cf99e901c8aeb4df00017e6b7 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Thu, 28 May 2026 23:13:44 +0800 Subject: [PATCH 2/7] fix(ci): replace bash prebuild with cross-platform Node.js script The prebuild script used bash syntax (mkdir, for, [ -f ]) which fails on Windows. Replace with a Node.js script that checks platform() and skips non-Linux, keeping the plugin copy logic only where it's needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/app/package.json | 2 +- openless-all/app/scripts/copy-plugin.mjs | 27 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 openless-all/app/scripts/copy-plugin.mjs diff --git a/openless-all/app/package.json b/openless-all/app/package.json index 70d83121..c08eacba 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "prebuild": "mkdir -p src-tauri/linux-fcitx5-plugin && for f in libopenless.so openless.conf; do [ -f ../scripts/linux-fcitx5-plugin/build_release/$f ] && cp ../scripts/linux-fcitx5-plugin/build_release/$f src-tauri/linux-fcitx5-plugin/; done; true", + "prebuild": "node scripts/copy-plugin.mjs", "preview": "vite preview", "tauri": "tauri", "check:hotkey-injection": "node scripts/check-hotkey-injection.mjs" diff --git a/openless-all/app/scripts/copy-plugin.mjs b/openless-all/app/scripts/copy-plugin.mjs new file mode 100644 index 00000000..ca1dd612 --- /dev/null +++ b/openless-all/app/scripts/copy-plugin.mjs @@ -0,0 +1,27 @@ +import { copyFileSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { platform } from 'os'; + +// Only relevant on Linux — skip silently on other platforms +if (platform() !== 'linux') { + process.exit(0); +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const srcDir = join(__dirname, '..', 'scripts', 'linux-fcitx5-plugin', 'build_release'); +const destDir = join(__dirname, 'src-tauri', 'linux-fcitx5-plugin'); + +const files = ['libopenless.so', 'openless.conf']; + +for (const f of files) { + const src = join(srcDir, f); + const dest = join(destDir, f); + try { + mkdirSync(destDir, { recursive: true }); + copyFileSync(src, dest); + console.log(`[copy:linux-plugin] copied ${f}`); + } catch { + console.log(`[copy:linux-plugin] ${f} not found — skipping`); + } +} From 2bd733ecb3403ef8a9f562a3e514826e608f28eb Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Thu, 28 May 2026 23:16:53 +0800 Subject: [PATCH 3/7] fix(ci): remove Tauri resources for plugin files, simplify to path check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove `resources` from tauri.linux.conf.json — the .so/.conf files don't exist on CI (plugin is built separately), causing cargo check to fail with "resource path doesn't exist" - Simplify ensure_plugin_installed to just check system paths exist, no bundled comparison needed - Remove prebuild script and copy-plugin.mjs (no longer needed) - Remove plugin entries from .gitignore Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/app/.gitignore | 2 - openless-all/app/package.json | 1 - openless-all/app/scripts/copy-plugin.mjs | 27 --------- openless-all/app/src-tauri/src/linux_fcitx.rs | 55 ++++--------------- .../app/src-tauri/tauri.linux.conf.json | 4 -- 5 files changed, 10 insertions(+), 79 deletions(-) delete mode 100644 openless-all/app/scripts/copy-plugin.mjs diff --git a/openless-all/app/.gitignore b/openless-all/app/.gitignore index 8371a704..625e21f7 100644 --- a/openless-all/app/.gitignore +++ b/openless-all/app/.gitignore @@ -9,5 +9,3 @@ dist/ # Tauri src-tauri/target/ src-tauri/gen/ -src-tauri/linux-fcitx5-plugin/libopenless.so -src-tauri/linux-fcitx5-plugin/openless.conf diff --git a/openless-all/app/package.json b/openless-all/app/package.json index c08eacba..5ec2d5eb 100644 --- a/openless-all/app/package.json +++ b/openless-all/app/package.json @@ -6,7 +6,6 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "prebuild": "node scripts/copy-plugin.mjs", "preview": "vite preview", "tauri": "tauri", "check:hotkey-injection": "node scripts/check-hotkey-injection.mjs" diff --git a/openless-all/app/scripts/copy-plugin.mjs b/openless-all/app/scripts/copy-plugin.mjs deleted file mode 100644 index ca1dd612..00000000 --- a/openless-all/app/scripts/copy-plugin.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import { copyFileSync, mkdirSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { platform } from 'os'; - -// Only relevant on Linux — skip silently on other platforms -if (platform() !== 'linux') { - process.exit(0); -} - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const srcDir = join(__dirname, '..', 'scripts', 'linux-fcitx5-plugin', 'build_release'); -const destDir = join(__dirname, 'src-tauri', 'linux-fcitx5-plugin'); - -const files = ['libopenless.so', 'openless.conf']; - -for (const f of files) { - const src = join(srcDir, f); - const dest = join(destDir, f); - try { - mkdirSync(destDir, { recursive: true }); - copyFileSync(src, dest); - console.log(`[copy:linux-plugin] copied ${f}`); - } catch { - console.log(`[copy:linux-plugin] ${f} not found — skipping`); - } -} diff --git a/openless-all/app/src-tauri/src/linux_fcitx.rs b/openless-all/app/src-tauri/src/linux_fcitx.rs index 90ce7d9c..9bd62e94 100644 --- a/openless-all/app/src-tauri/src/linux_fcitx.rs +++ b/openless-all/app/src-tauri/src/linux_fcitx.rs @@ -404,57 +404,22 @@ pub fn start_dictation_signal_listener( .ok(); } -/// 检查 fcitx5 插件是否已安装到系统路径,与 bundled 版本比对。 +/// 检查 fcitx5 插件是否已安装到系统路径。 /// /// 所有 Linux 格式(deb/rpm/AppImage)的插件安装都在打包时完成 -///(`scripts/inject-fcitx5-plugin.sh`),此处仅做版本检查。 -/// 不做任何文件 I/O 写入——版本不匹配或未安装时仅输出警告。 +///(`scripts/inject-fcitx5-plugin.sh`),此处仅确认文件存在。 +/// 未安装时输出警告,不做任何文件 I/O。 #[cfg(target_os = "linux")] -pub fn ensure_plugin_installed(app: &tauri::AppHandle) { - use tauri::Manager; - - let resource_dir = match app.path().resource_dir() { - Ok(d) => d, - Err(e) => { - log::warn!("[fcitx] Cannot resolve resource dir: {e}"); - return; - } - }; - - let bundled_so = resource_dir.join("linux-fcitx5-plugin").join("libopenless.so"); - if !bundled_so.exists() { - // 非打包环境(dev build),跳过检查 - return; - } - - let bundled_size = match bundled_so.metadata() { - Ok(m) => m.len(), - Err(_) => return, - }; - - // fcitx5 标准系统路径(打包时由 inject-fcitx5-plugin.sh 注入) +pub fn ensure_plugin_installed(_app: &tauri::AppHandle) { let system_so = std::path::Path::new("/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so"); let system_conf = std::path::Path::new("/usr/share/fcitx5/addon/openless.conf"); - match (system_so.exists(), system_conf.exists()) { - (true, true) => { - if let Ok(m) = system_so.metadata() { - if m.len() != bundled_size { - log::warn!( - "[fcitx] Plugin version mismatch: system={}, bundled={}. Reinstall OpenLess to update.", - m.len(), - bundled_size - ); - } - } - } - (false, _) | (_, false) => { - log::warn!( - "[fcitx] fcitx5 plugin not installed at system paths ({:?}, {:?}). \ - The OpenLess package may be incomplete.", - system_so, system_conf - ); - } + if !system_so.exists() || !system_conf.exists() { + log::warn!( + "[fcitx] fcitx5 plugin not installed at system paths ({:?}, {:?}). \ + The OpenLess package may be incomplete.", + system_so, system_conf + ); } } diff --git a/openless-all/app/src-tauri/tauri.linux.conf.json b/openless-all/app/src-tauri/tauri.linux.conf.json index 39892ef4..36b93619 100644 --- a/openless-all/app/src-tauri/tauri.linux.conf.json +++ b/openless-all/app/src-tauri/tauri.linux.conf.json @@ -53,10 +53,6 @@ ] }, "bundle": { - "resources": [ - "linux-fcitx5-plugin/libopenless.so", - "linux-fcitx5-plugin/openless.conf" - ], "linux": { "deb": { "depends": [ From 0e5f90bcf27371b93c1fceaba9d2a2e631853026 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Thu, 28 May 2026 23:32:10 +0800 Subject: [PATCH 4/7] fix(inject): use correct RPM lib path, remove broken AppImage file mode - RPM: use /usr/lib64/fcitx5/ (RPM 64-bit standard) instead of Debian multiarch path - AppImage: only support AppDir directory input, remove .AppImage file extraction which couldn't repack and would lose changes Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/scripts/inject-fcitx5-plugin.sh | 43 ++++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/openless-all/scripts/inject-fcitx5-plugin.sh b/openless-all/scripts/inject-fcitx5-plugin.sh index c4b53d09..1c00087e 100755 --- a/openless-all/scripts/inject-fcitx5-plugin.sh +++ b/openless-all/scripts/inject-fcitx5-plugin.sh @@ -16,35 +16,33 @@ if [ ! -f "$SO_SRC" ] || [ ! -f "$CONF_SRC" ]; then exit 0 fi -TARGET_LIB="/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so" TARGET_CONF="/usr/share/fcitx5/addon/openless.conf" case "$PKG" in *.deb) + TARGET_LIB="/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so" echo "[inject-fcitx5] Injecting into deb: $PKG" TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT dpkg-deb -R "$PKG" "$TMPDIR" - mkdir -p "$TMPDIR/usr/lib/x86_64-linux-gnu/fcitx5" - mkdir -p "$TMPDIR/usr/share/fcitx5/addon" + mkdir -p "$TMPDIR/$(dirname "$TARGET_LIB")" + mkdir -p "$TMPDIR/$(dirname "$TARGET_CONF")" cp "$SO_SRC" "$TMPDIR/$TARGET_LIB" cp "$CONF_SRC" "$TMPDIR/$TARGET_CONF" dpkg-deb -b "$TMPDIR" "$PKG" echo "[inject-fcitx5] Done — deb updated" ;; *.rpm) + TARGET_LIB="/usr/lib64/fcitx5/libopenless.so" echo "[inject-fcitx5] Injecting into rpm: $PKG" TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT cd "$TMPDIR" rpm2cpio "$PKG" | cpio -idm 2>/dev/null || true - mkdir -p ".$TARGET_LIB" ".$TARGET_CONF" 2>/dev/null || true - rmdir ".$TARGET_LIB" ".$TARGET_CONF" 2>/dev/null || true mkdir -p "$(dirname ".$TARGET_LIB")" mkdir -p "$(dirname ".$TARGET_CONF")" cp "$SO_SRC" ".$TARGET_LIB" cp "$CONF_SRC" ".$TARGET_CONF" - # rpmrebuild is the safest way to repack if command -v rpmrebuild &>/dev/null; then rpmrebuild -np -d "$TMPDIR" "$PKG" 2>/dev/null || { echo "[inject-fcitx5] rpmrebuild failed — rpm injection not available, skipping" @@ -56,28 +54,19 @@ case "$PKG" in fi echo "[inject-fcitx5] Done — rpm updated" ;; - *.AppImage|*/AppDir|*/appdir) - # AppImage / AppDir: add files to the AppDir before packaging - echo "[inject-fcitx5] Injecting into AppDir: $PKG" - if [ -d "$PKG" ]; then - APPDIR="$PKG" - else - # Extract AppImage to get AppDir - TMPDIR=$(mktemp -d) - trap 'rm -rf "$TMPDIR"' EXIT - cd "$TMPDIR" - "$PKG" --appimage-extract 2>/dev/null || { - echo "[inject-fcitx5] Cannot extract AppImage — skipping" - exit 0 - } - APPDIR="$TMPDIR/squashfs-root" + */AppDir|*/appdir|*.AppDir) + TARGET_LIB="/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so" + # Inject into AppDir before it's packaged into AppImage. + # Must be a directory, not an existing .AppImage file. + if [ ! -d "$PKG" ]; then + echo "[inject-fcitx5] AppImage injection only supports AppDir (directory), not a packaged .AppImage file. Skipping." + exit 0 fi - mkdir -p "$APPDIR/$TARGET_LIB" 2>/dev/null || true - rmdir "$APPDIR/$TARGET_LIB" 2>/dev/null || true - mkdir -p "$APPDIR/$(dirname "$TARGET_LIB")" - mkdir -p "$APPDIR/$(dirname "$TARGET_CONF")" - cp "$SO_SRC" "$APPDIR/$TARGET_LIB" - cp "$CONF_SRC" "$APPDIR/$TARGET_CONF" + echo "[inject-fcitx5] Injecting into AppDir: $PKG" + mkdir -p "$PKG/$(dirname "$TARGET_LIB")" + mkdir -p "$PKG/$(dirname "$TARGET_CONF")" + cp "$SO_SRC" "$PKG/$TARGET_LIB" + cp "$CONF_SRC" "$PKG/$TARGET_CONF" echo "[inject-fcitx5] Done — AppDir updated" ;; *) From 71a36344b2afd1f909945e29a1dfa27d0b3e7693 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Thu, 28 May 2026 23:38:08 +0800 Subject: [PATCH 5/7] fix(fcitx): probe multiple distro lib paths for plugin check Debian uses /usr/lib/x86_64-linux-gnu/fcitx5, RPM uses /usr/lib64/fcitx5, and some distros use /usr/lib/fcitx5. Check all three to avoid false alarms on non-Debian systems. Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/app/src-tauri/src/linux_fcitx.rs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src-tauri/src/linux_fcitx.rs b/openless-all/app/src-tauri/src/linux_fcitx.rs index 9bd62e94..92164a5f 100644 --- a/openless-all/app/src-tauri/src/linux_fcitx.rs +++ b/openless-all/app/src-tauri/src/linux_fcitx.rs @@ -411,14 +411,32 @@ pub fn start_dictation_signal_listener( /// 未安装时输出警告,不做任何文件 I/O。 #[cfg(target_os = "linux")] pub fn ensure_plugin_installed(_app: &tauri::AppHandle) { - let system_so = std::path::Path::new("/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so"); + // fcitx5 在不同发行版的 lib 路径不同 + let lib_dirs = [ + "/usr/lib/x86_64-linux-gnu/fcitx5", // Debian multiarch + "/usr/lib64/fcitx5", // RPM 64-bit + "/usr/lib/fcitx5", // 通用回退 + ]; let system_conf = std::path::Path::new("/usr/share/fcitx5/addon/openless.conf"); - if !system_so.exists() || !system_conf.exists() { + if !system_conf.exists() { log::warn!( - "[fcitx] fcitx5 plugin not installed at system paths ({:?}, {:?}). \ + "[fcitx] fcitx5 addon config not installed at {:?}. \ The OpenLess package may be incomplete.", - system_so, system_conf + system_conf + ); + return; + } + + let found = lib_dirs.iter().any(|dir| { + std::path::Path::new(dir).join("libopenless.so").exists() + }); + + if !found { + log::warn!( + "[fcitx] fcitx5 plugin .so not found in any of {:?}. \ + The OpenLess package may be incomplete.", + lib_dirs ); } } From f5bf1ff1536f9db03390fad30cacfe2e1cab01a7 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Thu, 28 May 2026 23:53:53 +0800 Subject: [PATCH 6/7] fix(inject): remove AppImage support, detect Debian multiarch dynamically - AppImage cannot work for fcitx5 plugin injection: fcitx5 runs on the host system and loads addons from host paths (/usr/lib/.../fcitx5). Files placed inside the AppImage mount are invisible to fcitx5. - For Debian, use dpkg-architecture to detect the multiarch triplet (x86_64-linux-gnu, aarch64-linux-gnu, etc.) instead of hardcoding. Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/scripts/inject-fcitx5-plugin.sh | 31 ++++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/openless-all/scripts/inject-fcitx5-plugin.sh b/openless-all/scripts/inject-fcitx5-plugin.sh index 1c00087e..2e971cf8 100755 --- a/openless-all/scripts/inject-fcitx5-plugin.sh +++ b/openless-all/scripts/inject-fcitx5-plugin.sh @@ -2,7 +2,9 @@ # Inject fcitx5 plugin files into Linux packages at system paths. # Usage: ./inject-fcitx5-plugin.sh # -# Supports: .deb, .rpm, AppImage (AppDir) +# Supports: .deb, .rpm +# AppImage is NOT supported — fcitx5 runs on the host and cannot load +# addons from inside the AppImage mount. set -euo pipefail PKG="$1" @@ -20,8 +22,14 @@ TARGET_CONF="/usr/share/fcitx5/addon/openless.conf" case "$PKG" in *.deb) - TARGET_LIB="/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so" - echo "[inject-fcitx5] Injecting into deb: $PKG" + # Detect multiarch triplet for the target architecture + MULTIARCH=$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null || echo "") + if [ -n "$MULTIARCH" ]; then + TARGET_LIB="/usr/lib/$MULTIARCH/fcitx5/libopenless.so" + else + TARGET_LIB="/usr/lib/fcitx5/libopenless.so" + fi + echo "[inject-fcitx5] Injecting into deb ($MULTIARCH): $PKG" TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT dpkg-deb -R "$PKG" "$TMPDIR" @@ -54,23 +62,8 @@ case "$PKG" in fi echo "[inject-fcitx5] Done — rpm updated" ;; - */AppDir|*/appdir|*.AppDir) - TARGET_LIB="/usr/lib/x86_64-linux-gnu/fcitx5/libopenless.so" - # Inject into AppDir before it's packaged into AppImage. - # Must be a directory, not an existing .AppImage file. - if [ ! -d "$PKG" ]; then - echo "[inject-fcitx5] AppImage injection only supports AppDir (directory), not a packaged .AppImage file. Skipping." - exit 0 - fi - echo "[inject-fcitx5] Injecting into AppDir: $PKG" - mkdir -p "$PKG/$(dirname "$TARGET_LIB")" - mkdir -p "$PKG/$(dirname "$TARGET_CONF")" - cp "$SO_SRC" "$PKG/$TARGET_LIB" - cp "$CONF_SRC" "$PKG/$TARGET_CONF" - echo "[inject-fcitx5] Done — AppDir updated" - ;; *) - echo "[inject-fcitx5] Unknown package format: $PKG — skipping" + echo "[inject-fcitx5] Unknown package format: $PKG (supported: .deb, .rpm)" exit 1 ;; esac From 28ef33267cea3bdea10a2c49e3686f8496524972 Mon Sep 17 00:00:00 2001 From: aeoform <2790848120@qq.com> Date: Thu, 28 May 2026 23:59:53 +0800 Subject: [PATCH 7/7] fix(inject): fail loudly when RPM injection cannot complete exit 0 on rpmrebuild failure produced a broken RPM silently. Use exit 1 and print to stderr so the CI pipeline catches it. Co-Authored-By: Claude Opus 4.7 (1M context) --- openless-all/scripts/inject-fcitx5-plugin.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openless-all/scripts/inject-fcitx5-plugin.sh b/openless-all/scripts/inject-fcitx5-plugin.sh index 2e971cf8..2b16f5fd 100755 --- a/openless-all/scripts/inject-fcitx5-plugin.sh +++ b/openless-all/scripts/inject-fcitx5-plugin.sh @@ -53,12 +53,12 @@ case "$PKG" in cp "$CONF_SRC" ".$TARGET_CONF" if command -v rpmrebuild &>/dev/null; then rpmrebuild -np -d "$TMPDIR" "$PKG" 2>/dev/null || { - echo "[inject-fcitx5] rpmrebuild failed — rpm injection not available, skipping" - exit 0 + echo "[inject-fcitx5] ERROR: rpmrebuild failed" >&2 + exit 1 } else - echo "[inject-fcitx5] rpmrebuild not found — install it for rpm injection support. Skipping." - exit 0 + echo "[inject-fcitx5] ERROR: rpmrebuild not found — required for RPM injection. Install it with: sudo dnf install rpmrebuild" >&2 + exit 1 fi echo "[inject-fcitx5] Done — rpm updated" ;;