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..92164a5f 100644 --- a/openless-all/app/src-tauri/src/linux_fcitx.rs +++ b/openless-all/app/src-tauri/src/linux_fcitx.rs @@ -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; - - 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 +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"); + + 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 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; + if !found { + log::warn!( + "[fcitx] fcitx5 plugin .so not found in any of {:?}. \ + The OpenLess package may be incomplete.", + lib_dirs + ); } - 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..36b93619 100644 --- a/openless-all/app/src-tauri/tauri.linux.conf.json +++ b/openless-all/app/src-tauri/tauri.linux.conf.json @@ -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" + ] + } + } } } diff --git a/openless-all/scripts/inject-fcitx5-plugin.sh b/openless-all/scripts/inject-fcitx5-plugin.sh new file mode 100755 index 00000000..2b16f5fd --- /dev/null +++ b/openless-all/scripts/inject-fcitx5-plugin.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Inject fcitx5 plugin files into Linux packages at system paths. +# Usage: ./inject-fcitx5-plugin.sh +# +# 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