Merge pull request #6398 from basecamp/supervise-fcitx5

Supervise fcitx5 so XCompose sequences can't silently die
This commit is contained in:
David Heinemeier Hansson
2026-07-27 09:51:02 -07:00
committed by GitHub
8 changed files with 110 additions and 4 deletions
+6 -1
View File
@@ -2,4 +2,9 @@
# omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings.
omarchy-restart-app fcitx5 --disable notificationitem
# A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's
# instance would exit on arrival and the stale process would keep serving the
# old ~/.XCompose -- a restart that reports success and changes nothing.
systemctl --user stop omarchy-fcitx5.service
pkill -x fcitx5 >/dev/null 2>&1 || true
systemctl --user start omarchy-fcitx5.service
-1
View File
@@ -4,7 +4,6 @@ hl.on("hyprland.start", function()
hl.exec_cmd("dbus-update-activation-environment --systemd --all")
hl.exec_cmd("quickshell -n -p $OMARCHY_PATH/shell")
hl.exec_cmd(o.launch("fcitx5 --disable notificationitem"))
hl.exec_cmd("omarchy-first-run")
hl.exec_cmd("omarchy-powerprofiles-init")
hl.exec_cmd(o.launch("omarchy-hyprland-monitor-watch"))
@@ -0,0 +1,31 @@
[Unit]
Description=Fcitx5 input method (XCompose sequences)
# fcitx5 turns the CapsLock compose sequences in ~/.XCompose into text for
# Wayland clients.
#
# Wait for the compositor: fcitx5 needs WAYLAND_DISPLAY and DISPLAY, which uwsm
# imports into the user manager before it reaches graphical-session.target.
After=graphical-session.target
# The wayland connection dies with the compositor, so follow the session rather
# than linger against a socket that is gone.
PartOf=graphical-session.target
# After= is ordering only -- it does not stop anything from starting this unit
# while the target is inactive. An `omarchy update` over SSH has a live user
# manager (pam_systemd) and no graphical session, and a fcitx5 started there
# comes up with no WAYLAND_DISPLAY and no way to reach any client. Worse, it
# stays active, so the later graphical-session.target activation won't pull in a
# working one -- Wants= does not restart what is already running. Skip the start
# instead; the unit stays enabled and starts for real at graphical login.
ConditionEnvironment=WAYLAND_DISPLAY
[Service]
Type=simple
# notificationitem duplicates the tray entry omarchy already renders itself.
ExecStart=/usr/bin/fcitx5 --disable notificationitem
# always, not on-failure: fcitx5 exits 0 when it detects another instance owning
# its bus name, and a clean exit still leaves the user with no input method.
Restart=always
RestartSec=2
[Install]
WantedBy=graphical-session.target
+2 -1
View File
@@ -228,7 +228,8 @@ systemd instance:
Voxtype post-update hook.
- `install/user/first-run/enable-user-units.sh``systemctl --user enable`
the shipped user units (`bt-agent`, `omarchy-sleep-lock`,
`omarchy-recover-internal-monitor`, `omarchy-migrate-notify.service`).
`omarchy-recover-internal-monitor`, `omarchy-migrate-notify.service`,
`omarchy-fcitx5.service`).
Done here, not at finalize, because
the user manager isn't reachable from the ISO chroot; `ConditionPath*`
in the unit files keeps services inert when they don't apply.
+2 -1
View File
@@ -16,4 +16,5 @@ systemctl --user enable --now \
bt-agent.service \
omarchy-recover-internal-monitor.service \
omarchy-sleep-lock.service \
omarchy-migrate-notify.service
omarchy-migrate-notify.service \
omarchy-fcitx5.service
+38
View File
@@ -0,0 +1,38 @@
echo "Supervise fcitx5 so CapsLock compose sequences can't silently die"
# fcitx5 was launched fire-and-forget from Hyprland's autostart. Nothing
# restarted it and nothing noticed when it went away, so a single lost start
# left every ~/.XCompose sequence (CapsLock m s -> emoji) dead for the rest of
# the session. It's a systemd user service with Restart=always now.
systemctl --user daemon-reload >/dev/null 2>&1 || true
# Enable without --now, and start by hand further down only when there is a
# session to start into. `systemctl enable` needs a live user manager, which an
# `omarchy update` from a TTY does not have, so fall back to writing exactly the
# symlink it would have written rather than silently leaving this unenabled.
if ! systemctl --user enable omarchy-fcitx5.service >/dev/null 2>&1; then
wants_dir="$HOME/.config/systemd/user/graphical-session.target.wants"
mkdir -p "$wants_dir"
ln -sfn /usr/lib/systemd/user/omarchy-fcitx5.service \
"$wants_dir/omarchy-fcitx5.service"
fi
# Outside a graphical session -- an update over SSH -- there is nothing to hand
# over: no autostart-launched fcitx5 to kill, and the unit's own
# ConditionEnvironment would skip the start anyway. The enablement above is the
# whole job; the next graphical login starts it.
if systemctl --user is-active --quiet graphical-session.target; then
# The service and the autostart-launched process can't coexist: a second
# fcitx5 sees the first owning org.fcitx.Fcitx5 and exits 0, which
# Restart=always would turn into a restart loop. Drop the stray first.
pkill -x fcitx5 >/dev/null 2>&1 || true
# Report what systemctl actually said. This kills a fcitx5 that was working a
# moment ago, so a start failure here has to be loud instead of leaving the
# session with no input method and a migration marked complete.
if ! error=$(systemctl --user start omarchy-fcitx5.service 2>&1); then
echo "Could not start omarchy-fcitx5.service: $error"
echo "Compose sequences (CapsLock m s) will not work until the next login."
fi
fi
+1
View File
@@ -139,6 +139,7 @@ package_defaults = [
("default/systemd/user/omarchy-recover-internal-monitor.service", "/usr/lib/systemd/user/omarchy-recover-internal-monitor.service", "systemd/user/omarchy-recover-internal-monitor.service"),
("default/systemd/user/omarchy-migrate-notify.service", "/usr/lib/systemd/user/omarchy-migrate-notify.service", "systemd/user/omarchy-migrate-notify.service"),
("default/systemd/user/omarchy-tailscale-receive.service", "/usr/lib/systemd/user/omarchy-tailscale-receive.service", "systemd/user/omarchy-tailscale-receive.service"),
("default/systemd/user/omarchy-fcitx5.service", "/usr/lib/systemd/user/omarchy-fcitx5.service", "systemd/user/omarchy-fcitx5.service"),
("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"),
("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"),
+30
View File
@@ -46,3 +46,33 @@ grep -F 'omarchy-migrate-notify.service' "$first_run_units" >/dev/null ||
grep -F 'omarchy-update-user-notify' "$first_run_units" >/dev/null &&
fail "first-run still enables the retired notifier units"
pass "first-run enables the login-only migration notifier"
fcitx_service="$ROOT/default/systemd/user/omarchy-fcitx5.service"
grep -Fx 'ExecStart=/usr/bin/fcitx5 --disable notificationitem' "$fcitx_service" >/dev/null
grep -Fx 'Restart=always' "$fcitx_service" >/dev/null ||
fail "fcitx5 exits 0 on a duplicate bus name, so on-failure would leave the user with no input method"
grep -Fx 'After=graphical-session.target' "$fcitx_service" >/dev/null ||
fail "fcitx5 needs WAYLAND_DISPLAY, which uwsm imports before it reaches graphical-session.target"
grep -Fx 'PartOf=graphical-session.target' "$fcitx_service" >/dev/null ||
fail "fcitx5 must stop with the compositor instead of lingering against a dead wayland socket"
grep -Fx 'WantedBy=graphical-session.target' "$fcitx_service" >/dev/null ||
fail "fcitx5 is never pulled in at login without a WantedBy"
grep -Fx 'ConditionEnvironment=WAYLAND_DISPLAY' "$fcitx_service" >/dev/null ||
fail "an update over SSH has a live user manager and no display; starting fcitx5 there wedges the unit active-but-blind, and Wants= will not replace it at graphical login"
fcitx_migration="$ROOT/migrations/1785167800.sh"
grep -F 'is-active --quiet graphical-session.target' "$fcitx_migration" >/dev/null ||
fail "migration kills fcitx5 and starts the unit outside a graphical session"
grep -F 'systemctl --user enable omarchy-fcitx5.service' "$fcitx_migration" >/dev/null ||
fail "migration must enable without --now; --now starts the unit before the session-gate check"
grep -F 'Could not start omarchy-fcitx5.service' "$fcitx_migration" >/dev/null ||
fail "migration pkills a working fcitx5, so a failed handover must be reported instead of marked complete"
grep -F 'pkill -x fcitx5' "$ROOT/bin/omarchy-restart-xcompose" >/dev/null ||
fail "restart-xcompose cannot reload a fcitx5 running outside the unit, so it silently keeps serving the old table"
grep -F 'omarchy-fcitx5.service' "$first_run_units" >/dev/null ||
fail "first-run does not enable the input method, so ~/.XCompose sequences never resolve"
grep -F 'fcitx5' "$ROOT/default/hypr/autostart.lua" >/dev/null &&
fail "fcitx5 is autostarted from Hyprland; an unsupervised launch dies silently and takes every compose sequence with it"
pass "fcitx5 runs supervised, so a lost input method comes back instead of killing XCompose until logout"