让 Fcitx5 的分组切换更像 Windows:KDE Plasma 常驻「中 / EN」指示器
最终效果与适用环境
这套方案的目标很简单:保留 Fcitx 原生托盘图标显示“当前输入法”,再额外用一个常驻的 中 / EN 指示器显示“当前 Fcitx 分组”。
- 适用环境:Debian Sid、KDE Plasma 6.7.4、Wayland、Fcitx5 5.1.x;其他使用 KDE Plasma + Fcitx5 的发行版也可以按文中的 D-Bus 接口和依赖调整。
- 切换方式:
Super + Space(Win + 空格)切换 Fcitx 分组。 - 显示效果:
默认分组显示中,english only分组显示EN;Fcitx 自己的拼 / 键盘图标继续独立工作。 - 更新方式:正常切换使用事件驱动,不依赖高频轮询。
- 实测延迟:从发出分组切换到自定义 indicator 发出图标更新,约 20.8 ms。
- 自启动:使用
systemd --user,随graphical-session.target启动,并在异常退出时自动重启。
如果你想要的是接近 Windows 语言栏的“当前输入环境一眼可见”,而不是只在切换瞬间弹出一条通知,这套实现就是为这个需求准备的。
我在 Linux 上使用 Fcitx5 的“分组”功能来模拟 Windows 的输入语言行为。我有两个分组:
默认:包含拼音和英文键盘;english only:只有英文键盘。
我把 Super + Space(也就是 Win + 空格)绑定为切换输入法分组。功能上已经很接近 Windows,但是视觉反馈一直不够好:Fcitx 自己的托盘图标只告诉我“当前输入法是什么”,不能告诉我“当前处于哪个分组”。
例如,下面两个状态在 Fcitx 原生托盘里很难区分:
默认分组 + keyboard-us
english only 分组 + keyboard-us
它们的当前输入法都是英文键盘,但语义完全不同。前者按 Shift 可以回到拼音,后者就是一个严格的纯英文环境。
我想要的是 Windows 右下角语言指示器那种感觉:无论当前组内是什么输入法,都有一个独立的、常驻的状态告诉我目前是中文环境还是 English-only 环境。
最终效果是:
Fcitx 原生图标 自定义分组图标
[拼 / 键盘] [中 / EN]
两者各司其职:原生 Fcitx 图标显示“当前输入法”,额外的 indicator 显示“当前分组”。
为什么最后自己写了一个,而不是直接装现成插件
这次配置过程中我首先尝试寻找现成方案,但没有找到一个成熟、直接针对 Fcitx5 Current Input Method Group 做常驻显示的 KDE 组件。
几个看起来接近的方案实际上解决的是不同问题:
- Fcitx5 自己的 Status Notifier:很好用,但它的图标主要表示当前输入法,例如拼音或键盘,不是当前输入法分组。
- KDE 的 Keyboard Layout 指示器:它表示的是 XKB 键盘布局,不等于 Fcitx 的 Input Method Group。
- KIMPanel / KDE Input Method Panel:更偏向输入法候选、状态和 UI 集成,并不是一个 Windows 风格的常驻“当前分组”标签。
- 自己写 Plasma plasmoid:我实际试过,但 Plasma 6 对 metadata、QML API、包结构都比较严格。第一次实验甚至让
plasmashell退出,顶部和底部面板以及桌面一起消失。虽然可以继续把 plasmoid 写正确,但为了一个输入法状态指示器,把代码直接加载进 Plasma Shell 的风险收益比并不好。
因此最后选择了一个更隔离的结构:独立 StatusNotifier / AppIndicator 进程。
它即使崩溃,也只是自己的 中 / EN 图标消失,不会拖垮 Plasma 面板。
Fcitx5 本身已经提供了我们需要的控制接口
Fcitx5 的 D-Bus Controller 可以直接查询和切换当前分组:
gdbus call --session \
--dest org.fcitx.Fcitx5 \
--object-path /controller \
--method org.fcitx.Fcitx.Controller1.CurrentInputMethodGroup
我的机器会返回:
('默认',)
切到 English-only:
gdbus call --session \
--dest org.fcitx.Fcitx5 \
--object-path /controller \
--method org.fcitx.Fcitx.Controller1.SwitchInputMethodGroup \
'english only'
因此 indicator 本身不需要模拟快捷键,也不需要解析 Fcitx 配置文件。
第一版的问题:300 ms 轮询仍然有明显割裂感
最初版本每 300 ms 调一次 CurrentInputMethodGroup():
每 300 ms询问一次 Fcitx
↓
发现分组变化
↓
更新托盘图标
技术上已经能用,但是在按下 Win + 空格后,屏幕上的 中 / EN 有时会晚一点才变。300 ms 看起来很短,放在一个需要即时反馈的输入状态 UI 上却足以让人感觉“不是系统原生行为”。
所以后面继续研究了 Fcitx 自己的事件链。
事件驱动:借用 Fcitx 官方托盘的 NewIcon 信号
Fcitx 内部存在 InputMethodGroupChanged 事件,而且 Fcitx 官方 Status Notifier 本来就会监听这个事件。当分组变化时,官方托盘会立即执行自己的图标/菜单更新,并在 D-Bus 上发出:
org.kde.StatusNotifierItem.NewIcon
于是可以把这个官方事件链当成我们的触发器:
Win + Space
↓
Fcitx 内部 InputMethodGroupChanged
↓
Fcitx 官方 StatusNotifier 发 NewIcon
↓
自定义 indicator 收到信号
↓
读取 CurrentInputMethodGroup
↓
立即更新 中 / EN
实测从发出分组切换到自定义 indicator 发出自己的 NewIcon,约为 20.8 ms。这已经基本属于按下快捷键就同步改变的体感。
同时代码仍然保留一个 10 秒一次的低频兜底同步,只用于 Fcitx 异常重启、托盘重新注册或极端情况下漏掉一次事件。正常切换不依赖这个定时器。
2026-08-15 修复说明: 初版代码中,
rediscover_fcitx_sni()作为GLib.idle_add()的回调时错误返回了True。GLib 会把True解释为“保留该 idle source 并继续调度”,从而形成高频 D-Bus 查询并造成 Python、dbus-daemon、Fcitx 与 KDED 的异常 CPU 占用。现已改为在该回调完成一次重新发现后返回False;正常分组变化仍由 Fcitx StatusNotifier 的NewIcon事件驱动,10 秒定时器仅作为低频兜底。
环境与依赖
这套配置是在下面的环境完成的:
Debian Sid
KDE Plasma 6.7.4
Wayland
Fcitx5 5.1.x
Python 3
Debian 上需要的包:
sudo apt install \
python3-dbus \
python3-gi \
gir1.2-gtk-3.0 \
gir1.2-ayatanaappindicator3-0.1
当前实现使用
AyatanaAppIndicator3。运行时可能会看到 libayatana-appindicator 的 deprecation warning;它不影响当前功能。以后如果要做成真正长期维护的小项目,可以改写成 Qt/KF6 原生 StatusNotifierItem。
最终目录结构
~/.local/share/fcitx-group-indicator/
├── indicator.py
└── icons/
├── index.theme
└── scalable/apps/
├── fcitx-group-default.svg
└── fcitx-group-english.svg
~/.config/systemd/user/
└── fcitx-group-indicator.service
下面是这台机器当前实际运行的完整代码。
indicator.py 完整内容
#!/usr/bin/python3
import os
import dbus
import gi
from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)
gi.require_version('Gtk', '3.0')
gi.require_version('AyatanaAppIndicator3', '0.1')
from gi.repository import Gtk, GLib, AyatanaAppIndicator3 as AppIndicator
BASE = os.path.dirname(os.path.abspath(__file__))
ZH_ICON = os.path.join(BASE, 'icons/scalable/apps/fcitx-group-default.svg')
EN_ICON = os.path.join(BASE, 'icons/scalable/apps/fcitx-group-english.svg')
FCITX_IFACE = 'org.fcitx.Fcitx.Controller1'
SNI_IFACE = 'org.kde.StatusNotifierItem'
WATCHER_IFACE = 'org.kde.StatusNotifierWatcher'
WATCHER_NAME = 'org.kde.StatusNotifierWatcher'
WATCHER_PATH = '/StatusNotifierWatcher'
bus = dbus.SessionBus()
fcitx_obj = bus.get_object('org.fcitx.Fcitx5', '/controller')
fcitx = dbus.Interface(fcitx_obj, FCITX_IFACE)
indicator = AppIndicator.Indicator.new(
'fcitx-group-indicator', 'input-keyboard',
AppIndicator.IndicatorCategory.SYSTEM_SERVICES)
indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE)
last_group = None
fcitx_sni_match = None
def current_group():
try:
return str(fcitx.CurrentInputMethodGroup())
except Exception:
return '?'
def refresh(*_args):
global last_group
group = current_group()
if group == last_group:
return True
last_group = group
if group == '默认':
icon, label = ZH_ICON, '中'
elif group == 'english only':
icon, label = EN_ICON, 'EN'
else:
icon, label = 'input-keyboard', '?'
indicator.set_icon_full(icon, 'Fcitx 输入法分组')
indicator.set_label(label, 'EN')
state_item.set_label('当前分组:' + group)
return True
def on_fcitx_new_icon(*_args):
# Fcitx's own StatusNotifier emits this immediately on
# EventType::InputMethodGroupChanged.
refresh()
def split_registered_item(item):
text = str(item)
pos = text.find('/')
if pos <= 0:
return None, None
return text[:pos], text[pos:]
def rediscover_fcitx_sni(*_args):
global fcitx_sni_match
try:
watcher_obj = bus.get_object(WATCHER_NAME, WATCHER_PATH)
props = dbus.Interface(watcher_obj, 'org.freedesktop.DBus.Properties')
items = props.Get(WATCHER_IFACE, 'RegisteredStatusNotifierItems')
except Exception:
return False
for item in items:
service, path = split_registered_item(item)
if not service:
continue
try:
obj = bus.get_object(service, path)
p = dbus.Interface(obj, 'org.freedesktop.DBus.Properties')
if str(p.Get(SNI_IFACE, 'Id')) != 'Fcitx':
continue
except Exception:
continue
if fcitx_sni_match is not None:
fcitx_sni_match.remove()
fcitx_sni_match = bus.add_signal_receiver(
on_fcitx_new_icon, signal_name='NewIcon',
dbus_interface=SNI_IFACE, bus_name=service, path=path)
refresh()
return False
return False
def toggle(_item=None):
group = current_group()
target = 'english only' if group == '默认' else '默认'
try:
fcitx.SwitchInputMethodGroup(target)
finally:
refresh()
menu = Gtk.Menu()
state_item = Gtk.MenuItem(label='当前分组:?')
state_item.set_sensitive(False)
menu.append(state_item)
switch_item = Gtk.MenuItem(label='切换分组')
switch_item.connect('activate', toggle)
menu.append(switch_item)
menu.append(Gtk.SeparatorMenuItem())
quit_item = Gtk.MenuItem(label='退出指示器')
quit_item.connect('activate', lambda _x: Gtk.main_quit())
menu.append(quit_item)
menu.show_all()
indicator.set_menu(menu)
bus.add_signal_receiver(
lambda *_: GLib.idle_add(rediscover_fcitx_sni),
signal_name='StatusNotifierItemRegistered',
dbus_interface=WATCHER_IFACE, bus_name=WATCHER_NAME,
path=WATCHER_PATH)
bus.add_signal_receiver(
lambda *_: GLib.idle_add(rediscover_fcitx_sni),
signal_name='StatusNotifierItemUnregistered',
dbus_interface=WATCHER_IFACE, bus_name=WATCHER_NAME,
path=WATCHER_PATH)
refresh()
rediscover_fcitx_sni()
# Low-frequency safety sync only; normal changes are event-driven.
GLib.timeout_add_seconds(10, refresh)
Gtk.main()
这份代码里有几个值得注意的点:
CurrentInputMethodGroup()用于读取当前 Fcitx 分组;SwitchInputMethodGroup()用于菜单里的手动切换;- indicator 会在 Plasma 的
org.kde.StatusNotifierWatcher中寻找Id == Fcitx的官方托盘项目; - 监听这个项目的
org.kde.StatusNotifierItem.NewIcon,把它作为分组变化的即时触发器; - 如果 Fcitx 自己的 StatusNotifier 被重新注册,代码会自动重新发现并重新绑定;
- 10 秒定时器只是兜底,不参与正常实时更新。
两个 SVG 图标
fcitx-group-default.svg
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect x="4" y="4" width="56" height="56" rx="12" fill="#2563eb" stroke="#ffffff" stroke-width="2"/>
<text x="32" y="45" text-anchor="middle" font-family="Noto Sans CJK SC, Noto Sans, sans-serif" font-size="38" font-weight="700" fill="#ffffff">中</text>
</svg>
fcitx-group-english.svg
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect x="4" y="4" width="56" height="56" rx="12" fill="#374151" stroke="#ffffff" stroke-width="2"/>
<text x="32" y="42" text-anchor="middle" font-family="Noto Sans, sans-serif" font-size="25" font-weight="700" fill="#ffffff">EN</text>
</svg>
icons/index.theme
[Icon Theme]
Name=Fcitx Group Indicator
Comment=Icons for Fcitx group indicator
Directories=scalable/apps
[scalable/apps]
Size=64
Context=Applications
Type=Scalable
MinSize=16
MaxSize=128
目前真正显示托盘图标时使用的是 SVG 的绝对路径,这样可以绕过 Plasma/Qt 图标主题缓存和图标名 fallback 带来的不确定性。
systemd 用户服务
我没有把它放进 KDE 的 ~/.config/autostart/*.desktop,而是使用 systemd user service。原因是这个 indicator 本质上是一个和图形会话同生命周期的小后台进程,systemd 更方便做自动启动、异常重启、日志和生命周期管理。
当前实际 unit 内容如下:
[Unit]
Description=Fcitx input method group indicator
PartOf=graphical-session.target
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/iruanp/.local/share/fcitx-group-indicator/indicator.py
Restart=on-failure
RestartSec=2
Environment=GTK_MODULES=
[Install]
WantedBy=graphical-session.target
启用并立即启动:
systemctl --user daemon-reload
systemctl --user enable --now fcitx-group-indicator.service
检查:
systemctl --user status fcitx-group-indicator.service
systemctl --user is-enabled fcitx-group-indicator.service
systemctl --user is-active fcitx-group-indicator.service
我的当前状态应该是:
enabled
active
因为 unit 使用:
WantedBy=graphical-session.target
PartOf=graphical-session.target
它只在图形桌面会话中运行。退出 Plasma 会话后也会一起停止,不需要 loginctl enable-linger。
上面的
ExecStart是我这台机器的实际路径。如果其他用户复制,需要把/home/iruanp换成自己的 home。更通用的 unit 也可以使用 systemd 的%hspecifier,例如ExecStart=/usr/bin/python3 %h/.local/share/fcitx-group-indicator/indicator.py。
从零安装
先创建目录:
mkdir -p ~/.local/share/fcitx-group-indicator/icons/scalable/apps
mkdir -p ~/.config/systemd/user
分别把上文代码保存到:
~/.local/share/fcitx-group-indicator/indicator.py
~/.local/share/fcitx-group-indicator/icons/index.theme
~/.local/share/fcitx-group-indicator/icons/scalable/apps/fcitx-group-default.svg
~/.local/share/fcitx-group-indicator/icons/scalable/apps/fcitx-group-english.svg
~/.config/systemd/user/fcitx-group-indicator.service
给 Python 文件执行权限不是必须的,因为 unit 明确使用 /usr/bin/python3 启动,不过也可以:
chmod +x ~/.local/share/fcitx-group-indicator/indicator.py
然后:
systemctl --user daemon-reload
systemctl --user enable --now fcitx-group-indicator.service
如果你的 Fcitx 分组名和我的不同
当前代码是按我的两个组名写的:
默认
english only
映射为:
默认 -> 中
english only -> EN
如果你的组名不同,修改 refresh() 中:
if group == '默认':
icon, label = ZH_ICON, '中'
elif group == 'english only':
icon, label = EN_ICON, 'EN'
以及 toggle() 中的目标组即可。
也可以继续把它改造成配置驱动,例如:
[groups]
"默认" = "中"
"english only" = "EN"
这样代码本身就完全不需要知道具体分组名了。
恢复 Fcitx“已切换分组”通知
我之前还遇到过另一个问题:曾经在 Fcitx 的“已切换到分组……”通知上点过“不再显示”,后来怎么开 ShowInputMethodInformation 都不回来。
原因是 Fcitx 会单独记录隐藏的通知类型:
[HiddenNotifications]
0=enumerate-group
文件是:
~/.config/fcitx5/conf/notifications.conf
把 enumerate-group 从 HiddenNotifications 中移除,再让 Fcitx 重新加载配置,就可以恢复切组通知。
这和本文的常驻 中 / EN 指示器是两个独立功能:瞬时通知适合确认“刚刚切换了”,常驻指示器适合随时确认“现在在哪个组”。
为什么不直接修改 Fcitx 官方托盘图标
理论上也可以 fork Fcitx 的 notificationitem 模块,让它直接按当前 group 改图标。但这会把一个很个人化的 UI 偏好变成 Fcitx 模块补丁:
- Fcitx 升级后需要维护补丁;
- 需要重新编译相关模块;
- 官方托盘图标本来还承担“当前输入法”的信息,改成 group 反而损失原本的信息。
保留两个状态项反而更清晰:
Fcitx:当前输入法
Indicator:当前分组
而且 indicator 和 Fcitx 是松耦合的:只依赖公开 D-Bus Controller 和官方托盘的标准 StatusNotifier 信号。
目前还可以继续改进的地方
当前版本已经把最明显的体验问题——轮询延迟——解决了,但它还不是一个正式发布的软件包。
后续如果继续打磨,我会考虑:
- 改成 Qt6 / KF6 原生 StatusNotifierItem,让菜单、字体、DPI 和 Plasma 更统一;
- 自动读取所有 Fcitx Groups,而不是硬编码两个组;
- 用 TOML/JSON 配置分组到文字、图标的映射;
- 菜单直接显示所有 group,并使用 radio item 标记当前项;
- 做成 Debian package 或独立 Git 仓库;
- 如果 Fcitx 未来公开“Current Group Changed”的 D-Bus 信号,直接监听它,不再借用官方托盘
NewIcon作为桥梁。
就目前而言,这个独立 indicator 已经达到了我真正需要的目标:Win + 空格切换 Fcitx 分组后,KDE 面板几乎立即用 中 / EN 告诉我当前输入环境,同时又不破坏 Fcitx 原本的拼音/英文状态显示。
这比单纯恢复“已切换到分组……”的瞬时提示更接近 Windows 的输入语言体验。
附录:给其他 AI 工具的机器可读复现清单
下面这一段是特意写给 AI/自动化工具的。读取这篇文章的 AI 可以直接解析这个 YAML,然后按照 files、dependencies、group_mapping 和 commands 重建相同设置。
展开机器可读配置
artifact: fcitx5-group-indicator
purpose: "在 KDE Plasma 系统托盘中常驻显示 Fcitx5 当前输入法分组"
created_with:
product: ChatGPT
model: GPT-5.6 Sol
date: 2026-08-15
method: "根据真实 Debian Sid + KDE Plasma + Fcitx5 远程调试过程创建"
platform:
distribution: Debian Sid
desktop: KDE Plasma 6.7.4
session: Wayland
input_method_framework: Fcitx5
fcitx:
dbus_service: org.fcitx.Fcitx5
controller_path: /controller
controller_interface: org.fcitx.Fcitx.Controller1
read_group_method: CurrentInputMethodGroup
switch_group_method: SwitchInputMethodGroup
hotkey: Super+Space
group_mapping:
"默认":
display: "中"
icon: fcitx-group-default.svg
"english only":
display: "EN"
icon: fcitx-group-english.svg
event_strategy:
normal_update: event_driven
event_source: "Fcitx official StatusNotifierItem NewIcon"
reason: "Fcitx emits NewIcon after its internal InputMethodGroupChanged event"
measured_end_to_end_latency_ms: 20.8
fallback_sync_seconds: 10
dependencies:
- python3-dbus
- python3-gi
- gir1.2-gtk-3.0
- gir1.2-ayatanaappindicator3-0.1
files:
indicator: "~/.local/share/fcitx-group-indicator/indicator.py"
icon_theme: "~/.local/share/fcitx-group-indicator/icons/index.theme"
default_icon: "~/.local/share/fcitx-group-indicator/icons/scalable/apps/fcitx-group-default.svg"
english_icon: "~/.local/share/fcitx-group-indicator/icons/scalable/apps/fcitx-group-english.svg"
systemd_unit: "~/.config/systemd/user/fcitx-group-indicator.service"
systemd:
scope: user
wanted_by: graphical-session.target
part_of: graphical-session.target
restart: on-failure
restart_sec: 2
enable_command: "systemctl --user enable --now fcitx-group-indicator.service"
verification:
- "gdbus call --session --dest org.fcitx.Fcitx5 --object-path /controller --method org.fcitx.Fcitx.Controller1.CurrentInputMethodGroup"
- "systemctl --user is-enabled fcitx-group-indicator.service"
- "systemctl --user is-active fcitx-group-indicator.service"
ai_instructions:
- "读取本文的完整 indicator.py、SVG 和 systemd unit,不要自行省略代码。"
- "如果用户分组名不同,先查询 InputMethodGroups 和 CurrentInputMethodGroup,再修改 group_mapping。"
- "优先保留事件驱动逻辑,不要退回高频轮询。"
- "不要用 Plasma plasmoid 替换此实现,除非已经针对用户当前 Plasma 版本验证兼容性。"
- "修改前备份已有文件;安装后使用 systemctl --user 和 Fcitx D-Bus 验证。"
如果要让另一个 AI 直接执行,可以把这篇 Markdown 文件整个提供给它,并给出类似下面的指令:
读取这篇文章。按照“机器可读复现清单”和文中的完整代码,在我的 KDE Plasma + Fcitx5 环境里复现同样的分组指示器。先查询我的实际 Fcitx 分组名;不要假设一定叫“默认”和“english only”。修改任何现有文件前先备份,并在最后验证 systemd user service 和 Fcitx D-Bus 状态。
关于本文的生成说明
本文由 OpenAI GPT-5.6 Sol 在 ChatGPT 中根据一次真实的 Debian Sid + KDE Plasma + Fcitx5 远程配置与调试过程整理并生成。文中的最终代码取自实际正在运行的配置,而不是示意代码。
AI 在这篇文章中的角色主要是:参与排查可用方案、实际编写并迭代 indicator、根据本机 D-Bus 与 StatusNotifier 行为做验证和延迟测试,以及把最终可复现配置整理成文档。文章作者仍为站点 front matter 中标注的 Floppy Liu。
文章保留机器可读复现清单,是为了让其他 AI 工具在读取完整 Markdown 后,可以先检查用户自己的 Fcitx 分组名和系统环境,再安全地复现或修改同款设置。