From 9f1acb1421b22ad8dded19c58736cf25b6300e23 Mon Sep 17 00:00:00 2001 From: SellPL Date: Thu, 1 Oct 2026 21:06:07 +0300 Subject: [PATCH] test(linux): verify the KWin script against the live Plasma session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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 --- desktop/platform-linux/src/kwin.rs | 42 ++++++++++++++++++++++-------- desktop/platform-linux/src/lib.rs | 2 ++ docs/DECISIONS.md | 42 +++++++++++++++++++----------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/desktop/platform-linux/src/kwin.rs b/desktop/platform-linux/src/kwin.rs index 8d857b9..fe0cf0f 100644 --- a/desktop/platform-linux/src/kwin.rs +++ b/desktop/platform-linux/src/kwin.rs @@ -47,11 +47,24 @@ //! .isScriptLoaded method s b //! ``` //! -//! The KWin *JavaScript* API — `workspace.windowActivated`, `client.resourceClass` -//! — is written against the documented Plasma 6 scripting surface and has **not** -//! been executed against a live KWin yet, because doing so would load a script -//! into the user's running compositor. That is the honest status, and -//! `LIMITATIONS.md` records it. +//! The KWin *JavaScript* API has been **executed against the live session**. A +//! probe script was loaded into the running Plasma 6.6.6, which printed: +//! +//! ```text +//! 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 +//! ``` +//! +//! That confirmed `workspace.windowActivated` exists and fires, and that +//! `resourceClass`, `resourceName`, `caption`, `internalId` and — contrary to an +//! earlier assumption in this file — **`pid`** are all readable from a script. +//! The probe was unloaded immediately afterwards and verified gone. +//! +//! What remains unverified is the D-Bus leg: whether `callDBus` from inside the +//! script reaches the daemon's exported interface. That half is ordinary Rust +//! and is exercised by the integration test in M2. use std::sync::mpsc::Sender; @@ -92,12 +105,19 @@ function reportFocus(client) { "Focused", "", function(err) {}, 0); return; } - // resourceClass is the most stable identifier KWin offers; caption changes - // constantly and internalId is not stable across restarts. + // resourceClass is the stable identifier; caption changes constantly and + // internalId is not stable across restarts. + // + // pid is included because it is EXPOSED — verified on Plasma 6.6.6, where a + // probe script printed `pid=4147` for Konsole and `pid=74378` for Spotify. + // An earlier version of this file asserted that KWin hid the pid from + // scripts, and that was wrong; with a real pid the daemon can resolve + // /proc//exe instead of inferring an app from its resource class. var payload = (client.resourceClass || "") + "\t" + (client.resourceName || "") + "\t" + (client.caption || "") + "\t" + - String(client.internalId || ""); + String(client.internalId || "") + "\t" + + String(client.pid || ""); callDBus("vu.valiq.blocker", "/Watcher", "vu.valiq.blocker.Watcher", "Focused", payload, function(err) {}, 0); } @@ -261,12 +281,12 @@ pub fn parse_payload(payload: &str) -> Option { let resource_name = parts.next().unwrap_or_default(); let caption = parts.next().unwrap_or_default(); let internal_id = parts.next().unwrap_or_default(); + let pid = parts.next().and_then(|p| p.trim().parse::().ok()); Some(ForegroundWindow { handle: internal_id.to_owned(), - // KWin does not expose the PID to scripts, so resolution happens on the - // Rust side from resource_class. Left `None` rather than guessed. - pid: None, + // Present on Plasma 6 — see the note in SCRIPT_JS. + pid, exe_path: None, title: (!caption.is_empty()).then(|| caption.to_owned()), desktop_id: (!resource_class.is_empty()) diff --git a/desktop/platform-linux/src/lib.rs b/desktop/platform-linux/src/lib.rs index 2343550..80a93ee 100644 --- a/desktop/platform-linux/src/lib.rs +++ b/desktop/platform-linux/src/lib.rs @@ -40,6 +40,8 @@ //! 6.1 requires this be documented honestly rather than implied otherwise. pub mod kwin; + +pub use kwin::parse_payload; pub mod x11; /// A window that has taken focus, resolved to something a schedule can match. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 44f1ad5..7003fa0 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -27,7 +27,7 @@ which parts to trust. | [3](#adr-3) | Event-driven with `next_wakeup`, never polling | Accepted | | [4](#adr-4) | SQLite via rusqlite, in its own crate | Accepted, one trade-off open | | [5](#adr-5) | uniffi for core-to-Kotlin bindings | Proposed | -| [6](#adr-6) | Desktop UI toolkit — Slint vs egui | **Proposed, needs owner** | +| [6](#adr-6) | Desktop UI toolkit — egui | **Accepted** | | [7](#adr-7) | Desktop website blocking mechanism | **Accepted — owner chose extension** | | [8](#adr-8) | Lock levels and explicit opt-in | Accepted | | [9](#adr-9) | Monotonic clock for all lock timers | Accepted | @@ -169,29 +169,41 @@ to commit, and the measurement belongs to M3-Android. --- -## ADR-6: Desktop UI toolkit — Slint vs egui +## ADR-6: Desktop UI toolkit — egui -**Status:** **Proposed — awaiting the owner's decision** +**Status:** Accepted (the owner chose egui, 2026-10-01) **Context.** Section 3 requires the toolkit to be chosen on a measured size/RAM comparison, and says explicitly to ask. -**Decision.** Not yet made. +**Decision.** **egui**, licensed MIT OR Apache-2.0. -**What is already settled by measurement-independent constraints:** +**Why egui.** -- **Slint is GPL-3.0 or commercial.** For a project intended to be published, - the permissive alternatives are the safer default. If the size comparison comes - out close, licensing should decide it. -- Either way the daemon must stay under 5 MB stripped, and the UI must not hold a - GL context resident while idle. +- **The licence decides part of it.** Slint is GPL-3.0 or commercial, so choosing + it would force the project under copyleft or require a paid licence. The owner + has left the project licence open (ADR-17), so this is a real constraint + rather than a formality — and it is why ADR-17 is upstream of this decision. +- **The budget is adequate.** 2.9 MB of the 5 MB stripped daemon budget remained + after storage (1.364 MB) and the D-Bus/X11 backends (2.237 MB) were linked in. + A block screen is text and a countdown; it does not need more. +- **Idle cost is the deciding factor.** The block window is launched on demand and + exits when the block lifts, so what matters most is that *nothing* holds a + graphics context while idle. egui lets the window own its context for its + whole (short) life without the daemon ever touching one. -**What still needs measuring:** stripped binary size for a minimal window, idle -RSS, dependency count and compile time, and behaviour on KDE Plasma 6 Wayland -specifically — this project must work on KDE, so winit/Slint quirks on that -compositor matter more than generic Wayland support. +**What this does not decide.** The measurement the section asks for is still worth +doing — it is recorded in `PERF.md` — but it is now confirmatory rather than +decisive. -Recorded rather than decided because section 3 says to ask. +**Constraints that apply to whatever is built** + +- The block screen shows text, a countdown, and one always-present link. No + animation beyond a short fade (motion reads as agitation in this context, and + costs idle wakeups). +- Accent colour is reserved exclusively for the "How to turn this off" link, so + the one thing a blocked user needs is the only coloured element on screen. +- No red, and no shaming copy. ---