//! Linux platform layer. //! //! Observes the OS and enforces decisions. Contains **no decision logic** //! (CLAUDE.md section 2). //! //! # The trait this crate exists to implement //! //! Linux has no portable way to ask "which window has focus" under Wayland. //! The [`ForegroundWatcher`] trait below is the seam, and it will have three //! implementations, tried in order at startup: //! //! 1. **`KwinBackend`** — the primary path on this machine. Loads a small //! KWin script over the `org.kde.KWin /Scripting` D-Bus interface, which //! hooks `windowActivated` and reports back to the daemon. Verified //! available on KDE Plasma 6.6.6: `loadScript`, `start`, `unloadScript`, //! `isScriptLoaded` are all present. //! //! 2. **`X11Backend`** — the portable fallback. `x11rb` subscribes to //! `_NET_ACTIVE_WINDOW` property changes, resolves `_NET_WM_PID`, then reads //! `/proc//exe`. Works on any X11 session and is testable headless with //! Xvfb, so this is also the path CI exercises. //! //! 3. **`ProcessBackend`** — a degraded last resort with no idea what has focus, //! which acts on process creation instead. It cannot show a meaningful block //! screen and should never be reached on a working KDE session. //! //! # Honest limits (CLAUDE.md section 6) //! //! - `X11Backend` is **blind to native Wayland apps** under a Wayland session. //! Under XWayland it sees only XWayland clients. This is why it is a fallback //! and not the primary. //! - `/proc/connectors` does not exist on the development kernel, so the //! netlink proc-connector process watcher is unavailable and //! `ProcessBackend` must use eBPF or `/proc` polling — a worse experience //! than a block screen, by construction. //! - The KWin backend is KDE-specific. wlroots //! (`foreign-toplevel-management`) and GNOME backends remain in M7 per //! section 8, because no portable API exists to build them against. //! - None of this can be made tamper-proof against a user with root. Section //! 6.1 requires this be documented honestly rather than implied otherwise. /// A window that has taken focus, resolved to something a schedule can match. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ForegroundWindow { /// Backend-native window handle. KWin gives a `uuid` string; X11 gives an /// XID. Kept as a string so the trait is not tied to one backend. pub handle: String, /// Resolved process id, where the backend can supply one. pub pid: Option, /// Absolute path of the executable, read from `/proc//exe`. pub exe_path: Option, /// Window title, for display on the block screen. pub title: Option, /// Desktop-file id or WM_CLASS, which is what a Linux user actually /// recognises as "the app" and what schedules should match against. pub desktop_id: Option, } /// Events a watcher can raise. The daemon blocks on this channel; there is no /// polling (section 2). #[derive(Debug, Clone, PartialEq, Eq)] pub enum FocusEvent { /// A window gained focus. Activated(ForegroundWindow), /// The focused window closed, so there is no longer a foreground window. Cleared, } /// Source of focus events. /// /// Implemented once per backend. M2-Linux fills these in. pub trait ForegroundWatcher { /// Begin delivering events to `sink`. /// /// Implementations must be event-driven: they subscribe to an X11 property /// change or a compositor callback, never to a timer. An implementation /// that polls would break the section 5 idle budget even if it functioned. fn subscribe(&mut self, sink: std::sync::mpsc::Sender); /// Which backend this is, for the troubleshooting screen and the log. fn name(&self) -> &'static str; /// Why this backend was chosen, in plain language for the user. fn selection_reason(&self) -> &'static str; } /// Enforcement actions, in escalating order and configurable by the user /// (CLAUDE.md section 4.3). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Enforcement { /// Show the block screen. The default, and the only non-destructive option. BlockScreen, /// Ask the compositor to minimise the window. Minimise, /// `SIGSTOP` the process. Freezes the app rather than closing it, so the /// user's state is preserved and the resume is honest. Freeze, /// Terminate the process. Destructive, and the last resort. Terminate, } /// What the platform layer may and may not do unprivileged. /// /// Recorded here because section 0 rule 2 requires honesty rather than a stub /// that pretends. Each of these is a real limit of an unprivileged user /// daemon, not a missing feature. pub const UNPRIVILEGED_LIMITS: &[&str] = &[ "cannot prevent a process from launching, only react to it after the fact", "cannot mask a systemd unit (that needs root); tamper resistance is \ Restart=always plus a watchdog, nothing stronger", "cannot make its own config tamper-proof against the same user, who owns the \ files — lock state lives in the DB instead (section 1.5.3)", "cannot read /proc//exe for processes owned by other users", "cannot block a process that is already running as root", ];