a79d26298631974a8e49f50b53be969716203318
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a79d262986 |
feat(linux): add KWin and X11 focus-detection backends
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>
|
||
|
|
585fd30440 |
feat(core): add workspace skeleton and the pure decision engine
Establishes the cargo workspace and the shared `blocker-core` crate. The engine is deliberately written before any platform code, because CLAUDE.md section 2 makes it the single source of decision truth and M1 acceptance requires it to be pure. **Workspace** — five crates plus a test crate, zero external dependencies. Section 3 requires every crate to justify its binary-size cost, so M0 adds none. [profile.release] is workspace-only (cargo rejects it in members) and carries the section 5 settings that make the 5 MB stripped budget reachable. **Purity contract** — `evaluate(&EngineInput) -> Decision` reads no clock, no disk and no OS API; everything arrives in the input. `tests/purity.rs` enforces this mechanically by asserting blocker-core has zero dependencies, so a stray I/O reach-in fails the build rather than silently making the golden tables useless as a parity contract. Decisions worth calling out: - **Monotonic time is a separate type from wall time.** Section 1.5.3 requires a system date change never to shorten a lock. Keeping them apart makes that structural rather than a rule to remember. - **`start > end` means overnight.** No `crosses_midnight` flag: the comparison already encodes it and a second field is a second way to be wrong. The weekday for a window that started yesterday evening is yesterday's, which `overnight_window_survives_past_midnight` pins down. - **`next_wakeup` is armed over every non-paused schedule, not only active ones.** Filtering to active conditions first would let the daemon sleep straight through the moment a schedule is due to start. - **Allowlist needs the installed-app inventory.** `EngineInput` carries `installed_apps`; an empty slice fails open rather than guessing. - **Wall-clock correction extends, never shortens, and clamps to the 24h cap.** Extending unbounded would satisfy anti-tamper and violate section 1.5.4. - **`cast_possible_truncation` is denied workspace-wide.** Truncating a millisecond counter would corrupt a lock timer. Verified: 22 tests pass, `cargo fmt --check` clean, `cargo clippy -D warnings` clean, stripped release daemon 0.279 MB against the 5 MB budget. Co-Authored-By: Claude Code <noreply@anthropic.com> |