Merge pull request #546 from aeoform/chore/linux-fcitx5-plugin-inject-and-cleanup
chore(linux): fcitx5 插件打包注入 + 清理 rdev 残留 + 声明系统依赖
This commit is contained in:
6 files changed
+132
-86
No files matched your search
@@ -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<HotkeyEvent>`。
|
||||
pub fn start_adapter(
|
||||
_binding: HotkeyBinding,
|
||||
tx: Sender<HotkeyEvent>,
|
||||
_tx: Sender<HotkeyEvent>,
|
||||
) -> Result<Box<dyn HotkeyAdapter>, 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:实现接口但不监听键盘。
|
||||
|
||||
@@ -404,88 +404,41 @@ pub fn start_dictation_signal_listener(
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// AppImage / 便携版:每次启动时从 bundled resources 复制插件到
|
||||
/// `~/.local/lib/fcitx5/` 和 `~/.local/share/fcitx5/addon/`,始终覆盖已有文件。
|
||||
/// 检查 fcitx5 插件是否已安装到系统路径。
|
||||
///
|
||||
/// 这确保 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;
|
||||
pub fn ensure_plugin_installed(_app: &tauri::AppHandle) {
|
||||
// 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");
|
||||
|
||||
let resource_dir = match app.path().resource_dir() {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::warn!("[fcitx-install] 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
|
||||
if !system_conf.exists() {
|
||||
log::warn!(
|
||||
"[fcitx] fcitx5 addon config not installed at {:?}. \
|
||||
The OpenLess package may be incomplete.",
|
||||
system_conf
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(home) = std::env::var("HOME") else {
|
||||
log::warn!("[fcitx-install] Cannot determine HOME dir");
|
||||
return;
|
||||
};
|
||||
let home = std::path::PathBuf::from(home);
|
||||
let found = lib_dirs.iter().any(|dir| {
|
||||
std::path::Path::new(dir).join("libopenless.so").exists()
|
||||
});
|
||||
|
||||
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 !found {
|
||||
log::warn!(
|
||||
"[fcitx] fcitx5 plugin .so not found in any of {:?}. \
|
||||
The OpenLess package may be incomplete.",
|
||||
lib_dirs
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
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。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -51,5 +51,33 @@
|
||||
"acceptFirstMouse": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Inject fcitx5 plugin files into Linux packages at system paths.
|
||||
# Usage: ./inject-fcitx5-plugin.sh <package-path>
|
||||
#
|
||||
# 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"
|
||||
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_CONF="/usr/share/fcitx5/addon/openless.conf"
|
||||
|
||||
case "$PKG" in
|
||||
*.deb)
|
||||
# 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"
|
||||
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 "$(dirname ".$TARGET_LIB")"
|
||||
mkdir -p "$(dirname ".$TARGET_CONF")"
|
||||
cp "$SO_SRC" ".$TARGET_LIB"
|
||||
cp "$CONF_SRC" ".$TARGET_CONF"
|
||||
if command -v rpmrebuild &>/dev/null; then
|
||||
rpmrebuild -np -d "$TMPDIR" "$PKG" 2>/dev/null || {
|
||||
echo "[inject-fcitx5] ERROR: rpmrebuild failed" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
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"
|
||||
;;
|
||||
*)
|
||||
echo "[inject-fcitx5] Unknown package format: $PKG (supported: .deb, .rpm)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in new issue
Block a user