When the mouse touches the border of a virtual desktop, my long-time preference (since fvwm) is to have it to seamlessly tunel to the contiguous virtual one. Problem is, with KDE (on Plasma 6 (X11)), sometimes this stops working: the mouse gets stuck in its current screen, no more escape from reality into a parallel universe!
I then have to reset the functionality with a keyboard scroll or with a zoom-in/zoom-out of the whole desktop, which is time consuming ($\approx$ a second $\times$ how many time this happens) but, more importantly, is also traumatizing as the brain starts to associate this disruption with whatever causes it, that you don't realize, but your brain does, in subconscious bursts of infinitesimal pain and micro-solitude.
I tried, oh I did try, to find a solution, several times in the previous years. It's been an old bug, documented, e.g., as Bug 496251, but not always taken seriously. This exchange in Bug 453040 is funny:
... STEPS TO REPRODUCE 2. Just use the computer for many days, it triggers within something like 180 days (this run lasted 49 days). ...
Reply from a maintainer:
Sorry this is not actionable or testable. No developer will run their PC for 180 days and there seems to be workaround - reboot.
Fair enough.
I tried to assess the situation with Claude Opus, but this didn't work, then with Claude Fable, which started to improve but this was quickly becoming a hassle with many things to do, so I asked it to pass it to Claude Code and it produced this handoff for me to hand it over to its machine-access counterpart:
kwin_x11), multi-monitor (laptop + external main display). User: laussy, home /home/laussy.
qdbus org.kde.KWin /KWin reconfigure does NOT cure it (even with a forced config delta). Edge reservation is intact; failure is downstream.ElectricBorderMaximize / ElectricBorderTiling were already false — tiling-contention hypothesis is DEAD. desktop switch; (c) **wmctrl -r :ACTIVE: -b toggle,maximized_vert — a single
run cures it** (tested; note it leaves the window state toggled).
interactiveMoveResizeFinished (withclientFinishUserMovedResizedlegacy fallback) DOES load and fire on this system (v1.2 confirmed via journal). BUT an internalframeGeometry±1px nudge from the script does NOT cure. Conclusion: the cure must arrive via the X11 event path (external client message), not KWin-internal calls.
console.warn, not print (filtered out). Journal: journalctl --user -u plasma-kwin_x11 -f.
qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript edge-rearm qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript /home/laussy/.local/share/kwin/scripts/edge-rearm/contents/code/main.js edge-rearm qdbus org.kde.KWin /Scripting org.kde.kwin.Scripting.start
windowsize has no --relative option. Use getwindowgeometry --shell + absolute sizes.
After every interactive (mouse) resize, automatically fire an X-side cure so the user never has to run anything manually. Architecture already designed:
KWin script (hook on resize-finished) → callDBus → kglobalaccel
invokeShortcut → custom shortcut running an external cure script.
Candidate A (invisible if it works): pure ConfigureRequest resize
#!/bin/bash
# edge-unstick.sh — v2.0
eval "$(xdotool getactivewindow getwindowgeometry --shell)"
xdotool windowsize "$WINDOW" $((WIDTH + 1)) "$HEIGHT"
xdotool windowsize "$WINDOW" "$WIDTH" "$HEIGHT"
Candidate B (state-neutral double toggle — verify double still cures, single is proven):
wmctrl -r :ACTIVE: -b toggle,maximized_vert
wmctrl -r :ACTIVE: -b toggle,maximized_vert
Test protocol per candidate: mouse-resize a window (breaks edges) → run candidate
→ push pointer to screen edge → does desktop switch? Pick the least intrusive
curing variant, install as /home/laussy/bin/edge-unstick.sh, chmod +x.
/home/laussy/bin/edge-unstick.sh, name edge-unstick, assign any key (kept as manual backup).
qdbus org.kde.kglobalaccel | grep -i component qdbus org.kde.kglobalaccel /component/<found> org.kde.kglobalaccel.Component.invokeShortcut "edge-unstick"
(break edges first, confirm this call cures).
~/.local/share/kwin/scripts/edge-rearm/contents/code/main.js (v1.3+; adjust
COMPONENT to T2's discovery):
// edge-rearm v1.3 — KDE bug 496251 workaround (X11)
var COMPONENT = "/component/edge_unstick"; // <-- adjust
function cure() {
callDBus("org.kde.kglobalaccel", COMPONENT,
"org.kde.kglobalaccel.Component", "invokeShortcut",
"edge-unstick");
console.warn("edge-rearm: external cure invoked");
}
function hook(w) {
if (!w) { return; }
if (w.interactiveMoveResizeFinished !== undefined) {
w.interactiveMoveResizeFinished.connect(cure);
} else if (w.clientFinishUserMovedResized !== undefined) {
w.clientFinishUserMovedResized.connect(cure);
}
}
workspace.windowList().forEach(hook);
workspace.windowAdded.connect(hook);
console.warn("edge-rearm v1.3 loaded");
Load with the dev loop from fact 6. End-to-end test: mouse resize → journal shows "external cure invoked" → edge switching works immediately.
loading (metadata.json validation: kpackagetool6 --type KWin/Script --show)
or add the loadScript sequence to an autostart script.
registered; inspect with qdbus org.kde.kglobalaccel /component/<x> org.kde.kglobalaccel.Component.shortcutNames.
kwin_x11 --replace (slow, buggy here) or Overview/Alt+E (slow) — those are the rituals this project replaces.edge-unstick.sh (versioned header).main.js (versioned header) + metadata.json that loads via kpackagetool.And this already did something: it worked better, or more, or at least differently. We identified that resizing was easily, almost systematically breaking, and the patches were working some of the times. But still not *all the time*. So we entered in a loooong process of debuging, testing no less than 11 alternative routes (that it called "cures"), all of them a few minutes for Claude Code, meaning half an hour or more for me if I'd had to do that myself, even merely following instructions.
I took it as an exercise and exploration of the limits of the system and of our tandem to see how AI (at the time of writing) could get to the bottom of it, or if we'd have to give up.
It's a subtle thing, apparently, and we failed to pinpoint what exactly causes it, but we could find ways to solve it: zooming-in and out—this I already knew, that's what I do—but also clicking on the keyboard-layout icon, flashing a popup, etc. So it just became a game of: let's find a fix that's convenient enough. Another chunk of the afternoon went into it.
It did try a lot of things, like resizing, nudging window geometry, sending signals, bypassing the keyboard, xdotooling it, wmctrl desktop switches, D-Bus layout switches, toggling keep-above, disabling the edge barrier, etc., lots of cures: to no avail.
It was coding, downloading, analyzing. I was looking, testing, suggesting, deciding whenver it came to which route to go for.
We eventually settled with the following solution, which doesn't identify the cause, but cures the ailment: the cure isn't input and isn't focus—it's a window being mapped. So the fix is a KWin script that, after every move/resize, spawns a throwaway X client which maps and instantly destroys a 1×1, off-screen, override-redirect window. Override-redirect means KWin never decorates or animates it—so it's completely invisible and costs ~60 ms. Edges come back every time, hands-free, with nothing on screen.
It produced the following artifacts:
Script directory:
/home/laussy/.local/share/kwin/scripts/edge-rearm/
Contents:
~/.local/share/kwin/scripts/edge-rearm/
├── metadata.json # plugin manifest (Id: edge-rearm, EnabledByDefault)
└── contents/
└── code/
└── main.js # the logic (fires on interactiveMoveResizeFinished)
So the actual code file is:
/home/laussy/.local/share/kwin/scripts/edge-rearm/contents/code/main.js
And it's activated by the line edge-rearmEnabled=true under [Plugins] in /home/laussy/.config/kwinrc.
For completeness, the full set of files that make up the working solution:
| Path | Role |
|---|---|
~/.local/share/kwin/scripts/edge-rearm/contents/code/main.js |
KWin script — calls the cure on every move/resize |
~/.local/share/kwin/scripts/edge-rearm/metadata.json |
KWin script manifest |
~/bin/edge-unstick.sh |
Dispatcher (CURE=INVIS)
|
~/bin/xnudge.py |
The invisible override-redirect window helper (the actual cure) |
~/.local/share/applications/net.local.edgeunstick.desktop |
Command-shortcut definition (the _launch D-Bus target)
|
~/.config/kglobalshortcutsrc |
Binds Alt+R to _launch
|
~/.config/kwinrc → [Plugins] edge-rearmEnabled=true |
Enables the script |
#!/usr/bin/env python3
# xnudge.py — KDE bug 496251 discreet cure.
# Creates a brand-new X client and briefly maps a 1x1 OVERRIDE-REDIRECT window
# (the unmanaged kind KWin never decorates or animates), then destroys it.
# A new mapped surface is the confirmed cure class for the dead-edge bug, but
# unlike a Plasma notification this has NO animation and NO visible flash.
# Pure X protocol via ctypes/libX11 — NO input injection (safe re French layout).
import ctypes, sys, time
x = ctypes.CDLL("libX11.so.6")
x.XOpenDisplay.restype = ctypes.c_void_p
x.XOpenDisplay.argtypes = [ctypes.c_char_p]
x.XDefaultRootWindow.restype = ctypes.c_ulong
x.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
x.XCreateSimpleWindow.restype = ctypes.c_ulong
x.XCreateSimpleWindow.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int,
ctypes.c_int, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint,
ctypes.c_ulong, ctypes.c_ulong]
class XSetWindowAttributes(ctypes.Structure):
_fields_ = [
("background_pixmap", ctypes.c_ulong), ("background_pixel", ctypes.c_ulong),
("border_pixmap", ctypes.c_ulong), ("border_pixel", ctypes.c_ulong),
("bit_gravity", ctypes.c_int), ("win_gravity", ctypes.c_int),
("backing_store", ctypes.c_int), ("backing_planes", ctypes.c_ulong),
("backing_pixel", ctypes.c_ulong), ("save_under", ctypes.c_int),
("event_mask", ctypes.c_long), ("do_not_propagate_mask", ctypes.c_long),
("override_redirect", ctypes.c_int), ("colormap", ctypes.c_ulong),
("cursor", ctypes.c_ulong)]
CWOverrideRedirect = 1 << 9
dpy = x.XOpenDisplay(None)
if not dpy:
sys.exit("cannot open display")
root = x.XDefaultRootWindow(dpy)
# 1x1 window parked off the visible area.
win = x.XCreateSimpleWindow(dpy, root, -10, -10, 1, 1, 0, 0, 0)
attrs = XSetWindowAttributes()
attrs.override_redirect = 1
x.XChangeWindowAttributes(dpy, ctypes.c_ulong(win), CWOverrideRedirect, ctypes.byref(attrs))
x.XMapWindow(dpy, ctypes.c_ulong(win))
x.XFlush(dpy)
x.XSync(dpy, False)
# Let the map settle through KWin's input/stacking pass, then tear down.
time.sleep(0.05)
x.XUnmapWindow(dpy, ctypes.c_ulong(win))
x.XDestroyWindow(dpy, ctypes.c_ulong(win))
x.XFlush(dpy)
x.XSync(dpy, False)
x.XCloseDisplay(dpy)
#!/bin/bash
# edge-unstick.sh — v5.0 (KDE bug 496251 cure)
#
# After a mouse move/resize, edge-triggered virtual-desktop switching (and edge
# scroll) dies until a NEW on-screen client appears. Invoked automatically by the
# edge-rearm KWin script on every move/resize (and bound to a manual shortcut).
#
# EMPIRICALLY ESTABLISHED (2026-07-19):
# - Shortcut keypress alone does NOT cure (kglobalacceld eats the key event).
# - A new on-screen CLIENT cures. A Plasma notification cures but is loud
# (slides in, animates, min display time -> disruptive on every resize).
# - So the default cure is INVIS: a brand-new X client that briefly maps a 1x1
# OVERRIDE-REDIRECT window (unmanaged -> KWin never animates it) via
# ~/bin/xnudge.py. No flash, no animation, instant.
#
# NO xdotool anywhere — see memory avoid-xdotool-french-layout.
#
# CURE: INVIS = invisible override-redirect window (discreet; default)
# NOTIFY = brief notification toast (proven cure, but visible/animated)
CURE=INVIS
# Log every firing (file only; not a window, cannot itself affect the bug).
printf '%s fired (%s)\n' "$(date '+%F %T')" "$CURE" \
>>/home/laussy/.local/share/edge-unstick.log 2>&1
case "$CURE" in
INVIS)
python3 /home/laussy/bin/xnudge.py >/dev/null 2>&1
;;
NOTIFY)
notify-send -t 900 -u low "edge re-armed" >/dev/null 2>&1
;;
esac
// edge-rearm — KDE bug 496251 workaround (X11).
// After every interactive move/resize, invoke the command shortcut that spawns
// a throwaway X client (xnudge.py). A brand-new window MAP re-arms the screen
// edges: the cure is neither input nor focus — just a map event reaching KWin's
// event loop. (Earlier "stuck-Alt / inject key-releases" theory was disproved.)
//
// Cure path: invoke the Plasma-6 command shortcut backed by
// ~/.local/share/applications/net.local.edgeunstick.desktop
// (component /component/net_local_edgeunstick_desktop, action "_launch").
var COMPONENT = "/component/net_local_edgeunstick_desktop";
var ACTION = "_launch";
function cure() {
callDBus("org.kde.kglobalaccel", COMPONENT,
"org.kde.kglobalaccel.Component", "invokeShortcut", ACTION);
console.warn("edge-rearm: external cure invoked");
}
function hook(w) {
if (!w) { return; }
if (w.interactiveMoveResizeFinished !== undefined) {
w.interactiveMoveResizeFinished.connect(cure);
} else if (w.clientFinishUserMovedResized !== undefined) {
w.clientFinishUserMovedResized.connect(cure);
}
}
workspace.windowList().forEach(hook);
workspace.windowAdded.connect(hook);
console.warn("edge-rearm loaded");
Finally, the two bits of boilerplate the above depends on, for a self-contained recipe. The KWin script won't load without its manifest ~/.local/share/kwin/scripts/edge-rearm/metadata.json:
{
"KPackageStructure": "KWin/Script",
"KPlugin": {
"Id": "edge-rearm",
"Name": "edge-rearm",
"Description": "Re-arms edge-triggered desktop switching after a mouse resize (KDE bug 496251 workaround, X11).",
"Authors": [ { "Name": "laussy" } ],
"Version": "1.3",
"License": "MIT",
"EnabledByDefault": true
},
"X-Plasma-API": "javascript",
"X-Plasma-MainScript": "code/main.js"
}
And the command-shortcut that provides the _launch D-Bus target the script calls — the desktop entry ~/.local/share/applications/net.local.edgeunstick.desktop:
[Desktop Entry]
Exec=/home/laussy/bin/edge-unstick.sh
Name=edge-unstick
NoDisplay=true
StartupNotify=false
Type=Application
X-KDE-GlobalAccel-CommandShortcut=true
bound (optionally, for the manual Alt+R backup) by this stanza in ~/.config/kglobalshortcutsrc:
[services][net.local.edgeunstick.desktop]
_launch=Alt+R
I also asked it to make a summary of the likely reason for the problem in the first place, and how much we understood of its actual cause:
Claude (Opus 4.8):
🔍 What's actually going on (and what's still murky)
What we're confident about (empirical): the breakage is triggered by any interactive move/resize, and the one thing that reliably fixes it is a new window being mapped. We ruled out — by direct test — input-injection, focus changes, stacking toggles on existing windows, geometry changes, and every ClientMessage/D-Bus path. The decisive experiment was the invisible override-redirect window: it's not input, it's never focused, it's never seen, it's not even a managed window — yet it cures. So the trigger is narrowed to the window-map event itself passing through KWin's event loop.
Best-guess mechanism (a hypothesis, not confirmed in source): Plasma's screen edges are implemented as input reservations / input-only regions. After an interactive move/resize, those edge reservations appear to be lost or shadowed (e.g. the resized window ends up above them, or they're not re-registered). KWin rebuilds them during its input-window update pass, and a new window map is one of the few things that reliably kicks off that pass — whereas geometry changes, ClientMessages, and consumed hotkeys don't. This lines up with the timing (regression since the 6.1 edge-barrier/input rework) and with why our earlier "nudge the geometry" attempts all failed.
What remains genuinely mysterious: we have not read the KWin source or traced the exact function that drops and rebuilds the reservation, so we can't name the offending code path. We also don't know why an interactive move/resize specifically corrupts the reservation while other geometry changes don't. So this is a solid behavioral characterization with a strong mechanistic hypothesis — not a proven root cause. That's exactly the honest framing to keep in the bug report: we can tell the maintainers precisely what re-arms it (a window map → input-window update), which should let someone who knows the edge code find the why quickly.
One caveat worth keeping in mind: you noted "to the best of my practice so far" — the invisible cure has only had light testing. If you ever catch an edge staying dead, tap Alt+R (manual backup); and if the invisible window ever proves not enough in some case, flipping CURE=NOTIFY in ~/bin/edge-unstick.sh falls back to the loud-but-proven notification.
From this I gather that if we'd delve into the actual Plasma source code, with more time (also of my time, though), we'd eventually pin it down, while we had to content with a hack here instead. Not a full victory but one good enough, and one which definitely I wouldn't have achieved by myself. I also notice one inaccuracy: the breakage was not triggered by any interactive move/resize, but could be, while resize indeed tends to make it deterministic in some configurations, the exact cause remains mysterious.
Probably in a few weeks or month the AI will be able to identify in a matter of minutes what was the actual bug that took us an entire day to only circumvent. This introspection into a glitch which I'm happy is now (hopefully) gone, was actually more about how to solve a concrete problem with AI, and how to empower yourself with a series of tools you didn't have in the morning. I can now have it crawl my web and fix a lot of problems, by batches! That deserves another, separate post!