diff --git a/bin/omarchy-restart-xcompose b/bin/omarchy-restart-xcompose index c0676b1c..db65bd4e 100755 --- a/bin/omarchy-restart-xcompose +++ b/bin/omarchy-restart-xcompose @@ -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 diff --git a/default/hypr/autostart.lua b/default/hypr/autostart.lua index dc192b0a..7d180a46 100644 --- a/default/hypr/autostart.lua +++ b/default/hypr/autostart.lua @@ -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")) diff --git a/default/systemd/user/omarchy-fcitx5.service b/default/systemd/user/omarchy-fcitx5.service new file mode 100644 index 00000000..672c67bf --- /dev/null +++ b/default/systemd/user/omarchy-fcitx5.service @@ -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 diff --git a/docs/file-layout.md b/docs/file-layout.md index 4f09cab0..f481d229 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -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. diff --git a/install/user/first-run/enable-user-units.sh b/install/user/first-run/enable-user-units.sh index a5d7cb8e..2af44e88 100755 --- a/install/user/first-run/enable-user-units.sh +++ b/install/user/first-run/enable-user-units.sh @@ -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 diff --git a/migrations/1785167800.sh b/migrations/1785167800.sh new file mode 100644 index 00000000..1c755e97 --- /dev/null +++ b/migrations/1785167800.sh @@ -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 diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 14ab4b30..566af035 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -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"), diff --git a/test/shell.d/systemd-test.sh b/test/shell.d/systemd-test.sh index af1654d7..7bce6240 100755 --- a/test/shell.d/systemd-test.sh +++ b/test/shell.d/systemd-test.sh @@ -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"