breadlock/EVENTS.md
Breadway 6925e132fd Add lock-screen status line and harden the locker
Show MPRIS now-playing and upower battery under the clock, polled on a
background thread (zbus). Flags gate both drawing and D-Bus; desktops
without a battery stay blank; titles are truncated.

Also the locker audit pass that landed in the same files:
- PAM authenticates getuid()/getpwuid_r, not $USER, and missing
  username still takes the lock
- one lock surface per output, generation-gated fail timers, PAM
  timeout, Tab-reveal off by default and cleared on keyboard leave
- GPU chrome is a dirty-rect quad with EGL cleanup; shm reuses a
  SlotPool; appear/shake/flash/veil/clock-crossfade bugs
- hold-to-reveal, checking ellipsis, and password display length

Live-checked on hermes (GPU path, PAM unlock, Tab-hold).
2026-08-23 14:21:19 +08:00

5.9 KiB

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). Commands are received on a BreadClient::subscribe background thread (reconnect/backoff) from two places:

  • the locker process itself, while the session is locked
  • breadlock listen, a tiny long-running subscriber so lock/unlock work while unlocked

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, or the compositor ended an already-active lock (SessionLockHandler::finished after locked — breadlock sends unlock_and_destroy then emits this). Not emitted when the lock was never acquired (finished before locked), on a dispatch-error exit (fail-secure: the session stays locked), or a failed/typo password.
bread.lock.lock.done {} bread.command.lock.lock was honored: the locker was already running, or a locker process was started (same no-args invocation as hypridle's lock_cmd = breadlock). This is the command confirmation, not compositor proof — wait on bread.lock.locked if you need the session-lock protocol to have completed.
bread.lock.lock.failed { "error": "<message>" } bread.command.lock.lock was received but the locker could not be started (e.g. this binary is missing from disk).
bread.lock.unlock.done {} bread.command.lock.unlock was honored: no locker was running (already unlocked), or loginctl unlock-session was invoked for this session. This is the command confirmation, not compositor proof — wait on bread.lock.unlocked if you need PAM + ext-session-lock-v1 unlock.
bread.lock.unlock.failed { "error": "<message>" } bread.command.lock.unlock was received but loginctl unlock-session could not be run (binary missing, non-zero exit).

Commands honored (bread.command.lock.*)

Verb Effect
lock If a locker is already running, emit bread.lock.lock.done and do nothing else. Otherwise start breadlock the same way hypridle does (lock_cmd = breadlock: this binary, no args) and emit done or failed.
unlock If no locker is running, emit bread.lock.unlock.done (already unlocked). Otherwise run loginctl unlock-session on the caller's session and emit done or failed. This is session-level (logind), not a passwordless PAM bypass: breadlock does not call compositor unlock() for this verb.

A Lua workflow that wants the session locked should bread.wait / bread.wait_any on bread.lock.lock.done (or .failed) with a timeout. To know the compositor actually locked, wait on bread.lock.locked. The same pattern applies to unlock: wait on bread.lock.unlock.done / .failed for the command, and on bread.lock.unlocked for PAM + compositor unlock.

Who is listening

bread.command.lock.lock / bread.command.lock.unlock are a silent no-op if nobody is subscribed (bread's usual "no listener, no-op" rule). Two subscribers exist:

  1. breadlock listen — run this for the unlocked path (Hyprland exec-once = breadlock listen, a bread module, or equivalent). Without it, a command sent while the session is unlocked has no process to receive it. Unlock while already unlocked is an idempotent done.
  2. The locker process — always subscribes once the lock screen is up, so lock during an active lock is an idempotent done, and unlock runs loginctl unlock-session rather than compositor unlock().

Session-level equivalent

Super+L on BOS is loginctl lock-session. hypridle picks that up and runs lock_cmd = breadlock. That path does not go through the bread command bus. It is the session-level equivalent of bread.command.lock.lock + breadlock listen: same locker binary, same ext-session-lock-v1 request. Prefer loginctl lock-session from a keybind; prefer the bus command from a Lua workflow.

loginctl unlock-session is the matching session-level unlock. The bus verb invokes that same command. Compositor unlock after a typed password is still PAM on this process (bread.lock.unlocked); a dispatch-error or crash path still does not call compositor unlock() (fail-secure).

Not implemented: pin / blur

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) and the command subscription simply never receives anything — breadlock's actual lock/unlock path is entirely unaffected either way.
  • If breadd restarts, the command subscription reconnects automatically (BreadClient::subscribe's background thread has its own backoff loop); no restart of the locker or of breadlock listen is needed.