Compare commits
3 commits
a91cde5621
...
2a541ca5e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a541ca5e9 | ||
|
|
0937c3b05c | ||
|
|
478d8285c3 |
12 changed files with 164 additions and 14 deletions
|
|
@ -7,6 +7,10 @@ on:
|
|||
jobs:
|
||||
package:
|
||||
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:
|
||||
image: archlinux:latest
|
||||
steps:
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -21,4 +21,3 @@ Thumbs.db
|
|||
.claude/
|
||||
|
||||
# Local hygiene notes (not for commit)
|
||||
CLAUDE.md
|
||||
|
|
|
|||
26
AGENTS.md
Normal file
26
AGENTS.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# AGENTS.md — Repo hygiene
|
||||
|
||||
Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation.
|
||||
|
||||
## Branch model
|
||||
- Single-trunk: `main` only. No `dev` or `beta` branch. Land small changes directly, or use short-lived `feature/x`/`fix/x` branches for anything non-trivial and merge back to `main`.
|
||||
- This replaced an earlier three-branch (`dev`/`beta`/`main`) model after `main` silently rotted across the ecosystem. Don't recreate those branches.
|
||||
|
||||
## Channel
|
||||
- **Pacman-only, permanently.** There is no `bakery.toml` on purpose: breadlock installs a root-owned `/etc/pam.d/breadlock` PAM service (and `breadgreet` is a greetd greeter). Bakery has no privileged-install path. Do not add `bakery.toml`.
|
||||
- Releases are `v*` tags. `.forgejo/workflows/package.yml` builds the `[breadway]` pacman package. There are no bakery tracks (`dev`/`beta`/`stable` indexes) for this repo.
|
||||
|
||||
## Remotes
|
||||
- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative. Push here.
|
||||
- `github` — GitHub mirror (push-mirror; do not push to it by hand).
|
||||
|
||||
## CI
|
||||
- `.forgejo/workflows/package.yml` triggers only on `push: tags: ['v*']` — regular pushes to `main` run nothing. Tag a release to trigger packaging.
|
||||
- No build/lint/test CI runs on ordinary commits or PRs — test locally before merging.
|
||||
|
||||
## Cleanup
|
||||
- Delete feature/fix branches (local + remote) once merged. Check with `git branch --merged main`.
|
||||
|
||||
## Don't
|
||||
- Don't add `bakery.toml`.
|
||||
- Don't embed credentials in remote URLs — SSH or a credential helper only.
|
||||
22
CONTRIBUTING.md
Normal file
22
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Contributing
|
||||
|
||||
`breadlock` / `breadgreet` — session locker and greetd greeter for Hyprland.
|
||||
|
||||
Single-trunk, same as the rest of the ecosystem: one long-lived branch
|
||||
(`main`), short-lived `feature/<name>` / `fix/<name>` branches, merge back.
|
||||
No `dev` or `beta` branch.
|
||||
|
||||
This repo is a deliberate **pacman-only** exception. There is no
|
||||
`bakery.toml` — breadlock needs a root-owned `/etc/pam.d/breadlock` PAM
|
||||
service, which bakery cannot install. Don't add one. Releases are `v*`
|
||||
tags that fire `.forgejo/workflows/package.yml` into the `[breadway]`
|
||||
pacman repo. There are no bakery tracks.
|
||||
|
||||
See `AGENTS.md` for remotes and CI details.
|
||||
|
||||
## Local development
|
||||
|
||||
```sh
|
||||
cargo build --release --bin breadlock --bin breadgreet
|
||||
cargo test --workspace
|
||||
```
|
||||
28
Cargo.lock
generated
28
Cargo.lock
generated
|
|
@ -64,10 +64,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "bread-theme"
|
||||
version = "0.2.3"
|
||||
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"gtk4",
|
||||
|
|
@ -75,6 +86,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "breadgreet"
|
||||
version = "0.2.0"
|
||||
|
|
@ -97,10 +119,12 @@ dependencies = [
|
|||
name = "breadlock"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"bread-utils",
|
||||
"breadlock-ui",
|
||||
"chrono",
|
||||
"pam-client2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smithay-client-toolkit",
|
||||
"thiserror 2.0.18",
|
||||
"tiny-skia",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ members = ["breadlock-ui", "breadlock", "breadgreet"]
|
|||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10" }
|
||||
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_json = "1"
|
||||
toml = "0.8"
|
||||
|
|
|
|||
44
EVENTS.md
Normal file
44
EVENTS.md
Normal 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.
|
||||
25
README.md
25
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# breadlock
|
||||
|
||||
Session locker and graphical [greetd](https://git.sr.ht/~kennylevinsen/greetd) greeter for [Hyprland](https://hyprland.org/) on Wayland — the bread-ecosystem replacement for `hyprlock` and the TUI greeter (`tuigreet`) BOS currently ships.
|
||||
Session locker and graphical [greetd](https://git.sr.ht/~kennylevinsen/greetd) greeter for [Hyprland](https://hyprland.org/) on Wayland — the bread-ecosystem replacement for `hyprlock` and `tuigreet`. BOS already ships both binaries: `breadgreet` under `cage` via greetd, and `breadlock` via hypridle (`SUPER+L` is `loginctl lock-session`).
|
||||
|
||||
Two binaries, one workspace:
|
||||
|
||||
|
|
@ -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).
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
|
|
@ -24,7 +31,7 @@ breadlock/
|
|||
|
||||
- **Protocol**: `ext-session-lock-v1` via [`smithay-client-toolkit`](https://docs.rs/smithay-client-toolkit) — GTK has no session-lock support, so this is a raw Wayland client, not a layer-shell surface like breadbar.
|
||||
- **Rendering**: fully software — `tiny-skia` composites each frame (background, rounded password pill, clock, status line) into a `wl_shm` buffer; `cosmic-text` shapes and rasterizes text (loads "Varela Round" by family name). No EGL/GL.
|
||||
- **Background**: a solid palette color or a static PNG (cover-fit). Live blur-of-desktop (hyprlock-style) is a **v2 follow-up** — it needs a `wlr-screencopy` capture (`libwayshot` is the right crate when this gets picked up); `background.blur = true` is accepted today but just logs a warning.
|
||||
- **Background**: a solid palette color or a static PNG (cover-fit). `background.blur` is **not implemented** — the key is accepted and logs a warning; the surface is drawn unblurred. Live blur-of-desktop (hyprlock-style) would need a `wlr-screencopy` capture.
|
||||
- **Auth**: [`pam-client2`](https://crates.io/crates/pam-client2) against the `breadlock` PAM service (`packaging/pam.d/breadlock`, installed to `/etc/pam.d/breadlock` by the package). Runs on its own OS thread — libpam's conversation callback is blocking FFI — and reports back through a `calloop::channel` registered on the render loop.
|
||||
|
||||
### breadgreet
|
||||
|
|
@ -54,29 +61,27 @@ sudo pacman -S gtk4 wayland libxkbcommon pam rust cargo
|
|||
|
||||
## Packaging
|
||||
|
||||
`packaging/arch/PKGBUILD` builds and installs both binaries plus `/etc/pam.d/breadlock`, published to the `[breadway]` pacman repo by `.forgejo/workflows/package.yml`. breadlock is pacman-only — it is not in bread-ecosystem's registry and has no `bakery.toml`; a PAM/greeter component gets installed through the package manager, not the bakery curl-script channel.
|
||||
`packaging/arch/PKGBUILD` builds and installs both binaries plus `/etc/pam.d/breadlock`, published to the `[breadway]` pacman repo by `.forgejo/workflows/package.yml`. breadlock is a deliberate **pacman-only** exception — there is no `bakery.toml` on purpose. A PAM service and greetd greeter need a root-owned install (`/etc/pam.d/breadlock`), which bakery has no privileged path for.
|
||||
|
||||
**Not included, by design**: this repo does not touch `/etc/greetd/config.toml`, install a lock keybind, or wire up `hypridle`. Once packaged, wiring BOS to actually use these binaries means:
|
||||
BOS already wires the packaged binaries (this repo still does not ship those system files):
|
||||
|
||||
```toml
|
||||
# /etc/greetd/config.toml — replace the current tuigreet line
|
||||
# /etc/greetd/config.toml — BOS default
|
||||
[default_session]
|
||||
command = "cage -s -- breadgreet"
|
||||
```
|
||||
|
||||
```
|
||||
# hyprland.conf
|
||||
bind = SUPER, L, exec, breadlock
|
||||
# hypridle lock_cmd (BOS). SUPER+L is loginctl lock-session, which hypridle picks up.
|
||||
lock_cmd = breadlock
|
||||
```
|
||||
|
||||
That's a separate, later BOS task — deliberately kept out of this change so the existing `tuigreet` login path stays untouched and available as a fallback while these binaries are tested.
|
||||
|
||||
## Verification (why this is safe to test without a lockout risk)
|
||||
|
||||
1. **PAM logic in isolation first**: `cargo run --bin breadlock-auth-check` exercises the exact PAM flow `breadlock` uses, against a typed password, with **no Wayland surface at all**. A bad `/etc/pam.d/breadlock` just prints an error here — it can never lock a session.
|
||||
2. **Locker rendering/lock lifecycle nested, never against the live session**: run `breadlock` inside a nested Hyprland instance or under `cage -- breadlock`. `ext-session-lock-v1` only ever affects the compositor instance the client is connected to (scoped to `$WAYLAND_DISPLAY`), so a nested lock can never lock the real outer session. Verify the full type-password → PAM check → unlock cycle there, including the wrong-password path, before ever binding a real keybind.
|
||||
3. **If testing against a live session**: keep a second TTY or SSH session open the whole time. Killing the `breadlock` process is **not** a safe unlock path — per the protocol, an abnormally-terminated lock client is expected to leave the compositor still locked. The real recovery path is "kill it, then use the second session to restart Hyprland or switch VT."
|
||||
4. **breadgreet**: `cargo test -p breadgreet` runs the `greetd_ipc` framing/state-machine tests against a mock Unix-socket server — no real `greetd` or PAM involved. Manual testing against a real `greetd` should happen on a disposable VT, leaving the existing `tuigreet` config on VT1 untouched as a fallback.
|
||||
4. **breadgreet**: `cargo test -p breadgreet` runs the `greetd_ipc` framing/state-machine tests against a mock Unix-socket server — no real `greetd` or PAM involved. Manual testing against a real `greetd` should happen on a disposable VT, not by replacing the live BOS `cage -s -- breadgreet` session on VT1.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ path = "src/bin/breadlock-auth-check.rs"
|
|||
|
||||
[dependencies]
|
||||
breadlock-ui = { path = "../breadlock-ui", features = ["paint"] }
|
||||
bread-utils = { workspace = true, features = ["bread-client"] }
|
||||
smithay-client-toolkit = "0.20"
|
||||
wayland-client = "0.31"
|
||||
tiny-skia = "0.12"
|
||||
|
|
@ -26,6 +27,7 @@ chrono = "0.4"
|
|||
pam-client2 = { version = "0.5", default-features = false }
|
||||
zeroize = { version = "1", features = ["std"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
|
|
|||
20
breadlock/src/bread_events.rs
Normal file
20
breadlock/src/bread_events.rs
Normal 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!({}));
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ impl SessionLockHandler for AppState {
|
|||
fn locked(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, session_lock: SessionLock) {
|
||||
tracing::info!("session locked");
|
||||
self.session_lock = Some(session_lock);
|
||||
crate::bread_events::emit_locked();
|
||||
}
|
||||
|
||||
/// The compositor denied the lock request, or ended an active lock out
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
mod auth;
|
||||
mod background;
|
||||
mod bread_events;
|
||||
mod config;
|
||||
mod input;
|
||||
mod lock;
|
||||
|
|
@ -53,6 +54,7 @@ fn main() {
|
|||
tracing::info!("authenticated, unlocking");
|
||||
if let Some(lock) = state.session_lock.take() {
|
||||
lock.unlock();
|
||||
bread_events::emit_unlocked();
|
||||
}
|
||||
state.exit = true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue