From 1e383143df111a33add2b3c254c8552cd566f05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ti=E1=BA=BFn=20Huy?= Date: Tue, 6 Oct 2026 08:36:31 +0700 Subject: [PATCH 1/6] fix: ensure atomic write of replacement token and handle session teardown properly --- src/native_injector.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/native_injector.cpp b/src/native_injector.cpp index f397b52..13b70dd 100644 --- a/src/native_injector.cpp +++ b/src/native_injector.cpp @@ -59,11 +59,16 @@ struct NativeInjector::Impl { const auto path = tokenPath(); std::error_code ec; std::filesystem::create_directories(path.parent_path(), ec); - std::ofstream out(path, std::ios::trunc); + // Write replacement token atomically; a killed Fcitx must not leave a + // zero-length token that looks like a valid restore state. + const auto tmp = path.string() + ".tmp-" + std::to_string(getpid()); + std::ofstream out(tmp, std::ios::trunc); if (out) { out << token << '\n'; out.close(); - chmod(path.c_str(), 0600); + chmod(tmp.c_str(), 0600); + std::filesystem::rename(tmp, path, ec); + if (ec) std::filesystem::remove(tmp, ec); } g_free(token); } @@ -206,7 +211,20 @@ struct NativeInjector::Impl { if (wakeSource) { g_source_destroy(wakeSource); g_source_unref(wakeSource); } if (keyboard) keyboard = ei_device_unref(keyboard); if (connection) connection = ei_unref(connection); - if (session) { xdp_session_close(session); g_object_unref(session); } + if (session) { + // The portal consumes a restore token before starting a restored + // session and writes it back only when Close is delivered. The + // close call is asynchronous; give GLib time to flush it before + // tearing down the portal connection during Fcitx shutdown. + xdp_session_close(session); + const gint64 deadline = g_get_monotonic_time() + 250000; + while (g_get_monotonic_time() < deadline) { + while (g_main_context_pending(context)) + g_main_context_iteration(context, FALSE); + g_usleep(10000); + } + g_object_unref(session); + } if (portal) g_object_unref(portal); g_object_unref(cancellable); g_main_context_pop_thread_default(context); From 3b762a636171bf2d8e2d42aa52fb8efd109d6984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ti=E1=BA=BFn=20Huy?= Date: Tue, 6 Oct 2026 12:27:34 +0700 Subject: [PATCH 2/6] fix: ensure proper daemon reload before starting fcitx5 to avoid session conflicts --- scripts/skey-restart-fcitx5 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/skey-restart-fcitx5 b/scripts/skey-restart-fcitx5 index 4772723..59eb27e 100755 --- a/scripts/skey-restart-fcitx5 +++ b/scripts/skey-restart-fcitx5 @@ -19,7 +19,12 @@ # Redirect BOTH streams: the daemon keeps stdout/stderr open across the # fork, and when this script runs under `systemd-run --wait --pipe` # (postinst), an open pipe FD makes --pipe never see EOF. -fcitx5 -r -d >/dev/null 2>&1 || true +# Ask the existing daemon to reload first. Starting `fcitx5 -r -d` from a +# transient package-update unit can leave a second daemon alive after the unit +# exits; that races the libei portal restore token with the old addon session. +if ! fcitx5-remote -r >/dev/null 2>&1; then + fcitx5 -r -d >/dev/null 2>&1 || true +fi # No Wayland / no KDE Plasma → nothing to reconnect. if [ "${XDG_SESSION_TYPE:-}" != "wayland" ]; then From 5a516f69c87cd087498c9366e2045ba3038034e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ti=E1=BA=BFn=20Huy?= Date: Tue, 6 Oct 2026 12:44:34 +0700 Subject: [PATCH 3/6] fix: update comments for clarity and improve handling of Wayland session reload --- scripts/skey-restart-fcitx5 | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/scripts/skey-restart-fcitx5 b/scripts/skey-restart-fcitx5 index 59eb27e..d4de3ce 100755 --- a/scripts/skey-restart-fcitx5 +++ b/scripts/skey-restart-fcitx5 @@ -26,7 +26,7 @@ if ! fcitx5-remote -r >/dev/null 2>&1; then fcitx5 -r -d >/dev/null 2>&1 || true fi -# No Wayland / no KDE Plasma → nothing to reconnect. +# No Wayland / no KDE Plasma → reload is complete. if [ "${XDG_SESSION_TYPE:-}" != "wayland" ]; then exit 0 fi @@ -48,10 +48,9 @@ for _ in $(seq 1 40); do sleep 0.25 done # Wait for the skey addon to fully register (the last addon to load, -# ~2s after daemon start) before toggling. A fixed 1s margin raced with -# it: toggling KWin while fcitx5 was half-initialized left text-input-v3 -# clients that never re-commit (STerm) dead until app restart, while -# Chromium apps recovered on their own and masked the problem. +# ~2s after daemon start). Do not toggle KWin here: that tears down and +# recreates the Wayland launcher, causing another Fcitx5/libei session while +# the previous portal restore token is still being saved. if [ "$ready" = 1 ]; then for _ in $(seq 1 40); do if [ -n "$("$QBUS" org.fcitx.Fcitx5 /controller \ @@ -61,14 +60,3 @@ if [ "$ready" = 1 ]; then sleep 0.25 done fi - -# Minimal 100ms off window: short enough that apps keep their text input -# registration, long enough for KWin to tear down and re-spawn the -# fcitx5-wayland-launcher against the new instance. -"$QBUS" org.kde.KWin /VirtualKeyboard \ - org.freedesktop.DBus.Properties.Set \ - org.kde.kwin.VirtualKeyboard enabled false 2>/dev/null || true -sleep 0.1 -"$QBUS" org.kde.KWin /VirtualKeyboard \ - org.freedesktop.DBus.Properties.Set \ - org.kde.kwin.VirtualKeyboard enabled true 2>/dev/null || true From 9a06f6889fe45d97da3f5f237d1a8b37d0c04be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ti=E1=BA=BFn=20Huy?= Date: Tue, 6 Oct 2026 13:06:24 +0700 Subject: [PATCH 4/6] fix: streamline fcitx5 restart process and improve Wayland session handling --- scripts/skey-restart-fcitx5 | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/scripts/skey-restart-fcitx5 b/scripts/skey-restart-fcitx5 index d4de3ce..4772723 100755 --- a/scripts/skey-restart-fcitx5 +++ b/scripts/skey-restart-fcitx5 @@ -19,14 +19,9 @@ # Redirect BOTH streams: the daemon keeps stdout/stderr open across the # fork, and when this script runs under `systemd-run --wait --pipe` # (postinst), an open pipe FD makes --pipe never see EOF. -# Ask the existing daemon to reload first. Starting `fcitx5 -r -d` from a -# transient package-update unit can leave a second daemon alive after the unit -# exits; that races the libei portal restore token with the old addon session. -if ! fcitx5-remote -r >/dev/null 2>&1; then - fcitx5 -r -d >/dev/null 2>&1 || true -fi +fcitx5 -r -d >/dev/null 2>&1 || true -# No Wayland / no KDE Plasma → reload is complete. +# No Wayland / no KDE Plasma → nothing to reconnect. if [ "${XDG_SESSION_TYPE:-}" != "wayland" ]; then exit 0 fi @@ -48,9 +43,10 @@ for _ in $(seq 1 40); do sleep 0.25 done # Wait for the skey addon to fully register (the last addon to load, -# ~2s after daemon start). Do not toggle KWin here: that tears down and -# recreates the Wayland launcher, causing another Fcitx5/libei session while -# the previous portal restore token is still being saved. +# ~2s after daemon start) before toggling. A fixed 1s margin raced with +# it: toggling KWin while fcitx5 was half-initialized left text-input-v3 +# clients that never re-commit (STerm) dead until app restart, while +# Chromium apps recovered on their own and masked the problem. if [ "$ready" = 1 ]; then for _ in $(seq 1 40); do if [ -n "$("$QBUS" org.fcitx.Fcitx5 /controller \ @@ -60,3 +56,14 @@ if [ "$ready" = 1 ]; then sleep 0.25 done fi + +# Minimal 100ms off window: short enough that apps keep their text input +# registration, long enough for KWin to tear down and re-spawn the +# fcitx5-wayland-launcher against the new instance. +"$QBUS" org.kde.KWin /VirtualKeyboard \ + org.freedesktop.DBus.Properties.Set \ + org.kde.kwin.VirtualKeyboard enabled false 2>/dev/null || true +sleep 0.1 +"$QBUS" org.kde.KWin /VirtualKeyboard \ + org.freedesktop.DBus.Properties.Set \ + org.kde.kwin.VirtualKeyboard enabled true 2>/dev/null || true From 66d148b9a641c7721274d804403865f574270121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ti=E1=BA=BFn=20Huy?= Date: Tue, 6 Oct 2026 13:45:46 +0700 Subject: [PATCH 5/6] fix: improve Wayland selection handling for Backspace in SKeyState --- src/engine.cpp | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/engine.cpp b/src/engine.cpp index 8926440..02ee062 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -4915,11 +4915,18 @@ void SKeyState::keyEvent(KeyEvent &keyEvent) { unsigned int selEnd = std::max(surrounding.anchor(), surrounding.cursor()); unsigned int deleteSize = selEnd - selStart; - const bool chromiumWaylandSelection = isWayland() && isChromiumCached(); + // GTK's GNOME Text Editor has the same Wayland behaviour as + // Chromium here: a forwarded Backspace first collapses Ctrl+A's + // selection, requiring a second physical Backspace to delete it. + // Send the exact selected range through the surrounding-text API so + // one user Backspace performs one atomic deletion. + const bool nativeWaylandSelection = + isWayland() && + (isChromiumCached() || appProgram() == "gnome-text-editor"); SKEY_DEBUG() << "SurrBS: delete selection size=" << deleteSize - << (chromiumWaylandSelection ? " via surrounding text" - : " via forwardKey"); - if (chromiumWaylandSelection) { + << (nativeWaylandSelection ? " via surrounding text" + : " via forwardKey"); + if (nativeWaylandSelection) { // Chromium Wayland may consume a forwarded Backspace without // applying it to a DOM selection. The surrounding-text request // carries the exact selected range and is handled by the same text @@ -4952,8 +4959,18 @@ void SKeyState::keyEvent(KeyEvent &keyEvent) { // character and cannot clear a multi-character selection. if ((!surrounding.isValid() || surrounding.cursor() == 0) && committedLen_ <= 0) { - SKEY_DEBUG() << "SurrBS: forwardKey (valid=" << surrounding.isValid() - << " cursor=" << surrounding.cursor() << ")"; + // Native Wayland clients can expose the SurroundingText capability + // while publishing no usable snapshot (GTK does this after Ctrl+A). + // forwardKey() is a synthetic fcitx event and is ignored by this + // path, so accepting the event would swallow the user's Backspace. + // Leave it unaccepted and let the compositor deliver the original + // key, preserving application selection deletion. + SKEY_DEBUG() << "SurrBS: pass-through (valid=" << surrounding.isValid() + << " cursor=" << surrounding.cursor() + << (isWayland() ? " wayland" : "") << ")"; + if (isWayland()) { + return; + } ic_->forwardKey(Key(FcitxKey_BackSpace)); keyEvent.filterAndAccept(); return; From bc102e367b098c075de23d6c126cf1c4d418da35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ti=E1=BA=BFn=20Huy?= Date: Tue, 6 Oct 2026 22:55:51 +0700 Subject: [PATCH 6/6] fix: update project version to 0.8.11 in CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 15d9645..5003f58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(fcitx5-skey VERSION 0.8.10) +project(fcitx5-skey VERSION 0.8.11) # Version compiled into the settings GUI / updater. Dev builds append a # suffix (e.g. 0.7.5~dev.123) — project() VERSION must stay purely numeric,