Terminal External Input Method Issue


问题

这是我曾经出现过的两次相同的问题
第一次是在我把Ubuntu自带的DE, DM等完整图形化框架拆完了,只保留了X11并使用i3-wm作为窗口管理器时出现的
第二次是我Ubuntu换Debian过程中出现的,同样是没有完整的图形界面
具体表现为输入法只在Terminal里工作,其他的窗口不工作,包括vim等这种在终端窗口启动的应用\

分析

第一次遇到这个问题时,我怀疑过输入法框架,我换了Fcitx5, IBus;我怀疑过浏览器,我尝试用过chrome, firefox, w3m, qutebrowser等各种浏览器,甚至因为qutebrowser用的chrome内核过于老旧尝试过编译最新版本的内核,但没有结果
我一度打算接受就在Linux上使用英文环境,我甚至还写了一段时间的英文博客
我在Google和各大论坛上搜索过相关内容,无外乎是以下三种处理方式:

  • 环境变量,在~/.xprofile中写入输入法环境变量 export GTK_IM_MODULE=fcitx, export QT_IM_MODULE=fcitx, export XMODIFIERS=@im=fcitx
  • 在i3 config里添加 exec --no-startup-id fcitx5 &
  • 排查GUI框架的兼容性,从古老的XIM协议到现代使用的GTK3/4, Qt5/6和Electron
  • 排查Fcitx5本身的问题,用fcitx5-diagnose进行检查

现代输入法框架(Fcitx5, IBus)已经不是键盘映射工具,本质上是C/S(客户端/服务端)架构的复杂系统

  • 服务端:输入法守护进程
  • 客户端:各个GUI应用程序(浏览器,编辑器等)

值得说的一点是,fcitx5-diagnose会直接检测出D-Bus的连接状态、环境变量在各个层面的存在情况
所以,经过以上的排除,问题定格在通信上,在Linux中就是D-Bus(具体来说是其中的Session Bus),它是Linux中的一个重要的IPC和RPC系统,是连接客户端和服务端的通路
如果图形界面初始化不完整,导致D-Bus没启动,或者环境变量没传递到位,总线就断了,应用窗口就无法唤起输入法

完整桌面环境的情况

完整的桌面环境(如GNOME/KDE)在启动时,通过显示管理器(如GDM/SDDM)登录DE时,会经历以下启动流程

  1. 拉起总线:显示管理器会话管理器(如gnome-session)会首先调用dbus-run-session通过systemd --user启动属于你的Session Bus
  2. 将生成的DBUS_SESSION_BUS_ADDRESS写入当前图形绘画的根环境中
  3. 环境同步:通过执行dbus-update-activation-environment --all,把这个变量同步给systemd --user空间和X11/Wayland服务端

随后由桌面拉起来的所有组件(窗口管理器、状态栏、输入法、浏览器)作为子进程,天然继承了这个环境变量,它们之间能够通信

不完整的桌面环境的情况

我只装了一个窗口管理器,并通过startx启动,就没有上述完整的流水线,总线的问题会表现为以下两种

  1. 直接在.xinitrcexec i3,整个X11里没有Session Bus进程。输入法启动时试图去注册自己,直接报错退出;应用程序想去找输入法,没有总线可以连接
  2. 总线启动了,但是断连,现代Linux中,即使只用startxsystemd --user往往也会在后台自动塞一个dbus-daemon启动,但是没人把它的地址告诉X11或窗口管理器,结果就是有总线但是没人知道没人用

为什么Terminal里可以正常工作

我记不太清我当初怎么配置Ubuntu了,但是有一下三种方式会导致这种奇观

1. 在终端配置文件如.bashrc里写入fcitx5 &

这意味着fcitx5是当前终端拉起来的守护进程,当前终端自然能用
但这也会导致每做一次会执行.bashrc的操作,比如新建终端,新开标签页,在tmux里切一个新panel,都会试图拉起一个新的fcitx5,好在Fcitx5内部有单例锁
这是极其不正确的做法,让终端的配置变得很脏

2. XIM的自动回退

由于D-Bus故障,现代浏览器和终端在面对这个情况时,可能会采取完全不同的态度

  • 许多终端内部保留了对XIM这个老输入法协议的支持。当D-Bus挂了,或者现代IM模块通信失效时,终端会自动向下兼容
  • 现代浏览器大多为了保障安全都已经砍掉XIM,只认基于D-Bus的现代输入法框架

这就造成,当D-Bus损坏时,终端可以通过XIM进行对输入法的通信,而不认识XIM的实例无法通信

3. startx~/.bash_profile/~/.profile的时间差切片

如果不用GDM/LightDM这种图形登录界面,而是在tty里输入密码登录,然后手动startx启动X11,这期间有一个巨大的环境变量陷阱

  1. 登录tty时,系统会加载~/.bash_profile/~/.profile
  2. 如果把输入法环境变量写在~/.profile里,此时当前tty确实拥有了这些变量
  3. 接着运行startx,如果~/。xinitrc没有显式地继承或传递当前shell的环境变量,X服务拉起来的WM拿到的就是一个纯净的环境
  4. 当通过WM打开一个终端时,这个终端为了初始化,可能会再次作为登录shell运行,或者重新读取了某些包含变量的配置文件

这也就造成了WM拉起来的非终端窗口不知道输入法变量;终端悄悄补齐了这些变量

解决

本质:在图形界面的“最上游”把D-Bus会话拉起来,并将输入法环境变量强行注入到整个图形世界的“根环境”中

方案1: .xinitrc.xprofile包裹

如果是通过startx启动极简窗口管理器,或者使用LightDM这种会读取~/.xprofile的登录管理器
可以在~/.xinitrc~/.xprofile里这么写

#!/bin/sh

# 声明输入法环境变量(让整个图形界面的父进程都拥有这些变量)
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS="@im=fcitx"

# 将这些变量强行同步给 D-Bus 和 systemd 空间
# 这样通过 systemd 或是 D-Bus 激活的沙盒应用(如 Flatpak/Snap)也能拿到变量
if which dbus-update-activation-environment >/dev/null 2>&1; then
    dbus-update-activation-environment --all
fi

# 后台启动输入法守护进程
fcitx5 -d &

#  用 dbus-run-session 启动窗口管理器(以 i3 为例)
# 这保证了 WM 以及它以后拉起的所有浏览器、编辑器,全部处于同一个 D-Bus 会话总线中
exec dbus-run-session i3

方案2:编辑/etc/environment

在文件末尾加入以下内容

GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx

这个文件在系统刚开机、用户还没登录时就会被内核级进程读取。所有登录后的会话都会无条件继承这里面的变量
配置后需要重启生效,需要手动启动输入法

方案3:通过systemd管理

在使用systemd的发行版上,可以将fcitx5的生命周期交给systemd --usr来管理

启动Fcitx5的自带用户服务

systemctl --user enable --now fcitx5

解决systemd 空间的变量隔离,因为systemd用户实例和X11图形会话是平行的,它默认拿不到图形界面里的变量
所以必须在窗口管理器中先执行dbus-update-avtivation-environment --all

这样,每次窗口管理器起来后,会立刻把当前的D-Bus地址和输入法变量“反哺”给systemd,systemd随后拉起来的fcitx5就能和整个桌面通信

本篇主要基于X11分析,在Wayland下,情况会更复杂,因为Wayland协议要求输入法通过zwp_input_method_v2这类专用协议进行进程间通信,对安全隔离要求更高,直接设置GTK_IM_MODULE这种变量的方式可能会失效