fix: bundle fcitx gtk module in appimage

This commit is contained in:
lucaronin
2026-05-13 16:44:38 +02:00
parent 41f0e00cfb
commit 201aefca05
9 changed files with 327 additions and 25 deletions

View File

@@ -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<F>(get_var: &mut F, key: &str) -> Option<String>
where
F: FnMut(&str) -> Option<String>,
{
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<F>(get_var: &mut F) -> bool
where
F: FnMut(&str) -> Option<String>,
{
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(&gtk_im_module);
}
has_fcitx_env_hint(get_var)
}
fn fcitx_gtk_im_module_file_override_with<F, E>(
get_var: &mut F,
mut module_exists: E,
cache_path: std::path::PathBuf,
) -> Option<FcitxGtkModuleFileOverride>
where
F: FnMut(&str) -> Option<String>,
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<F>(get_var: &mut F) -> Option<StartupEnvOverride>
where
F: FnMut(&str) -> Option<String>,
@@ -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<std::path::PathBuf> {
)
}
#[cfg(all(desktop, target_os = "linux"))]
fn fcitx_immodules_cache_file_path() -> Option<std::path::PathBuf> {
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<std::path::PathBuf, String> {
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<StartupEnvOverride> {
@@ -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(