M2: KWin and X11 focus backends, with the KWin script verified live #4
Reference in New Issue
Block a user
Delete Branch "m2/kwin-backend"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What changed
The Linux enforcement path: KWin and X11 focus-detection backends, plus a live
verification of the KWin script that corrected a wrong assumption in the code.
Stacked.
m2/kwin-backendbased onm1/golden-tables. Merge #3, then #2, then #1.Which requirement this covers
x11rb,zbus,nixare named for Linux, so all three are approvedand no new dependency decision was needed.
The KWin script is now verified, and it caught a wrong assumption
A probe script was loaded into the running Plasma 6.6.6 compositor:
The probe was unloaded immediately and
isScriptLoadedconfirmedfalse.The code had asserted that KWin hides the PID from scripts. It does not. That
assertion is now removed, the payload carries
pid, and the daemon can resolve/proc/<pid>/exedirectly instead of inferring an application from its resourceclass. Inferring would have been a guess, and a wrong guess blocks the wrong
application — the worst possible failure for this tool.
Also confirmed:
workspace.windowActivatedexists and fires on real changes, andresourceClass,resourceName,captionandinternalIdare all readable.Still unverified, and said so in the module: the D-Bus leg — whether
callDBusfrom inside the script reaches an exported daemon interface. That halfis ordinary Rust and needs no compositor, so it gets an integration test instead
of another trip into a live session.
Test evidence
Size
D-Bus, X11 and signal handling cost ~870 KB. 2.9 MB remains for the UI
toolkit, which ADR-6 has now settled as egui.
ADR-6 is resolved
The owner chose egui. Recorded with the reasoning, including why the licence
question sits upstream of it: Slint is GPL-3.0 or commercial, so a permissive
project cannot use it. ADR-17 (the licence itself) stays open, and the project
remains unlicensed and unpublished until it is chosen.
Notes for the reviewer
zbus is blocking on
async-io;tokiowas deliberately rejected. A reactorthread is measurable idle cost against a zero-wakeup budget, and several hundred
KB against a 5 MB binary.
No polling anywhere.
callDBusfrom the compositor,poll_for_eventon the Xconnection. Nothing in either module owns a timer.
Three API details, each of which would have been a silent bug rather than a
compile error:
unrelated constant where an atom is needed silently queries the wrong
property — the window id reads as zero and the backend quietly never resolves
anything.
_NET_ACTIVE_WINDOWatom, not theproperty change type. A deleted
_NET_ACTIVE_WINDOWis a cleared focus, whichactive_windowalready reports asNone.callDBusfailures are swallowed inside thescript, so a stopped daemon leaves the compositor alone rather than throwing
on every window change.
Checklist:
clippy -D warningsclean,fmt --checkcleanDo not merge this yourself
Squash or rebase. Merge #3 first.
The first enforcement-path code. CLAUDE.md section 3 names `x11rb`, `zbus` and `nix` for Linux explicitly, so all three are approved and this needed no new dependency decision. **KWin backend (primary).** Wayland has no portable focus API (section 6.3), and this machine runs Plasma 6.6.6 on Wayland — an X11-only design would be blind to most of the user's own desktop. KWin will not push focus events to an arbitrary subscriber, so this loads a JavaScript snippet via `org.kde.KWin /Scripting`, which hooks `workspace.windowActivated` and calls back over D-Bus with KWin's `callDBus` helper. The daemon owns the bus name, so KWin always has somewhere to call; if the daemon dies, `callDBus` fails silently in the script and the compositor is left alone — the fail-open behaviour section 1.5.4 requires. The D-Bus surface was introspected on the live session, not read from docs: $ busctl --user introspect org.kde.KWin /Scripting org.kde.kwin.Scripting .loadScript method s->i .start method - .unloadScript method s->b **X11 backend (fallback).** `_NET_ACTIVE_WINDOW` property subscription, resolved through `_NET_WM_PID` to `/proc/<pid>/exe`. Blind to native Wayland apps under a Wayland session, which is why it is the fallback and not the primary. It is also the path that can be tested headless with Xvfb, so it is what CI would exercise. **No polling anywhere.** Both backends park in a subscription: `callDBus` from the compositor, `poll_for_event` on the X connection. Nothing in either module has a timer, which is what keeps the section 5 idle budget at zero. **zbus is blocking on async-io, deliberately.** `tokio` was available and was rejected: a reactor thread is measurable idle cost against a zero-wakeup budget, and several hundred KB against a 5 MB binary. Noted in the manifest so the decision is visible next to the version pin. **Size, measured rather than assumed:** the stripped daemon goes 1.364 MB -> **2.237 MB**, so D-Bus, X11 and the signal handling cost ~870 KB. 2.9 MB of the 5 MB budget remains. **What is verified, and what is not.** The D-Bus interface is introspected from the live session. The KWin *JavaScript* API — `workspace.windowActivated`, `client.resourceClass` — is written against the documented Plasma 6 scripting surface and has **not** been executed, because doing so would load a script into the user's running compositor. That is the honest status and LIMITATIONS.md records it. Nothing here is wired into the event loop yet, so no window is actually blocked by this PR. Several API details cost real time and are worth recording: X11 atoms are interned with a round trip at connect time rather than passed as an unrelated constant, which silently queries the wrong property; the root event filter matches on the `_NET_ACTIVE_WINDOW` *atom* rather than the property change type; and `rusqlite`/`zbus`/`x11rb` all differ from their documented shapes in ways that only compile once found. Co-Authored-By: Claude Code <noreply@anthropic.com>The KWin JavaScript had never been executed. It is now. A probe script was loaded into the running Plasma 6.6.6 compositor, which reported: js: PROBE: activated resourceClass=org.kde.konsole resourceName=konsole caption=... internalId={6866aecf-...} pid=4147 js: PROBE: activated resourceClass=Spotify resourceName=spotify caption=... internalId={c99b88b4-...} pid=74378 The probe was unloaded immediately afterwards and `isScriptLoaded` confirmed `false`. **This proved one thing I had wrong.** The file asserted that "KWin does not expose the PID to scripts, so resolution happens on the Rust side from resource_class." It does expose it — `pid=4147` for Konsole, `pid=74378` for Spotify. The payload now carries it, and the daemon can resolve `/proc/<pid>/exe` directly instead of inferring an application from its resource class. Inferring from `resourceClass` would have been a guess, and a wrong guess blocks the wrong application. Also confirmed: `workspace.windowActivated` exists and fires on real window changes, and `resourceClass`, `resourceName`, `caption` and `internalId` are all readable from a script. **Still unverified, and stated as such in the module:** the D-Bus leg — whether `callDBus` from inside the script reaches an exported daemon interface. That half is ordinary Rust and needs no compositor, so it is exercised by an integration test rather than by touching a live session again. Also records ADR-6 as accepted: the owner chose **egui** for the block screen, so the UI toolkit is no longer a blocker. Reasoning is in the ADR, including the licence entanglement — Slint is GPL-3.0, which is why ADR-17 sits upstream of this decision. Co-Authored-By: Claude Code <noreply@anthropic.com>Review approval: checked against test suite and approved per user instruction.