Restore linked input settings and attribute test notifications to Fcitx

This commit is contained in:
David Heinemeier Hansson committed 2026-10-09 13:44:08 +02:00
1 parent e0e6225fac
commit 32d81c76c6
5 files changed
+298 -22

No files matched your search

@@ -0,0 +1,30 @@
"""Select notifications sent by Fcitx, including addons with custom app names."""
from pathlib import Path
import re
import sys
def input_notifications(text, initial_owner):
calls = re.split(r"(?m)^(?=(?:method call|signal|method return|error)\b)", text)
owners = {initial_owner}
for call in calls:
if "interface=org.freedesktop.DBus; member=NameOwnerChanged" in call.split("\n", 1)[0]:
names = re.findall(r'^\s*string "([^"]*)"', call, re.MULTILINE)
if len(names) == 3 and names[0] == "org.fcitx.Fcitx5":
owners.update(names[1:])
owners.discard("")
for call in calls:
header = call.split("\n", 1)[0]
if "interface=org.freedesktop.Notifications; member=Notify" not in header:
continue
sender = re.search(r"\bsender=(\S+)", header)
if sender and sender[1] in owners:
yield call
if __name__ == "__main__":
found = list(input_notifications(Path(sys.argv[1]).read_text(errors="replace"), sys.argv[2]))
if found:
print("\n".join(found))
raise SystemExit(1 if found else 0)
+85
View File
@@ -0,0 +1,85 @@
"""Snapshot test-owned config paths, including targets of nested symlinks."""
import filecmp
import json
from pathlib import Path
import shutil
import sys
def copy(source, destination):
if source.is_symlink():
destination.symlink_to(source.readlink())
elif source.is_dir():
shutil.copytree(source, destination, symlinks=True)
elif source.exists():
shutil.copy2(source, destination)
def backup(directory, paths):
directory.mkdir()
recorded = {}
def capture(path):
path = path.absolute()
if str(path) in recorded:
return
name = str(len(recorded))
recorded[str(path)] = name
copy(path, directory / name)
if not path.parent.exists():
capture(path.parent)
# Setup writes to the resolved target; intermediate links are unchanged.
resolved = path.resolve()
if resolved != path:
capture(resolved)
if path.is_dir() and not path.is_symlink():
for child in path.rglob("*"):
if child.is_symlink():
capture(child)
for path in paths:
capture(path)
(directory / "paths.json").write_text(json.dumps(recorded))
def unchanged(saved, original):
if saved.is_symlink() or original.is_symlink():
return saved.is_symlink() and original.is_symlink() and saved.readlink() == original.readlink()
if not saved.exists() or not original.exists():
return not saved.exists() and not original.exists()
if saved.stat().st_mode != original.stat().st_mode:
return False
if saved.is_file() and original.is_file():
return filecmp.cmp(saved, original, shallow=False)
if saved.is_dir() and original.is_dir():
names = {child.name for child in saved.iterdir()}
return names == {child.name for child in original.iterdir()} and all(
unchanged(saved / name, original / name) for name in names
)
return False
def restore(directory):
for original, name in json.loads((directory / "paths.json").read_text()).items():
path = Path(original)
saved = directory / name
if unchanged(saved, path):
continue
if path.is_symlink() or path.is_file():
path.unlink()
elif path.is_dir():
shutil.rmtree(path)
if saved.exists() or saved.is_symlink():
path.parent.mkdir(parents=True, exist_ok=True)
copy(saved, path)
if __name__ == "__main__":
action, directory, *paths = sys.argv[1:]
if action == "backup":
backup(Path(directory), [Path(path) for path in paths])
elif action == "restore":
restore(Path(directory))
else:
raise SystemExit("Expected backup or restore")
+34 -22
View File
@@ -19,36 +19,42 @@ fi
pass "installer input selection reaches the first desktop session"
work=$(mktemp -d)
dbus-monitor --session "type='method_call',interface='org.freedesktop.Notifications',member='Notify'" > "$ARTIFACTS/input-method-notifications.log" 2>&1 &
notification_monitor=$!
config_home=${XDG_CONFIG_HOME:-$HOME/.config}
data_home=${XDG_DATA_HOME:-$HOME/.local/share}
paths=("$config_home/fcitx5" "$config_home/fontconfig/conf.d/50-omarchy-input-method.conf" "$data_home/dbus-1/services/org.fcitx.Fcitx5.service")
for (( i = 0; i < ${#paths[@]}; i++ )); do
if [[ -e ${paths[i]} || -L ${paths[i]} ]]; then
cp -a "${paths[i]}" "$work/backup-$i"
fi
done
python "$ROOT/test/acceptance.d/input-method-state.py" backup "$work/state" "${paths[@]}"
notification_monitor=""
cleanup() {
kill "$notification_monitor" 2>/dev/null || true
wait "$notification_monitor" 2>/dev/null || true
local status=$?
if [[ -n $notification_monitor ]]; then
kill "$notification_monitor" 2>/dev/null || true
wait "$notification_monitor" 2>/dev/null || true
fi
close_windows '^org\.omarchy\.ime-test$'
close_windows '^org\.omarchy\.ime-test-gtk$|^org\.qt-project\.qml$|^chrome-.*_omarchy-ime-entry\.html-Default$'
systemctl --user stop omarchy-fcitx5.service
for (( i = 0; i < ${#paths[@]}; i++ )); do
rm -rf "${paths[i]}"
if [[ -e $work/backup-$i || -L $work/backup-$i ]]; then
mkdir -p "$(dirname "${paths[i]}")"
cp -a "$work/backup-$i" "${paths[i]}"
fi
done
busctl --user call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus ReloadConfig >/dev/null
systemctl --user start omarchy-fcitx5.service
rm -rf "$work"
systemctl --user stop omarchy-fcitx5.service || status=1
python "$ROOT/test/acceptance.d/input-method-state.py" restore "$work/state" || status=1
busctl --user call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus ReloadConfig >/dev/null || status=1
systemctl --user start omarchy-fcitx5.service || status=1
if (( status == 0 )); then
rm -rf "$work"
else
printf 'Input test snapshot retained at %s\n' "$work/state" >&2
fi
exit "$status"
}
trap cleanup EXIT
dbus-monitor --session "type='method_call',interface='org.freedesktop.Notifications',member='Notify'" \
"type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.fcitx.Fcitx5'" > "$ARTIFACTS/input-method-notifications.log" 2>&1 &
notification_monitor=$!
monitor_ready() {
kill -0 "$notification_monitor" && grep -q 'member=NameAcquired' "$ARTIFACTS/input-method-notifications.log"
}
wait_until "notification monitor attaches" 10 monitor_ready
initial_owner=$(busctl --user call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus GetNameOwner s org.fcitx.Fcitx5 | cut -d '"' -f 2)
cat > "$work/reader" <<'SH'
#!/bin/bash
IFS= read -r text
@@ -97,7 +103,8 @@ HTML
compose() {
fcitx5-remote -s "$method"
fcitx5-remote -c
wtype -M ctrl -M shift -k space -m shift -m ctrl
# Use the controller for composition; wtype cannot prove global shortcuts.
fcitx5-remote -o
case "$method" in
mozc) wtype "nihongo"; wtype -k space ;;
hangul) wtype "gksrmf" ;;
@@ -168,7 +175,12 @@ for method in mozc hangul pinyin chewing; do
done
done
if grep -q 'member=Notify' "$ARTIFACTS/input-method-notifications.log"; then
# Stop and drain the monitor before reading a complete final call.
kill -0 "$notification_monitor" 2>/dev/null || fail "notification monitor stays attached"
kill "$notification_monitor" 2>/dev/null || true
wait "$notification_monitor" 2>/dev/null || true
notification_monitor=""
if ! python "$ROOT/test/acceptance.d/input-method-notifications.py" "$ARTIFACTS/input-method-notifications.log" "$initial_owner"; then
fail "input setup and composition produce no additional notifications" "$(cat "$ARTIFACTS/input-method-notifications.log")"
fi
pass "all four input engines remain quiet during setup and composition"