From 201aefca05aa597315c1d6ebb388e4b31d3e8552 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 13 May 2026 16:44:38 +0200 Subject: [PATCH] fix: bundle fcitx gtk module in appimage --- .github/workflows/release-stable.yml | 1 + .github/workflows/release.yml | 1 + docs/ABSTRACTIONS.md | 2 +- docs/ARCHITECTURE.md | 2 +- docs/GETTING-STARTED.md | 2 +- ...117-appimage-fcitx-gtk3-frontend-bundle.md | 24 +++ scripts/appimage-launcher-tools.mjs | 100 ++++++++-- scripts/appimage-launcher-tools.test.mjs | 49 +++++ src-tauri/src/linux_appimage.rs | 171 +++++++++++++++++- 9 files changed, 327 insertions(+), 25 deletions(-) create mode 100644 docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 686a1904..3336affc 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -283,6 +283,7 @@ jobs: libxdo-dev \ libssl-dev \ libayatana-appindicator3-dev \ + fcitx5-frontend-gtk3 \ librsvg2-dev \ curl \ wget \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72bbd02e..a61e643f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -339,6 +339,7 @@ jobs: libxdo-dev \ libssl-dev \ libayatana-appindicator3-dev \ + fcitx5-frontend-gtk3 \ librsvg2-dev \ curl \ wget \ diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 2081545b..c58e8a1d 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -821,7 +821,7 @@ Tolaria delegates remote auth to the user's system git setup: - `clone_git_repo` and `create_getting_started_vault` both run system git clone work in blocking Tokio tasks so clone UIs stay responsive - On macOS, system-git commands prefer the user's login-shell `git` and `PATH`, and `git_add_remote` preflights HTTPS remotes through `git credential fill` so Keychain can prompt/grant access before the first fetch or push - On Linux AppImage launches, every system-git command and MCP Node subprocess removes AppImage loader overrides such as `LD_LIBRARY_PATH`, `LD_PRELOAD`, and `GIT_EXEC_PATH` before spawning, so helpers like `git-remote-https` and system `node` bind against the host library stack instead of Tolaria's bundled WebKit/AppImage libraries -- On Linux AppImage Wayland launches, startup environment safeguards also set `GTK_IM_MODULE=fcitx` when common input-method variables already indicate fcitx and the user has not explicitly chosen a GTK IM module, allowing WebKitGTK editor input to reach fcitx5 on compositors such as niri without overriding deliberate `GTK_IM_MODULE` choices. +- On Linux AppImage launches, release packaging bundles the GTK3 fcitx immodule into the AppImage and startup environment safeguards write a cache-local `GTK_IM_MODULE_FILE` that points GTK at the mounted module whenever fcitx is configured. If the user has not explicitly chosen a GTK IM module, Tolaria also sets `GTK_IM_MODULE=fcitx`, allowing WebKitGTK editor input to reach fcitx5 on both Wayland and X11 fallback launches without relying on host GTK module cache paths. - `git_add_remote` uses the same system git path and refuses remotes whose history is unrelated or ahead of the local vault - Existing `git_pull` / `git_push` commands keep surfacing raw git errors, and clone commands fail fast when git wants interactive terminal input - No provider-specific token or username is stored in app settings diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7917e746..8876781c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -234,7 +234,7 @@ The main Tauri window also persists its last normal size and screen position in Tauri setup keeps launch-time filesystem and subprocess work off the window creation critical path. Legacy `~/Laputa` housekeeping and the initial persisted-vault MCP bridge sync run on named background threads, so large legacy vaults, stale active-vault paths, or slow process startup cannot beachball the macOS app before React mounts. React still resyncs the bridge from `useVaultSwitcher` after the persisted selection loads, and no selected vault stops the bridge. The HTML bootstrap also installs a Tauri-only one-shot watchdog: React reports readiness from an effect after the root commits, and if that readiness signal never arrives the WebView reloads once instead of leaving macOS users in an inert rendered shell. Linux uses custom React-rendered window chrome instead of the native Tauri menu bar. `setup_linux_window_chrome()` drops server-side decorations on the main window, `openNoteInNewWindow()` does the same for detached note windows, and `LinuxTitlebar`/`LinuxMenuButton` route both window controls and menu actions back through the same shared command pipeline that the desktop native menus use. The native app menu keeps macOS-only Services/Hide entries off Windows and Linux, registers the macOS Window submenu with Tauri's reserved `WINDOW_SUBMENU_ID` so NSApp can add system window-management and window-list items, registers a window-scoped menu event handler on Windows where Tauri delivers menu clicks through the main `WebviewWindow`, and cross-platform custom items such as Check for Updates emit Tolaria command IDs with visible updater feedback. -When Tolaria is launched from a Linux AppImage, `run()` also applies AppImage-only WebKitGTK startup safeguards without changing native package installs. It injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, and on Wayland sessions it re-execs once with the first architecture-matching system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The candidate order prefers Fedora-style `lib64` and Debian-style `x86_64-linux-gnu` paths before generic `/usr/lib`, and the ELF header is checked so a 64-bit Tolaria process does not retry with a 32-bit Wayland client library. If an AppImage Wayland session advertises fcitx through common input-method environment hints and the user has not already chosen `GTK_IM_MODULE`, Tolaria sets `GTK_IM_MODULE=fcitx` before WebKit starts so GTK/WebKit input contexts reach fcitx5 on compositors that do not reliably provide the Wayland input method frontend. The same AppImage path checks whether `fc-match` resolves the default emoji font to `Noto-COLRv1.ttf`; when the user has not provided `FONTCONFIG_FILE` or `FONTCONFIG_PATH`, Tolaria writes a cache-local fontconfig file that rejects only that matched font file and exports it before WebKit starts. The rendering overrides keep AppImage WebViews from blanking after accelerated compositing/DMA-BUF failures, the re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER`, the fcitx GTK fallback preserves Chinese/Japanese/Korean IME composition for affected AppImage Wayland users, and the fontconfig guard avoids known WebKit crashes in COLRv1 emoji font rendering while leaving other emoji fonts available. +When Tolaria is launched from a Linux AppImage, `run()` also applies AppImage-only WebKitGTK startup safeguards without changing native package installs. It injects `WEBKIT_DISABLE_DMABUF_RENDERER=1` and `WEBKIT_DISABLE_COMPOSITING_MODE=1` independently unless the user already set either variable, and on Wayland sessions it re-execs once with the first architecture-matching system `libwayland-client.so` in `LD_PRELOAD` when the user has not provided their own preload. The candidate order prefers Fedora-style `lib64` and Debian-style `x86_64-linux-gnu` paths before generic `/usr/lib`, and the ELF header is checked so a 64-bit Tolaria process does not retry with a 32-bit Wayland client library. Linux release jobs install `fcitx5-frontend-gtk3`, the AppImage output-plugin shim copies the GTK3 fcitx immodule and client library into the AppDir before sealing, and runtime startup writes a mount-path-specific `GTK_IM_MODULE_FILE` cache when fcitx is configured via `GTK_IM_MODULE=fcitx` or common fcitx environment hints. If the user has not already chosen `GTK_IM_MODULE`, Tolaria sets `GTK_IM_MODULE=fcitx` before WebKit starts, so both Wayland and X11 AppImage launches can load the bundled module instead of depending on host GTK module caches. The same AppImage path checks whether `fc-match` resolves the default emoji font to `Noto-COLRv1.ttf`; when the user has not provided `FONTCONFIG_FILE` or `FONTCONFIG_PATH`, Tolaria writes a cache-local fontconfig file that rejects only that matched font file and exports it before WebKit starts. The rendering overrides keep AppImage WebViews from blanking after accelerated compositing/DMA-BUF failures, the re-exec addresses AppImage library-order failures that can surface as `Could not create default EGL display: EGL_BAD_PARAMETER`, the fcitx GTK bundle preserves Chinese/Japanese/Korean IME composition for affected AppImage users, and the fontconfig guard avoids known WebKit crashes in COLRv1 emoji font rendering while leaving other emoji fonts available. ## Multi-Window (Note Windows) diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 6cce7d58..64b61609 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -53,7 +53,7 @@ Linux release CI prepares Tauri's AppImage tools cache before `pnpm tauri build` node scripts/appimage-launcher-tools.mjs prepare-plugin ``` -That step patches linuxdeploy's generated GTK `AppRun` wrapper before the AppImage is sealed so direct launches, absolute symlinks, and relative symlinks resolve the real AppRun path before choosing its directory. After build, CI extracts every produced AppImage launcher and verifies the symlink-safe resolver: +That step patches linuxdeploy's generated GTK `AppRun` wrapper before the AppImage is sealed so direct launches, absolute symlinks, and relative symlinks resolve the real AppRun path before choosing its directory. Linux release jobs also install `fcitx5-frontend-gtk3`; the same AppImage output-plugin wrapper copies the GTK3 fcitx immodule and client library into the AppDir so Chinese/Japanese/Korean input does not depend on host GTK module cache paths. After build, CI extracts every produced AppImage launcher and verifies the symlink-safe resolver plus the bundled fcitx files: ```bash node scripts/appimage-launcher-tools.mjs validate-appimages src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage diff --git a/docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md b/docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md new file mode 100644 index 00000000..f7793522 --- /dev/null +++ b/docs/adr/0117-appimage-fcitx-gtk3-frontend-bundle.md @@ -0,0 +1,24 @@ +# ADR-0117: Bundle the fcitx GTK3 frontend in Linux AppImages + +## Status + +Accepted + +## Context + +Linux AppImages run WebKitGTK through the GTK3 input-method stack. Users with fcitx5 can export `GTK_IM_MODULE=fcitx`, `QT_IM_MODULE=fcitx`, and `XMODIFIERS=@im=fcitx`, but the AppImage still cannot load the host GTK immodule reliably because the GTK module cache and library paths are isolated from the mounted AppDir. + +The previous AppImage startup fallback set `GTK_IM_MODULE=fcitx` when fcitx was detected, but it did not make the `im-fcitx5.so` module available inside the AppImage. That left Chinese/Japanese/Korean input dependent on host paths that GTK may not search from a sealed AppImage. + +## Decision + +Linux release jobs install `fcitx5-frontend-gtk3` and the AppImage output-plugin shim copies the GTK3 fcitx immodule plus its client library into the AppDir before the AppImage is sealed. At runtime, AppImage startup writes a mount-path-specific `GTK_IM_MODULE_FILE` cache that points GTK at the bundled module whenever fcitx is configured explicitly or through common fcitx environment hints. + +The sealed AppImage validation step extracts every produced AppImage and fails the release if the symlink-safe AppRun resolver, the bundled fcitx immodule, or the fcitx client library is missing. + +## Consequences + +- fcitx5 input works in AppImage launches without relying on the host GTK immodule cache path. +- X11 fallback launches with explicit `GTK_IM_MODULE=fcitx` use the same bundled module path as Wayland launches. +- Linux AppImage builds now depend on the distro package that provides the GTK3 fcitx frontend. +- If the Ubuntu package path changes, the AppImage validation step fails before publishing a broken bundle. diff --git a/scripts/appimage-launcher-tools.mjs b/scripts/appimage-launcher-tools.mjs index 386cbcc3..2d2cae75 100644 --- a/scripts/appimage-launcher-tools.mjs +++ b/scripts/appimage-launcher-tools.mjs @@ -21,10 +21,19 @@ export const FIXED_LINUXDEPLOY_APPRUN_DIR_LINE = 'this_dir="$(dirname "$(readlink -f "$0")")"' export const APPIMAGE_PLUGIN_WRAPPER_NAME = 'linuxdeploy-plugin-appimage.AppImage' export const REAL_APPIMAGE_PLUGIN_NAME = 'linuxdeploy-plugin-appimage.real.AppImage' +export const APPIMAGE_FCITX_GTK3_IM_MODULE_PATH = + 'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so' +export const APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH = + 'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2' export const DEFAULT_APPIMAGE_PLUGIN_URL = 'https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage' const WRAPPER_MARKER = 'Tolaria AppImage symlink launcher shim' +const REQUIRED_APPIMAGE_PATHS = [ + 'AppRun', + APPIMAGE_FCITX_GTK3_IM_MODULE_PATH, + APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH, +] export function tauriToolsCacheDir(env = process.env) { if (env.TOLARIA_TAURI_TOOLS_DIR) { @@ -76,6 +85,8 @@ set -euo pipefail PLUGIN_URL="\${TOLARIA_APPIMAGE_PLUGIN_URL:-${pluginUrl}}" SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" REAL_PLUGIN="\${TOLARIA_APPIMAGE_REAL_PLUGIN:-"$SCRIPT_DIR/${REAL_APPIMAGE_PLUGIN_NAME}"}" +FCITX_GTK3_IM_MODULE="\${TOLARIA_FCITX_GTK3_IM_MODULE:-/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so}" +FCITX_LIBRARY_DIR="\${TOLARIA_FCITX_LIBRARY_DIR:-/usr/lib/x86_64-linux-gnu}" appdir_from_args() { local previous="" @@ -148,8 +159,39 @@ elif "autogenerated by linuxdeploy" in text and "AppRun.wrapped" in text: PY } +bundle_fcitx_gtk3_module() { + local appdir="\${APPDIR:-}" + + if [ -z "$appdir" ]; then + appdir="$(appdir_from_args "$@" || true)" + fi + + if [ -z "$appdir" ] || [ ! -d "$appdir" ] || [ ! -f "$FCITX_GTK3_IM_MODULE" ]; then + return 0 + fi + + local module_dest="$appdir/${APPIMAGE_FCITX_GTK3_IM_MODULE_PATH}" + local library_dest_dir="$appdir/usr/lib/x86_64-linux-gnu" + + mkdir -p "$(dirname -- "$module_dest")" "$library_dest_dir" + cp -a "$FCITX_GTK3_IM_MODULE" "$module_dest" + + local copied_library=0 + shopt -s nullglob + for lib in "$FCITX_LIBRARY_DIR"/libFcitx5GClient.so* "$FCITX_LIBRARY_DIR"/libFcitx5Utils.so*; do + cp -a "$lib" "$library_dest_dir/" + copied_library=1 + done + shopt -u nullglob + + if [ "$copied_library" -eq 0 ]; then + echo "No fcitx GTK client libraries found in $FCITX_LIBRARY_DIR" >&2 + fi +} + download_real_plugin patch_apprun "$@" +bundle_fcitx_gtk3_module "$@" exec "$REAL_PLUGIN" "$@" ` } @@ -181,6 +223,45 @@ export async function validateAppRunFile(path) { assertSymlinkSafeAppRunText(text, path) } +function extractAppImagePath(appImage, requiredPath, tempDir) { + const result = spawnSync(appImage, ['--appimage-extract', requiredPath], { + cwd: tempDir, + encoding: 'utf8', + }) + + if (result.status === 0) { + return + } + + throw new Error( + [ + `Failed to extract ${requiredPath} from ${appImage}`, + result.stdout.trim(), + result.stderr.trim(), + ] + .filter(Boolean) + .join('\n'), + ) +} + +function assertAppImagePathsExtracted(appImage, tempDir, requiredPaths) { + for (const requiredPath of requiredPaths) { + const extractedPath = join(tempDir, 'squashfs-root', requiredPath) + if (!existsSync(extractedPath)) { + throw new Error(`${appImage} is missing ${requiredPath}`) + } + } +} + +async function validateExtractedAppImage(appImage, tempDir) { + for (const requiredPath of REQUIRED_APPIMAGE_PATHS) { + extractAppImagePath(appImage, requiredPath, tempDir) + } + + await validateAppRunFile(join(tempDir, 'squashfs-root', 'AppRun')) + assertAppImagePathsExtracted(appImage, tempDir, REQUIRED_APPIMAGE_PATHS.slice(1)) +} + export async function validateAppImages(paths) { if (paths.length === 0) { throw new Error('At least one AppImage path is required for launcher validation') @@ -189,24 +270,7 @@ export async function validateAppImages(paths) { for (const appImage of paths.map((path) => resolve(path))) { const tempDir = await mkdtemp(join(tmpdir(), 'tolaria-appimage-')) try { - const result = spawnSync(appImage, ['--appimage-extract', 'AppRun'], { - cwd: tempDir, - encoding: 'utf8', - }) - - if (result.status !== 0) { - throw new Error( - [ - `Failed to extract AppRun from ${appImage}`, - result.stdout.trim(), - result.stderr.trim(), - ] - .filter(Boolean) - .join('\n'), - ) - } - - await validateAppRunFile(join(tempDir, 'squashfs-root', 'AppRun')) + await validateExtractedAppImage(appImage, tempDir) } finally { await rm(tempDir, { recursive: true, force: true }) } diff --git a/scripts/appimage-launcher-tools.test.mjs b/scripts/appimage-launcher-tools.test.mjs index 2dad3378..25365fa7 100644 --- a/scripts/appimage-launcher-tools.test.mjs +++ b/scripts/appimage-launcher-tools.test.mjs @@ -112,3 +112,52 @@ test('plugin wrapper patches AppRun before delegating to the real output plugin' assert.equal(patched.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false) assert.equal(patched.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true) }) + +test('plugin wrapper bundles fcitx GTK3 input module before sealing AppImage', async () => { + const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-fcitx-')) + const appDir = join(root, 'Tolaria.AppDir') + const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage') + const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage') + const pluginMarker = join(root, 'plugin-ran') + const hostModule = join(root, 'host', 'im-fcitx5.so') + const hostLibraryDir = join(root, 'host-lib') + const hostLibrary = join(hostLibraryDir, 'libFcitx5GClient.so.2') + + await mkdir(appDir) + await mkdir(dirname(hostModule), { recursive: true }) + await mkdir(hostLibraryDir) + await writeFile(hostModule, 'fake fcitx gtk module', 'utf8') + await writeFile(hostLibrary, 'fake fcitx client library', 'utf8') + await writeFile(wrapper, appImagePluginWrapperSource(), 'utf8') + await chmod(wrapper, 0o755) + await writeFile( + realPlugin, + `#!/usr/bin/env bash\nset -euo pipefail\ntouch "${pluginMarker}"\n`, + 'utf8', + ) + await chmod(realPlugin, 0o755) + + const result = spawnSync(wrapper, [], { + encoding: 'utf8', + env: { + ...process.env, + APPDIR: appDir, + TOLARIA_APPIMAGE_REAL_PLUGIN: realPlugin, + TOLARIA_FCITX_GTK3_IM_MODULE: hostModule, + TOLARIA_FCITX_LIBRARY_DIR: hostLibraryDir, + }, + }) + + assert.equal(result.status, 0, result.stderr) + assert.equal(existsSync(pluginMarker), true) + assert.equal( + existsSync( + join( + appDir, + 'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so', + ), + ), + true, + ) + assert.equal(existsSync(join(appDir, 'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2')), true) +}) diff --git a/src-tauri/src/linux_appimage.rs b/src-tauri/src/linux_appimage.rs index b03fda3c..7ef8716f 100644 --- a/src-tauri/src/linux_appimage.rs +++ b/src-tauri/src/linux_appimage.rs @@ -25,6 +25,10 @@ const FCITX_ENV_HINT_KEYS: [&str; 4] = [ "QT_IM_MODULE", "SDL_IM_MODULE", ]; +const FCITX_GTK3_IM_MODULE_RELATIVE_PATH: &str = + "usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so"; +#[cfg(all(desktop, target_os = "linux"))] +const TOLARIA_FCITX_IMMODULES_CACHE_FILE: &str = "tolaria-appimage-fcitx5-immodules.cache"; const COLRV1_EMOJI_FONT_FILE: &str = "Noto-COLRv1.ttf"; #[cfg(all(desktop, target_os = "linux"))] const TOLARIA_COLRV1_FONTCONFIG_FILE: &str = "tolaria-appimage-no-colrv1-emoji.conf"; @@ -101,6 +105,68 @@ where .any(|key| get_var(key).is_some_and(|value| env_mentions_fcitx(&value))) } +fn non_empty_env(get_var: &mut F, key: &str) -> Option +where + F: FnMut(&str) -> Option, +{ + get_var(key).filter(|value| !value.trim().is_empty()) +} + +#[derive(Debug, PartialEq, Eq)] +struct FcitxGtkModuleFileOverride { + cache_path: std::path::PathBuf, + cache_contents: String, +} + +fn fcitx_immodules_cache_contents(module_path: &std::path::Path) -> String { + format!( + r#"# Tolaria AppImage GTK input method modules +"{}" +"fcitx" "Fcitx 5" "fcitx" "" "ja:ko:zh:*" +"#, + module_path.display() + ) +} + +fn should_use_bundled_fcitx_gtk_module(get_var: &mut F) -> bool +where + F: FnMut(&str) -> Option, +{ + if !is_linux_appimage_launch(&mut *get_var) { + return false; + } + if has_non_empty_env(get_var, "GTK_IM_MODULE_FILE") { + return false; + } + if let Some(gtk_im_module) = non_empty_env(get_var, "GTK_IM_MODULE") { + return env_mentions_fcitx(>k_im_module); + } + + has_fcitx_env_hint(get_var) +} + +fn fcitx_gtk_im_module_file_override_with( + get_var: &mut F, + mut module_exists: E, + cache_path: std::path::PathBuf, +) -> Option +where + F: FnMut(&str) -> Option, + E: FnMut(&std::path::Path) -> bool, +{ + if !should_use_bundled_fcitx_gtk_module(get_var) { + return None; + } + + let appdir = non_empty_env(get_var, "APPDIR")?; + let module_path = std::path::Path::new(appdir.trim()).join(FCITX_GTK3_IM_MODULE_RELATIVE_PATH); + + module_exists(&module_path).then(|| FcitxGtkModuleFileOverride { + cache_path, + cache_contents: fcitx_immodules_cache_contents(&module_path), + }) +} + fn fcitx_gtk_im_module_override_with(get_var: &mut F) -> Option where F: FnMut(&str) -> Option, @@ -108,9 +174,6 @@ where if !is_linux_appimage_launch(&mut *get_var) { return None; } - if !is_wayland_session(&mut *get_var) { - return None; - } if has_env(get_var, "GTK_IM_MODULE") { return None; } @@ -245,12 +308,43 @@ where pub(crate) fn apply_startup_env_overrides() { apply_wayland_client_preload(); apply_colrv1_emoji_font_guard(); + apply_fcitx_gtk_im_module_file(); for env_override in startup_env_overrides_with(|key| std::env::var(key).ok()) { std::env::set_var(env_override.key, env_override.value); } } +#[cfg(all(desktop, target_os = "linux"))] +fn apply_fcitx_gtk_im_module_file() { + let Some(cache_path) = fcitx_immodules_cache_file_path() else { + eprintln!("Tolaria AppImage fcitx GTK module skipped: failed to resolve cache directory"); + return; + }; + let Some(env_override) = fcitx_gtk_im_module_file_override_with( + &mut |key| std::env::var(key).ok(), + std::path::Path::is_file, + cache_path, + ) else { + return; + }; + let Some(parent) = env_override.cache_path.parent() else { + return; + }; + + if let Err(error) = std::fs::create_dir_all(parent) { + eprintln!("Tolaria AppImage fcitx GTK module skipped: failed to prepare cache ({error})"); + return; + } + + if let Err(error) = std::fs::write(&env_override.cache_path, env_override.cache_contents) { + eprintln!("Tolaria AppImage fcitx GTK module skipped: failed to write cache ({error})"); + return; + } + + std::env::set_var("GTK_IM_MODULE_FILE", env_override.cache_path.as_os_str()); +} + #[cfg(all(desktop, target_os = "linux"))] fn apply_colrv1_emoji_font_guard() { let Some(font_path) = @@ -309,6 +403,18 @@ fn colrv1_fontconfig_file_path() -> Option { ) } +#[cfg(all(desktop, target_os = "linux"))] +fn fcitx_immodules_cache_file_path() -> Option { + let cache_dir = + dirs::cache_dir().or_else(|| dirs::home_dir().map(|home| home.join(".cache")))?; + + Some( + cache_dir + .join("tolaria") + .join(TOLARIA_FCITX_IMMODULES_CACHE_FILE), + ) +} + #[cfg(all(desktop, target_os = "linux"))] fn launched_appimage_path() -> Result { if let Some(appimage) = std::env::var_os("APPIMAGE").filter(|value| !value.is_empty()) { @@ -366,7 +472,8 @@ fn apply_wayland_client_preload() { mod tests { use super::{ colrv1_emoji_font_path_with, colrv1_emoji_fontconfig_contents, elf_library_matches_process, - startup_env_overrides_with, wayland_client_preload_path_with, StartupEnvOverride, + fcitx_gtk_im_module_file_override_with, startup_env_overrides_with, + wayland_client_preload_path_with, StartupEnvOverride, FCITX_GTK3_IM_MODULE_RELATIVE_PATH, }; fn default_webkit_overrides() -> Vec { @@ -442,6 +549,18 @@ mod tests { assert_eq!(overrides, default_webkit_and_fcitx_overrides()); } + #[test] + fn startup_env_overrides_enable_fcitx_gtk_module_for_x11_appimage() { + let overrides = startup_env_overrides_with(|key| match key { + "APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()), + "XDG_SESSION_TYPE" => Some("x11".to_string()), + "XMODIFIERS" => Some("@im=fcitx".to_string()), + _ => None, + }); + + assert_eq!(overrides, default_webkit_and_fcitx_overrides()); + } + #[test] fn startup_env_overrides_preserve_explicit_gtk_im_module() { let overrides = @@ -461,6 +580,50 @@ mod tests { assert_eq!(overrides, default_webkit_overrides()); } + #[test] + fn fcitx_module_file_override_points_gtk_to_bundled_appimage_module() { + let appdir = std::path::Path::new("/tmp/.mount_Tolaria"); + let module_path = appdir.join(FCITX_GTK3_IM_MODULE_RELATIVE_PATH); + let cache_path = std::path::PathBuf::from("/tmp/tolaria/immodules.cache"); + let env_override = fcitx_gtk_im_module_file_override_with( + &mut |key| match key { + "APPDIR" => Some(appdir.display().to_string()), + "APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()), + "GTK_IM_MODULE" => Some("fcitx".to_string()), + "XDG_SESSION_TYPE" => Some("x11".to_string()), + _ => None, + }, + |path| path == module_path, + cache_path.clone(), + ) + .unwrap(); + + assert_eq!(env_override.cache_path, cache_path); + assert!(env_override + .cache_contents + .contains(&module_path.display().to_string())); + assert!(env_override + .cache_contents + .contains("\"fcitx\" \"Fcitx 5\"")); + } + + #[test] + fn fcitx_module_file_override_preserves_explicit_module_cache() { + let env_override = fcitx_gtk_im_module_file_override_with( + &mut |key| match key { + "APPDIR" => Some("/tmp/.mount_Tolaria".to_string()), + "APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()), + "GTK_IM_MODULE" => Some("fcitx".to_string()), + "GTK_IM_MODULE_FILE" => Some("/tmp/custom-immodules.cache".to_string()), + _ => None, + }, + |_| true, + std::path::PathBuf::from("/tmp/tolaria/immodules.cache"), + ); + + assert_eq!(env_override, None); + } + #[test] fn colrv1_font_guard_targets_reported_appimage_emoji_font() { let font_path = colrv1_emoji_font_path_with(