Wire breadlock to the bread event bus (app id lock)

Emit bread.lock.locked when ext-session-lock-v1 is accepted and
bread.lock.unlocked after PAM success. Fail-silent if breadd is down.
No command verbs (no pin/blur stubs). breadgreet is not on the bus.

Note that Forgejo Arch registry packages are unsigned (SigLevel=Never
on BOS) without changing the package.yml registry.
This commit is contained in:
Breadway 2026-08-15 22:15:17 +08:00
parent 0937c3b05c
commit 2a541ca5e9
9 changed files with 105 additions and 0 deletions

View file

@ -7,6 +7,10 @@ on:
jobs: jobs:
package: package:
runs-on: [self-hosted, hestia] runs-on: [self-hosted, hestia]
# Forgejo's Arch package registry does not GPG-sign packages for pacman.
# BOS therefore uses SigLevel=Never on [Breadway.os.git.breadway.dev]
# until a signed repo exists. Keep publishing here — do not flip this
# job to a different registry just to get signatures.
container: container:
image: archlinux:latest image: archlinux:latest
steps: steps:

24
Cargo.lock generated
View file

@ -64,6 +64,17 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "bread-shared"
version = "0.7.0"
source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b"
dependencies = [
"dirs",
"serde",
"serde_json",
"toml 0.8.23",
]
[[package]] [[package]]
name = "bread-theme" name = "bread-theme"
version = "0.3.1" version = "0.3.1"
@ -75,6 +86,17 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "bread-utils"
version = "0.3.1"
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
dependencies = [
"bread-shared",
"dirs",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "breadgreet" name = "breadgreet"
version = "0.2.0" version = "0.2.0"
@ -97,10 +119,12 @@ dependencies = [
name = "breadlock" name = "breadlock"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"bread-utils",
"breadlock-ui", "breadlock-ui",
"chrono", "chrono",
"pam-client2", "pam-client2",
"serde", "serde",
"serde_json",
"smithay-client-toolkit", "smithay-client-toolkit",
"thiserror 2.0.18", "thiserror 2.0.18",
"tiny-skia", "tiny-skia",

View file

@ -4,6 +4,7 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" } bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" }
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
toml = "0.8" toml = "0.8"

44
EVENTS.md Normal file
View file

@ -0,0 +1,44 @@
# breadlock — bread event integration
breadlock is a standalone session locker: it works exactly the same with
or without `breadd` running. When breadd *is* present, `breadlock`
publishes events into the shared bread automation fabric. See the parent
`bread` repo's `Documentation.md` — specifically its "Namespaces" and
"Integrating a bread\* app" sections — for the general convention this
follows.
App id: **`lock`**. Transport: `bread-utils`'s `bread_client` module
(feature `bread-client`) — `breadlock` links it directly. Each `emit` is
its own short-lived connection (`BreadClient::emit` is fire-and-forget);
there is no long-running subscription half because breadlock has no
command verbs (see below).
`breadgreet` is not wired to the bus. It runs under greetd (typically as
the dedicated greeter user, before a user session exists), so breadd is
usually not there to receive anything, and login is a different lifecycle
from session lock/unlock.
## Events published (`bread.lock.*`)
| Event | Data | When |
|-------|------|------|
| `bread.lock.locked` | `{}` | The compositor accepted the `ext-session-lock-v1` request (`SessionLockHandler::locked`). Not emitted merely because breadlock started or asked to lock. |
| `bread.lock.unlocked` | `{}` | PAM authenticated successfully and breadlock sent `unlock` to the compositor. Not emitted on a compositor-ended lock (`finished`), a dispatch-error exit (fail-secure: the session stays locked), or a failed/typo password. |
## Commands honored (`bread.command.lock.*`)
None. breadlock is started by hypridle / `loginctl lock-session` (or
directly) and unlocks only via PAM on this process. There is no
`lock`/`unlock`/`pin`/`blur` verb, and none is stubbed as a no-op.
`background.blur` in `breadlock.toml` remains a documented locker no-op
(accepted, warned, surface drawn unblurred). That is appearance config,
not a bus command — do not invent `bread.command.lock.blur` for it.
## Fail-safe behavior
- If breadd isn't installed or isn't running, `emit` is a silent no-op
(`BreadClient::emit` never blocks or errors the caller) — breadlock's
actual lock/unlock path is entirely unaffected either way.
- There is no command subscription, so a breadd restart while the lock
screen is up changes nothing on this side.

View file

@ -9,6 +9,13 @@ Two binaries, one workspace:
Both use [`bread-theme`](https://git.breadway.dev/Breadway/bread-ecosystem) for palette loading, matching the rest of the bread* ecosystem (breadbar, breadbox, bos-settings). Both use [`bread-theme`](https://git.breadway.dev/Breadway/bread-ecosystem) for palette loading, matching the rest of the bread* ecosystem (breadbar, breadbox, bos-settings).
## bread event integration
`breadlock` works the same with or without `breadd`. When `breadd` is
running, it publishes `bread.lock.locked` / `bread.lock.unlocked`. See
[EVENTS.md](EVENTS.md) for the bus contract. `breadgreet` is not on the
bus. There is no `bakery.toml` (PAM / pacman exception).
## Architecture ## Architecture
``` ```

View file

@ -19,6 +19,7 @@ path = "src/bin/breadlock-auth-check.rs"
[dependencies] [dependencies]
breadlock-ui = { path = "../breadlock-ui", features = ["paint"] } breadlock-ui = { path = "../breadlock-ui", features = ["paint"] }
bread-utils = { workspace = true, features = ["bread-client"] }
smithay-client-toolkit = "0.20" smithay-client-toolkit = "0.20"
wayland-client = "0.31" wayland-client = "0.31"
tiny-skia = "0.12" tiny-skia = "0.12"
@ -26,6 +27,7 @@ chrono = "0.4"
pam-client2 = { version = "0.5", default-features = false } pam-client2 = { version = "0.5", default-features = false }
zeroize = { version = "1", features = ["std"] } zeroize = { version = "1", features = ["std"] }
serde.workspace = true serde.workspace = true
serde_json.workspace = true
toml.workspace = true toml.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true

View file

@ -0,0 +1,20 @@
//! `bread.lock.*` event integration — optional, non-blocking. See
//! `EVENTS.md` at the repo root for the full contract. breadlock works
//! identically with or without breadd running; every call here is
//! fire-and-forget (`BreadClient::emit` never blocks or errors this
//! process) so a missing or restarting breadd never affects locking
//! itself.
use bread_utils::bread_client::BreadClient;
/// This app's id in bread's sibling-app namespace registry
/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.lock.*`.
pub const APP_ID: &str = "lock";
pub fn emit_locked() {
BreadClient::connect(APP_ID).emit("bread.lock.locked", serde_json::json!({}));
}
pub fn emit_unlocked() {
BreadClient::connect(APP_ID).emit("bread.lock.unlocked", serde_json::json!({}));
}

View file

@ -9,6 +9,7 @@ impl SessionLockHandler for AppState {
fn locked(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, session_lock: SessionLock) { fn locked(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, session_lock: SessionLock) {
tracing::info!("session locked"); tracing::info!("session locked");
self.session_lock = Some(session_lock); self.session_lock = Some(session_lock);
crate::bread_events::emit_locked();
} }
/// The compositor denied the lock request, or ended an active lock out /// The compositor denied the lock request, or ended an active lock out

View file

@ -1,5 +1,6 @@
mod auth; mod auth;
mod background; mod background;
mod bread_events;
mod config; mod config;
mod input; mod input;
mod lock; mod lock;
@ -53,6 +54,7 @@ fn main() {
tracing::info!("authenticated, unlocking"); tracing::info!("authenticated, unlocking");
if let Some(lock) = state.session_lock.take() { if let Some(lock) = state.session_lock.take() {
lock.unlock(); lock.unlock();
bread_events::emit_unlocked();
} }
state.exit = true; state.exit = true;
} }