workspace: add bread-app crate and first-cut bread-polkit agent
bread-app is the GTK bootstrap new tools should use instead of another copied main.rs: com.breadway.* app id, singleton lock, optional gtk_popup re-export, optional bread.command.<app>.** listen loop. Tests cover app-id helpers and command-verb parse. Existing apps are not migrated. bread-polkit is an own PolicyKit1 session authentication agent with a bread-theme GTK4 password prompt (not a polkit-gnome wrapper). Autostart via contrib/bread-polkit.desktop or exec-once. Not a bakery product; not added to the BOS ISO lockfile.
This commit is contained in:
parent
c296d26408
commit
ff2bb36a15
19 changed files with 2049 additions and 32 deletions
|
|
@ -3,10 +3,13 @@
|
||||||
This repo is a Cargo workspace. Bakery-channel products shipped from here
|
This repo is a Cargo workspace. Bakery-channel products shipped from here
|
||||||
are `bakery` (the ecosystem package manager) and `bread-theme` (the shared
|
are `bakery` (the ecosystem package manager) and `bread-theme` (the shared
|
||||||
theming crate). Shared crates that sibling apps pin — not bakery packages
|
theming crate). Shared crates that sibling apps pin — not bakery packages
|
||||||
of their own — are `bread-utils`, `bread-onnx`, `bread-screenshots`, and
|
of their own — are `bread-utils`, `bread-app`, `bread-onnx`,
|
||||||
`bread-capture`. Other ecosystem products (`bread`, `breadbar`, `breadbox`,
|
`bread-screenshots`, and `bread-capture`. `bread-polkit` is an in-tree
|
||||||
…) live in their own repos under `Breadway/` but follow the same workflow
|
session agent, also not a bakery product. Other ecosystem products
|
||||||
described here. The product list is `registry/bread-ecosystem.toml`.
|
(`bread`, `breadbar`, `breadbox`, …) live in their own repos under
|
||||||
|
`Breadway/` but follow the same workflow described here. The product list
|
||||||
|
is `registry/bread-ecosystem.toml`. New GTK tools should depend on
|
||||||
|
`bread-app` instead of copying another app's bootstrap.
|
||||||
|
|
||||||
## Branches
|
## Branches
|
||||||
|
|
||||||
|
|
@ -88,10 +91,12 @@ cargo build --release -p bakery
|
||||||
cargo test --release -p bakery
|
cargo test --release -p bakery
|
||||||
```
|
```
|
||||||
|
|
||||||
`bakery`, `bread-theme`, `bread-utils`, `bread-onnx`, `bread-screenshots`,
|
`bakery`, `bread-theme`, `bread-utils`, `bread-app`, `bread-polkit`,
|
||||||
and `bread-capture` are all workspace members. Run the same commands with
|
`bread-onnx`, `bread-screenshots`, and `bread-capture` are all workspace
|
||||||
`-p bread-theme --bin bread-theme` for that crate, or `-p bread-utils
|
members. Run the same commands with `-p bread-theme --bin bread-theme`
|
||||||
--features bread-client` for the IPC client.
|
for that crate, `-p bread-utils --features bread-client` for the IPC
|
||||||
|
client, or `-p bread-app --features bread-client` for the GTK bootstrap
|
||||||
|
helpers.
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
|
|
|
||||||
439
Cargo.lock
generated
439
Cargo.lock
generated
|
|
@ -96,6 +96,40 @@ version = "1.0.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-broadcast"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-recursion"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
|
|
@ -151,6 +185,13 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bread-app"
|
||||||
|
version = "0.7.2"
|
||||||
|
dependencies = [
|
||||||
|
"bread-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bread-capture"
|
name = "bread-capture"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
|
@ -176,6 +217,21 @@ dependencies = [
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bread-polkit"
|
||||||
|
version = "0.7.2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bread-app",
|
||||||
|
"bread-theme",
|
||||||
|
"gtk4",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bread-screenshots"
|
name = "bread-screenshots"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
|
@ -239,6 +295,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cairo-rs"
|
name = "cairo-rs"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
|
|
@ -350,7 +412,7 @@ dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -478,7 +540,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -489,7 +551,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -519,7 +581,7 @@ dependencies = [
|
||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -529,7 +591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_builder_core",
|
"derive_builder_core",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -571,7 +633,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -586,6 +648,33 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "endi"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||||
|
dependencies = [
|
||||||
|
"enumflags2_derive",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2_derive"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -611,6 +700,26 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "5.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||||
|
dependencies = [
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener-strategy"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
|
|
@ -709,6 +818,19 @@ version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
|
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
|
|
@ -717,7 +839,7 @@ checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -921,7 +1043,7 @@ dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1055,7 +1177,7 @@ dependencies = [
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1302,6 +1424,12 @@ version = "3.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libadwaita"
|
name = "libadwaita"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
|
|
@ -1397,6 +1525,15 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
|
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matrixmultiply"
|
name = "matrixmultiply"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
|
|
@ -1444,6 +1581,17 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monostate"
|
name = "monostate"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
|
|
@ -1463,7 +1611,7 @@ checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1568,6 +1716,16 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-stream"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ort"
|
name = "ort"
|
||||||
version = "2.0.0-rc.12"
|
version = "2.0.0-rc.12"
|
||||||
|
|
@ -1610,6 +1768,12 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
|
@ -1960,7 +2124,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1976,6 +2140,17 @@ dependencies = [
|
||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.9"
|
version = "0.6.9"
|
||||||
|
|
@ -2005,12 +2180,31 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
|
|
@ -2029,6 +2223,16 @@ version = "1.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spm_precompiled"
|
name = "spm_precompiled"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
|
@ -2076,6 +2280,17 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
|
|
@ -2084,7 +2299,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2145,7 +2360,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2156,7 +2371,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2203,6 +2427,34 @@ dependencies = [
|
||||||
"unicode_categories",
|
"unicode_categories",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.23"
|
version = "0.8.23"
|
||||||
|
|
@ -2314,7 +2566,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2326,12 +2578,38 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
|
"sharded-slab",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uds_windows"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||||
|
dependencies = [
|
||||||
|
"memoffset",
|
||||||
|
"tempfile",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|
@ -2419,6 +2697,17 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde_core",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -2478,7 +2767,7 @@ dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2540,7 +2829,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2551,7 +2840,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2781,10 +3070,75 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus"
|
||||||
|
version = "5.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
|
||||||
|
dependencies = [
|
||||||
|
"async-broadcast",
|
||||||
|
"async-recursion",
|
||||||
|
"async-trait",
|
||||||
|
"enumflags2",
|
||||||
|
"event-listener",
|
||||||
|
"futures-core",
|
||||||
|
"futures-lite",
|
||||||
|
"hex",
|
||||||
|
"libc",
|
||||||
|
"ordered-stream",
|
||||||
|
"rustix 1.1.4",
|
||||||
|
"serde",
|
||||||
|
"serde_repr",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"uds_windows",
|
||||||
|
"uuid",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
"winnow 1.0.4",
|
||||||
|
"zbus_macros",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_macros"
|
||||||
|
version = "5.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_names"
|
||||||
|
version = "4.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"winnow 1.0.4",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zcheapstr"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.54"
|
version = "0.8.54"
|
||||||
|
|
@ -2802,7 +3156,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2822,7 +3176,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2862,7 +3216,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2870,3 +3224,44 @@ name = "zmij"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant"
|
||||||
|
version = "5.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e28c25bd8bb8da5a1f3e7065d0c156b9ee9a7973adf78b0e35eaefdf3b1b5c"
|
||||||
|
dependencies = [
|
||||||
|
"endi",
|
||||||
|
"enumflags2",
|
||||||
|
"serde",
|
||||||
|
"winnow 1.0.4",
|
||||||
|
"zcheapstr",
|
||||||
|
"zvariant_derive",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_derive"
|
||||||
|
version = "5.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d496a145685283b67e232bd9e47377f6b60ad9d51e3601b23867f77c42477f96"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_utils"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "629d80ece222cad20fe0e8741be493c4ab166acf3b85341bdc2cdbcfd8f3c2d6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"syn 3.0.3",
|
||||||
|
"winnow 1.0.4",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["bakery", "bread-theme", "bread-utils", "bread-onnx", "bread-screenshots", "bread-capture"]
|
members = ["bakery", "bread-theme", "bread-utils", "bread-onnx", "bread-screenshots", "bread-capture", "bread-app", "bread-polkit"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
|
||||||
37
README.md
37
README.md
|
|
@ -145,13 +145,15 @@ Install all required deps with `sudo pacman -S <packages>`. Use `pacman -Q <pkg>
|
||||||
|
|
||||||
This repo is a Cargo workspace. Bakery-channel products shipped from here
|
This repo is a Cargo workspace. Bakery-channel products shipped from here
|
||||||
are `bakery` and `bread-theme`; the other members are shared crates sibling
|
are `bakery` and `bread-theme`; the other members are shared crates sibling
|
||||||
apps pin, not bakery packages of their own.
|
apps pin, or in-tree tools that are not bakery packages of their own.
|
||||||
|
|
||||||
```
|
```
|
||||||
bread-ecosystem/
|
bread-ecosystem/
|
||||||
├── bakery/ # package manager binary
|
├── bakery/ # package manager binary
|
||||||
├── bread-theme/ # shared pywal + fixed-dark-base theming crate
|
├── bread-theme/ # shared pywal + fixed-dark-base theming crate
|
||||||
├── bread-utils/ # shared plumbing (Hyprland IPC, singleton, XDG, BreadClient, …)
|
├── bread-utils/ # shared plumbing (Hyprland IPC, singleton, XDG, BreadClient, …)
|
||||||
|
├── bread-app/ # GTK bootstrap new tools should use (app id, singleton, overlay, command listen)
|
||||||
|
├── bread-polkit/ # themed PolicyKit authentication agent (not a bakery product)
|
||||||
├── bread-onnx/ # shared ONNX runtime helpers
|
├── bread-onnx/ # shared ONNX runtime helpers
|
||||||
├── bread-screenshots/ # grim capture primitive used by app `--screenshot` modes
|
├── bread-screenshots/ # grim capture primitive used by app `--screenshot` modes
|
||||||
├── bread-capture/ # orchestrator that drives those `--screenshot` modes
|
├── bread-capture/ # orchestrator that drives those `--screenshot` modes
|
||||||
|
|
@ -162,6 +164,39 @@ bread-ecosystem/
|
||||||
└── gen-readme-products.sh # rewrites the Products table from the registry
|
└── gen-readme-products.sh # rewrites the Products table from the registry
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### New GTK tools
|
||||||
|
|
||||||
|
Do not copy another app's `main.rs`. Depend on `bread-app`:
|
||||||
|
|
||||||
|
- `bread_app::application_id` / `try_acquire` / `toggle_or_kill` for the
|
||||||
|
`com.breadway.*` application id and single-instance lock
|
||||||
|
- feature `gtk` re-exports `bread_utils::gtk_popup` (layer-shell overlay)
|
||||||
|
- feature `bread-client` for `listen_commands` on `bread.command.<app>.**`
|
||||||
|
|
||||||
|
See the `bread-app` crate docs. Existing apps are not migrated in this
|
||||||
|
tree; `bread-polkit` is the first in-tree consumer.
|
||||||
|
|
||||||
|
### bread-polkit
|
||||||
|
|
||||||
|
A session PolicyKit authentication agent (password prompt, cancel,
|
||||||
|
identity). Not a wrapper around `polkit-gnome`. Not published via bakery
|
||||||
|
and not on the BOS ISO lockfile.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p bread-polkit
|
||||||
|
```
|
||||||
|
|
||||||
|
Autostart — pick one:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp bread-polkit/contrib/bread-polkit.desktop ~/.config/autostart/
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# hyprland.conf
|
||||||
|
exec-once = bread-polkit
|
||||||
|
```
|
||||||
|
|
||||||
## Release pipeline
|
## Release pipeline
|
||||||
|
|
||||||
Each product repo (`Breadway/bread`, `Breadway/breadbar`, …) has
|
Each product repo (`Breadway/bread`, `Breadway/breadbar`, …) has
|
||||||
|
|
|
||||||
20
bread-app/Cargo.toml
Normal file
20
bread-app/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "bread-app"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
description = "GTK application bootstrap for bread desktop tools: app id, singleton, optional overlay popup, and command listen loop"
|
||||||
|
repository = "https://git.breadway.dev/Breadway/bread-ecosystem"
|
||||||
|
keywords = ["gtk4", "wayland", "hyprland"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bread-utils = { path = "../bread-utils" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Layer-shell overlay helper (`gtk_popup`). Matches `bread-utils/gtk` so a
|
||||||
|
# consumer that only wants app-id / singleton helpers does not pull GTK4.
|
||||||
|
gtk = ["bread-utils/gtk"]
|
||||||
|
# `BreadClient` listen loop on `bread.command.<app>.**`. Matches
|
||||||
|
# `bread-utils/bread-client`.
|
||||||
|
bread-client = ["bread-utils/bread-client"]
|
||||||
121
bread-app/src/command.rs
Normal file
121
bread-app/src/command.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
//! Command-bus helpers for `bread.command.<app>.**`.
|
||||||
|
//!
|
||||||
|
//! The `command_id` here is the breadd sibling-app id (`clip`, `box`,
|
||||||
|
//! `shot`) — often shorter than the GTK / singleton name (`breadclip`).
|
||||||
|
|
||||||
|
use crate::id::{parse_app_name, InvalidAppId};
|
||||||
|
use bread_utils::bread_client::{BreadClient, BreadEvent, Subscription};
|
||||||
|
|
||||||
|
/// Same charset as [`parse_app_name`]: a single command-bus segment.
|
||||||
|
pub fn parse_command_id(command_id: &str) -> Result<&str, InvalidAppId> {
|
||||||
|
parse_app_name(command_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe glob: `bread.command.<app>.**`.
|
||||||
|
pub fn command_pattern(command_id: &str) -> Result<String, InvalidAppId> {
|
||||||
|
let id = parse_command_id(command_id)?;
|
||||||
|
Ok(format!("bread.command.{id}.**"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The verb segment of `bread.command.<app>.<verb>` (and extra trailing
|
||||||
|
/// segments, if any). `None` when the event is not addressed to
|
||||||
|
/// `command_id` or the verb is missing.
|
||||||
|
///
|
||||||
|
/// Extra dotted remainder (`bread.command.clip.stack.clear`) yields the
|
||||||
|
/// first remaining segment (`stack`) — a verb is one segment, matching
|
||||||
|
/// [`BreadClient::command`].
|
||||||
|
pub fn command_verb<'a>(event: &'a str, command_id: &str) -> Option<&'a str> {
|
||||||
|
if command_id.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let prefix = format!("bread.command.{command_id}.");
|
||||||
|
let rest = event.strip_prefix(&prefix)?;
|
||||||
|
let verb = rest.split('.').next()?;
|
||||||
|
if verb.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(verb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to `bread.command.<command_id>.**` and invoke `on_verb` with
|
||||||
|
/// the parsed verb plus the raw event.
|
||||||
|
///
|
||||||
|
/// Fail-silent: constructing the client and holding the subscription never
|
||||||
|
/// requires breadd to be running. Drop the returned [`Subscription`] (or
|
||||||
|
/// call [`Subscription::stop`]) to end the loop.
|
||||||
|
pub fn listen_commands<F>(command_id: &str, on_verb: F) -> Result<Subscription, InvalidAppId>
|
||||||
|
where
|
||||||
|
F: Fn(&str, BreadEvent) + Send + 'static,
|
||||||
|
{
|
||||||
|
let id = parse_command_id(command_id)?.to_string();
|
||||||
|
let client = BreadClient::connect(id.clone());
|
||||||
|
let pattern = format!("bread.command.{id}.**");
|
||||||
|
Ok(client.subscribe(pattern, move |event| {
|
||||||
|
let Some(verb) = command_verb(&event.event, &id).map(str::to_owned) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
on_verb(&verb, event);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_pattern_uses_double_star() {
|
||||||
|
assert_eq!(command_pattern("clip").unwrap(), "bread.command.clip.**");
|
||||||
|
assert_eq!(command_pattern("shot").unwrap(), "bread.command.shot.**");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_pattern_rejects_invalid_id() {
|
||||||
|
assert!(command_pattern("").is_err());
|
||||||
|
assert!(command_pattern("clip.clear").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_verb_strips_app_prefix() {
|
||||||
|
assert_eq!(
|
||||||
|
command_verb("bread.command.clip.clear", "clip"),
|
||||||
|
Some("clear")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
command_verb("bread.command.shot.region", "shot"),
|
||||||
|
Some("region")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
command_verb("bread.command.shot.annotate", "shot"),
|
||||||
|
Some("annotate")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_verb_takes_first_segment_only() {
|
||||||
|
assert_eq!(
|
||||||
|
command_verb("bread.command.clip.stack.clear", "clip"),
|
||||||
|
Some("stack")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_verb_rejects_other_apps_and_missing_verb() {
|
||||||
|
assert_eq!(command_verb("bread.command.clip.clear", "shot"), None);
|
||||||
|
assert_eq!(command_verb("bread.command.clip", "clip"), None);
|
||||||
|
assert_eq!(command_verb("bread.command.clip.", "clip"), None);
|
||||||
|
assert_eq!(command_verb("bread.clip.copied", "clip"), None);
|
||||||
|
assert_eq!(command_verb("bread.command.clip.clear", ""), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn listen_commands_rejects_invalid_id() {
|
||||||
|
assert!(listen_commands("", |_, _| {}).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn listen_commands_stop_joins_without_a_daemon() {
|
||||||
|
let sub = listen_commands("clip", |_, _| {}).unwrap();
|
||||||
|
sub.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
145
bread-app/src/id.rs
Normal file
145
bread-app/src/id.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
//! App-id helpers shared by GTK tools and the singleton lock.
|
||||||
|
//!
|
||||||
|
//! The process / pid-file name (`breadbox`, `bread-polkit`) is also the
|
||||||
|
//! last segment of the GApplication id (`com.breadway.breadbox`). That is
|
||||||
|
//! *not* always the breadd command-bus id (`box`, `clip`) — see
|
||||||
|
//! [`crate::command_verb`] under feature `bread-client`.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crate::singleton::{self, Acquire, Toggle};
|
||||||
|
|
||||||
|
/// Why [`parse_app_name`] / [`application_id`] rejected a string.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct InvalidAppId {
|
||||||
|
/// The rejected input, owned so the error is `'static`.
|
||||||
|
pub name: String,
|
||||||
|
/// Short reason suitable for an `io::Error` / clap message.
|
||||||
|
pub reason: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for InvalidAppId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "invalid app id '{}': {}", self.name, self.reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for InvalidAppId {}
|
||||||
|
|
||||||
|
/// Accept a process / GTK application name (`breadbox`, `bread-polkit`).
|
||||||
|
///
|
||||||
|
/// Rules match a GApplication id *element*: non-empty, ASCII letter first,
|
||||||
|
/// then ASCII alphanumeric / `-` / `_`. Dots are rejected so the name can
|
||||||
|
/// sit in `com.breadway.<name>` without creating extra segments.
|
||||||
|
pub fn parse_app_name(name: &str) -> Result<&str, InvalidAppId> {
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(InvalidAppId {
|
||||||
|
name: name.to_string(),
|
||||||
|
reason: "must not be empty",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut chars = name.chars();
|
||||||
|
let first = chars.next().expect("non-empty");
|
||||||
|
if !first.is_ascii_alphabetic() {
|
||||||
|
return Err(InvalidAppId {
|
||||||
|
name: name.to_string(),
|
||||||
|
reason: "must start with an ASCII letter",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
|
||||||
|
return Err(InvalidAppId {
|
||||||
|
name: name.to_string(),
|
||||||
|
reason: "only ASCII letters, digits, '-' and '_' are allowed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reverse-DNS GApplication id: `com.breadway.<name>`.
|
||||||
|
pub fn application_id(app_name: &str) -> Result<String, InvalidAppId> {
|
||||||
|
let name = parse_app_name(app_name)?;
|
||||||
|
Ok(format!("com.breadway.{name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`singleton::try_acquire`] after [`parse_app_name`].
|
||||||
|
///
|
||||||
|
/// Invalid names become [`io::ErrorKind::InvalidInput`] and never touch
|
||||||
|
/// the pid file.
|
||||||
|
pub fn try_acquire(app_name: &str) -> io::Result<Acquire> {
|
||||||
|
let name =
|
||||||
|
parse_app_name(app_name).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||||
|
singleton::try_acquire(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`singleton::toggle_or_kill`] after [`parse_app_name`].
|
||||||
|
pub fn toggle_or_kill(app_name: &str) -> io::Result<Toggle> {
|
||||||
|
let name =
|
||||||
|
parse_app_name(app_name).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||||
|
singleton::toggle_or_kill(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_app_name_accepts_existing_tool_names() {
|
||||||
|
for name in ["breadbox", "breadclip", "bread-polkit", "breadcast"] {
|
||||||
|
assert_eq!(parse_app_name(name), Ok(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_app_name_rejects_empty_dot_and_leading_digit() {
|
||||||
|
assert!(parse_app_name("").is_err());
|
||||||
|
assert!(parse_app_name("bread.box").is_err());
|
||||||
|
assert!(parse_app_name("1box").is_err());
|
||||||
|
assert!(parse_app_name("-box").is_err());
|
||||||
|
assert!(parse_app_name("bread box").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn application_id_uses_com_breadway_prefix() {
|
||||||
|
assert_eq!(application_id("breadbox").unwrap(), "com.breadway.breadbox");
|
||||||
|
assert_eq!(
|
||||||
|
application_id("bread-polkit").unwrap(),
|
||||||
|
"com.breadway.bread-polkit"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn application_id_rejects_invalid_name() {
|
||||||
|
assert!(application_id("").is_err());
|
||||||
|
assert!(application_id("bread.box").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_acquire_rejects_invalid_name_before_lock() {
|
||||||
|
match try_acquire("") {
|
||||||
|
Err(err) => assert_eq!(err.kind(), io::ErrorKind::InvalidInput),
|
||||||
|
Ok(_) => panic!("empty name must not acquire a lock"),
|
||||||
|
}
|
||||||
|
match try_acquire("bread.box") {
|
||||||
|
Err(err) => assert_eq!(err.kind(), io::ErrorKind::InvalidInput),
|
||||||
|
Ok(_) => panic!("dotted name must not acquire a lock"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_acquire_accepts_valid_name() {
|
||||||
|
let name = format!("bread-app-id-test-{}", std::process::id());
|
||||||
|
match try_acquire(&name).unwrap() {
|
||||||
|
Acquire::Acquired(_guard) => {}
|
||||||
|
Acquire::HeldByOther(_) => panic!("expected first acquire to succeed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn toggle_or_kill_starts_when_nothing_else_is_running() {
|
||||||
|
let name = format!("bread-app-toggle-test-{}", std::process::id());
|
||||||
|
match toggle_or_kill(&name).unwrap() {
|
||||||
|
Toggle::Started(_guard) => {}
|
||||||
|
Toggle::KilledExisting => panic!("expected to start as the first instance"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
bread-app/src/lib.rs
Normal file
67
bread-app/src/lib.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
//! GTK application bootstrap for bread desktop tools.
|
||||||
|
//!
|
||||||
|
//! New GTK tools should depend on this crate instead of copying a sixth
|
||||||
|
//! `main.rs` that wires a `com.breadway.*` application id, a
|
||||||
|
//! [`bread_utils::singleton`] lock, a layer-shell overlay, and a
|
||||||
|
//! `bread.command.<app>.**` listen loop.
|
||||||
|
//!
|
||||||
|
//! # What this is
|
||||||
|
//!
|
||||||
|
//! The pieces every bread GTK binary already copies:
|
||||||
|
//!
|
||||||
|
//! - [`application_id`] / [`parse_app_name`] — reverse-DNS id
|
||||||
|
//! (`com.breadway.breadbox`) and the same name used for the singleton
|
||||||
|
//! pid file.
|
||||||
|
//! - [`try_acquire`] / [`toggle_or_kill`] — [`bread_utils::singleton`]
|
||||||
|
//! wrappers that reject an invalid name before touching the lock.
|
||||||
|
//! - feature `gtk` — re-exports [`gtk_popup`] (`bread_utils::gtk_popup`)
|
||||||
|
//! for the full-screen overlay breadbox / breadclip / breadcast start
|
||||||
|
//! from.
|
||||||
|
//! - feature `bread-client` — [`listen_commands`] plus [`command_verb`] /
|
||||||
|
//! [`command_pattern`] so a tool can honor `bread.command.<app>.**`
|
||||||
|
//! without re-deriving the prefix strip.
|
||||||
|
//!
|
||||||
|
//! This crate does **not** migrate existing apps. Callers still own their
|
||||||
|
//! widgets, CSS, and clap. Screenshot / `--screenshot` helpers stay in
|
||||||
|
//! [`bread_utils::screenshot_cli`].
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! let _guard = match bread_app::try_acquire("breadbox")? {
|
||||||
|
//! bread_app::singleton::Acquire::Acquired(g) => g,
|
||||||
|
//! bread_app::singleton::Acquire::HeldByOther(_) => return Ok(()),
|
||||||
|
//! };
|
||||||
|
//! let app = gtk4::Application::builder()
|
||||||
|
//! .application_id(&bread_app::application_id("breadbox")?)
|
||||||
|
//! .build();
|
||||||
|
//!
|
||||||
|
//! #[cfg(feature = "gtk")]
|
||||||
|
//! app.connect_activate(|app| {
|
||||||
|
//! let window = bread_app::gtk_popup::new_overlay_window(app, "breadbox");
|
||||||
|
//! window.present();
|
||||||
|
//! });
|
||||||
|
//!
|
||||||
|
//! #[cfg(feature = "bread-client")]
|
||||||
|
//! let _commands = bread_app::listen_commands("box", |verb, event| {
|
||||||
|
//! // verb is the single segment after `bread.command.box.`
|
||||||
|
//! let _ = (verb, event);
|
||||||
|
//! })?;
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
pub use bread_utils::singleton;
|
||||||
|
|
||||||
|
#[cfg(feature = "gtk")]
|
||||||
|
pub use bread_utils::gtk_popup;
|
||||||
|
|
||||||
|
mod id;
|
||||||
|
|
||||||
|
pub use id::{application_id, parse_app_name, toggle_or_kill, try_acquire, InvalidAppId};
|
||||||
|
|
||||||
|
#[cfg(feature = "bread-client")]
|
||||||
|
mod command;
|
||||||
|
|
||||||
|
#[cfg(feature = "bread-client")]
|
||||||
|
pub use bread_utils::bread_client::{BreadClient, BreadEvent, Subscription};
|
||||||
|
#[cfg(feature = "bread-client")]
|
||||||
|
pub use command::{command_pattern, command_verb, listen_commands, parse_command_id};
|
||||||
27
bread-polkit/Cargo.toml
Normal file
27
bread-polkit/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "bread-polkit"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
description = "Themed PolicyKit authentication agent for the bread desktop"
|
||||||
|
repository = "https://git.breadway.dev/Breadway/bread-ecosystem"
|
||||||
|
keywords = ["polkit", "gtk4", "wayland"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "bread-polkit"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
bread-app = { path = "../bread-app", features = ["gtk"] }
|
||||||
|
bread-theme = { path = "../bread-theme", features = ["gtk"] }
|
||||||
|
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||||
|
serde = { workspace = true }
|
||||||
|
tokio = { version = "1", features = ["rt", "net", "sync", "time", "macros", "io-util", "process"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "std"] }
|
||||||
|
zbus = { version = "5", default-features = false, features = ["tokio"] }
|
||||||
12
bread-polkit/contrib/bread-polkit.desktop
Normal file
12
bread-polkit/contrib/bread-polkit.desktop
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Bread PolicyKit Agent
|
||||||
|
Comment=Themed PolicyKit authentication agent for the bread desktop
|
||||||
|
Exec=bread-polkit
|
||||||
|
Icon=dialog-password
|
||||||
|
Terminal=false
|
||||||
|
Categories=System;Security;
|
||||||
|
StartupNotify=false
|
||||||
|
X-GNOME-Autostart-Phase=Initialization
|
||||||
|
X-GNOME-AutoRestart=true
|
||||||
|
X-GNOME-Autostart-Notify=false
|
||||||
10
bread-polkit/contrib/hyprland.conf
Normal file
10
bread-polkit/contrib/hyprland.conf
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# bread-polkit — add to hyprland.conf
|
||||||
|
#
|
||||||
|
# Session authentication agent. Copy contrib/bread-polkit.desktop to
|
||||||
|
# ~/.config/autostart/ instead if you prefer XDG autostart.
|
||||||
|
|
||||||
|
exec-once = bread-polkit
|
||||||
|
|
||||||
|
# Optional: blur the overlay panel (namespace is bread-polkit).
|
||||||
|
layerrule = blur, bread-polkit
|
||||||
|
layerrule = ignorezero, bread-polkit
|
||||||
300
bread-polkit/src/agent.rs
Normal file
300
bread-polkit/src/agent.rs
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
//! Session-bus registration and the PolicyKit1 AuthenticationAgent.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use gtk4::glib;
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
use zbus::zvariant::{OwnedValue, Type, Value};
|
||||||
|
use zbus::{connection, interface, proxy, DBusError};
|
||||||
|
|
||||||
|
use bread_polkit::helper::{discover_transport, Transport};
|
||||||
|
use bread_polkit::identity::{current_uid, pick_user, read_passwd, users_from_uids, UnixUser};
|
||||||
|
use bread_polkit::session::session_id;
|
||||||
|
|
||||||
|
use crate::auth::{self, Outcome};
|
||||||
|
use crate::ui::{self, Prompt};
|
||||||
|
|
||||||
|
pub const OBJECT_PATH: &str = "/com/breadway/PolicyKit1/AuthenticationAgent";
|
||||||
|
|
||||||
|
/// Reply from the GTK prompt.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum UserAction {
|
||||||
|
Submit { username: String, password: String },
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, DBusError)]
|
||||||
|
#[zbus(prefix = "org.freedesktop.PolicyKit1.Error")]
|
||||||
|
enum AgentError {
|
||||||
|
#[zbus(error)]
|
||||||
|
ZBus(zbus::Error),
|
||||||
|
Failed(String),
|
||||||
|
Cancelled(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, Type)]
|
||||||
|
struct Identity {
|
||||||
|
kind: String,
|
||||||
|
details: HashMap<String, OwnedValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Type)]
|
||||||
|
struct Subject {
|
||||||
|
kind: String,
|
||||||
|
details: HashMap<String, OwnedValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proxy(
|
||||||
|
interface = "org.freedesktop.PolicyKit1.Authority",
|
||||||
|
default_service = "org.freedesktop.PolicyKit1",
|
||||||
|
default_path = "/org/freedesktop/PolicyKit1/Authority"
|
||||||
|
)]
|
||||||
|
trait Authority {
|
||||||
|
fn register_authentication_agent(
|
||||||
|
&self,
|
||||||
|
subject: &Subject,
|
||||||
|
locale: &str,
|
||||||
|
object_path: &str,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
fn unregister_authentication_agent(
|
||||||
|
&self,
|
||||||
|
subject: &Subject,
|
||||||
|
object_path: &str,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Agent {
|
||||||
|
transport: Transport,
|
||||||
|
pending: Arc<Mutex<Option<mpsc::Sender<UserAction>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interface(name = "org.freedesktop.PolicyKit1.AuthenticationAgent")]
|
||||||
|
impl Agent {
|
||||||
|
async fn begin_authentication(
|
||||||
|
&mut self,
|
||||||
|
action_id: String,
|
||||||
|
message: String,
|
||||||
|
_icon_name: String,
|
||||||
|
_details: HashMap<String, String>,
|
||||||
|
cookie: String,
|
||||||
|
identities: Vec<Identity>,
|
||||||
|
) -> Result<(), AgentError> {
|
||||||
|
tracing::info!(%action_id, %cookie, "BeginAuthentication");
|
||||||
|
|
||||||
|
let users = unix_users(&identities);
|
||||||
|
let username = pick_user(&users, current_uid())
|
||||||
|
.map(|u| u.name.clone())
|
||||||
|
.ok_or_else(|| AgentError::Failed("no unix-user identity".into()))?;
|
||||||
|
|
||||||
|
let (tx, mut rx) = mpsc::channel(4);
|
||||||
|
*self.pending.lock().await = Some(tx.clone());
|
||||||
|
|
||||||
|
let prompt = Prompt {
|
||||||
|
cookie: cookie.clone(),
|
||||||
|
message: message.clone(),
|
||||||
|
action_id: action_id.clone(),
|
||||||
|
username: username.clone(),
|
||||||
|
reply: tx,
|
||||||
|
};
|
||||||
|
invoke_ui(move || {
|
||||||
|
if let Some(app) = running_app() {
|
||||||
|
ui::show_prompt(&app, prompt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = self.drive_prompt(&cookie, &username, &mut rx).await;
|
||||||
|
|
||||||
|
*self.pending.lock().await = None;
|
||||||
|
let cookie_close = cookie.clone();
|
||||||
|
invoke_ui(move || ui::close_prompt(&cookie_close));
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel_authentication(&self, cookie: String) {
|
||||||
|
tracing::info!(%cookie, "CancelAuthentication");
|
||||||
|
if let Some(tx) = self.pending.lock().await.as_ref() {
|
||||||
|
let _ = tx.try_send(UserAction::Cancel);
|
||||||
|
}
|
||||||
|
invoke_ui(move || ui::close_prompt(&cookie));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Agent {
|
||||||
|
async fn drive_prompt(
|
||||||
|
&self,
|
||||||
|
cookie: &str,
|
||||||
|
default_user: &str,
|
||||||
|
rx: &mut mpsc::Receiver<UserAction>,
|
||||||
|
) -> Result<(), AgentError> {
|
||||||
|
loop {
|
||||||
|
match rx.recv().await {
|
||||||
|
None => {
|
||||||
|
return Err(AgentError::Cancelled("authentication prompt closed".into()));
|
||||||
|
}
|
||||||
|
Some(UserAction::Cancel) => {
|
||||||
|
return Err(AgentError::Cancelled("user cancelled".into()));
|
||||||
|
}
|
||||||
|
Some(UserAction::Submit { username, password }) => {
|
||||||
|
let user = if username.is_empty() {
|
||||||
|
default_user
|
||||||
|
} else {
|
||||||
|
username.as_str()
|
||||||
|
};
|
||||||
|
match auth::authenticate(&self.transport, user, cookie, &password).await {
|
||||||
|
Ok(Outcome::Success) => return Ok(()),
|
||||||
|
Ok(Outcome::Failure { message }) => {
|
||||||
|
let text = message
|
||||||
|
.unwrap_or_else(|| auth::default_failure_message().to_string());
|
||||||
|
let cookie = cookie.to_string();
|
||||||
|
invoke_ui(move || ui::show_retry(&cookie, &text));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("helper: {e:#}");
|
||||||
|
let text = e.to_string();
|
||||||
|
let cookie = cookie.to_string();
|
||||||
|
invoke_ui(move || ui::show_retry(&cookie, &text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_users(identities: &[Identity]) -> Vec<UnixUser> {
|
||||||
|
let mut uids = Vec::new();
|
||||||
|
for identity in identities {
|
||||||
|
if identity.kind != "unix-user" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(uid) = uid_from_details(&identity.details) {
|
||||||
|
uids.push(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users_from_uids(&uids, &read_passwd())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uid_from_details(details: &HashMap<String, OwnedValue>) -> Option<u32> {
|
||||||
|
let value = details.get("uid")?;
|
||||||
|
u32::try_from(value).ok().or_else(|| {
|
||||||
|
i32::try_from(value)
|
||||||
|
.ok()
|
||||||
|
.and_then(|n| u32::try_from(n).ok())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn running_app() -> Option<gtk4::Application> {
|
||||||
|
gtk4::gio::Application::default().and_then(|app| app.downcast::<gtk4::Application>().ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GTK thread-default context, captured in [`spawn`] so the dbus thread
|
||||||
|
/// can `invoke` onto the UI thread instead of its own empty context.
|
||||||
|
static GTK_CTX: OnceLock<glib::MainContext> = OnceLock::new();
|
||||||
|
|
||||||
|
fn invoke_ui(f: impl FnOnce() + Send + 'static) {
|
||||||
|
let ctx = GTK_CTX
|
||||||
|
.get()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(glib::MainContext::default);
|
||||||
|
ctx.invoke(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unix_session_subject(id: &str) -> Result<Subject> {
|
||||||
|
let value = Value::from(id.to_string());
|
||||||
|
let owned = OwnedValue::try_from(value).context("session-id variant")?;
|
||||||
|
let mut details = HashMap::new();
|
||||||
|
details.insert("session-id".into(), owned);
|
||||||
|
Ok(Subject {
|
||||||
|
kind: "unix-session".into(),
|
||||||
|
details,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the system-bus agent on a background thread. Returns once the
|
||||||
|
/// thread has been started; registration errors quit the GTK app.
|
||||||
|
///
|
||||||
|
/// Must be called from the GTK thread so the main context we capture is
|
||||||
|
/// the one driving the password prompt.
|
||||||
|
pub fn spawn() -> Result<()> {
|
||||||
|
let _ = GTK_CTX.set(glib::MainContext::default());
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("bread-polkit-dbus".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let rt = match tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
invoke_ui(move || {
|
||||||
|
eprintln!("bread-polkit: tokio runtime failed: {e}");
|
||||||
|
if let Some(app) = running_app() {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rt.block_on(async move {
|
||||||
|
if let Err(e) = run().await {
|
||||||
|
eprintln!("bread-polkit: {e:#}");
|
||||||
|
invoke_ui(|| {
|
||||||
|
if let Some(app) = running_app() {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.context("spawn dbus thread")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run() -> Result<()> {
|
||||||
|
let transport = discover_transport().context(
|
||||||
|
"no polkit helper: expected /run/polkit/agent-helper.socket \
|
||||||
|
or /usr/lib/polkit-1/polkit-agent-helper-1",
|
||||||
|
)?;
|
||||||
|
tracing::info!(?transport, "using polkit helper");
|
||||||
|
|
||||||
|
let session = session_id().context(
|
||||||
|
"no session id (XDG_SESSION_ID / /proc/self/sessionid); \
|
||||||
|
cannot register a session authentication agent",
|
||||||
|
)?;
|
||||||
|
let subject = unix_session_subject(&session)?;
|
||||||
|
let locale = std::env::var("LANG").unwrap_or_else(|_| "C".into());
|
||||||
|
|
||||||
|
let agent = Agent {
|
||||||
|
transport,
|
||||||
|
pending: Arc::new(Mutex::new(None)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let connection = connection::Builder::system()?
|
||||||
|
.serve_at(OBJECT_PATH, agent)?
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
.context("system bus")?;
|
||||||
|
|
||||||
|
let authority = AuthorityProxy::new(&connection)
|
||||||
|
.await
|
||||||
|
.context("PolicyKit1 authority proxy")?;
|
||||||
|
authority
|
||||||
|
.register_authentication_agent(&subject, &locale, OBJECT_PATH)
|
||||||
|
.await
|
||||||
|
.context("RegisterAuthenticationAgent")?;
|
||||||
|
tracing::info!(%session, "registered as PolicyKit authentication agent");
|
||||||
|
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
{
|
||||||
|
let _ = authority
|
||||||
|
.unregister_authentication_agent(&subject, OBJECT_PATH)
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
109
bread-polkit/src/auth.rs
Normal file
109
bread-polkit/src/auth.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
//! PAM conversation with the polkit agent helper.
|
||||||
|
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
use bread_polkit::helper::{parse_helper_line, HelperLine, Transport};
|
||||||
|
|
||||||
|
/// Outcome of one helper conversation.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Outcome {
|
||||||
|
Success,
|
||||||
|
Failure { message: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handshake + PAM loop for one password attempt.
|
||||||
|
pub async fn authenticate(
|
||||||
|
transport: &Transport,
|
||||||
|
username: &str,
|
||||||
|
cookie: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<Outcome> {
|
||||||
|
match transport {
|
||||||
|
Transport::Socket(path) => {
|
||||||
|
let mut stream = UnixStream::connect(path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("connect {}", path.display()))?;
|
||||||
|
stream.write_all(username.as_bytes()).await?;
|
||||||
|
stream.write_all(b"\n").await?;
|
||||||
|
stream.write_all(cookie.as_bytes()).await?;
|
||||||
|
stream.write_all(b"\n").await?;
|
||||||
|
let (reader, writer) = stream.into_split();
|
||||||
|
converse(BufReader::new(reader), writer, password).await
|
||||||
|
}
|
||||||
|
Transport::Exec(path) => {
|
||||||
|
let mut child = Command::new(path)
|
||||||
|
.arg(username)
|
||||||
|
.env("LC_ALL", "C")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| format!("spawn {}", path.display()))?;
|
||||||
|
let mut stdin = child.stdin.take().context("polkit helper has no stdin")?;
|
||||||
|
let stdout = child.stdout.take().context("polkit helper has no stdout")?;
|
||||||
|
stdin.write_all(cookie.as_bytes()).await?;
|
||||||
|
stdin.write_all(b"\n").await?;
|
||||||
|
let outcome = converse(BufReader::new(stdout), stdin, password).await;
|
||||||
|
let _ = child.wait().await;
|
||||||
|
outcome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn converse<R, W>(mut reader: BufReader<R>, mut writer: W, password: &str) -> Result<Outcome>
|
||||||
|
where
|
||||||
|
R: tokio::io::AsyncRead + Unpin,
|
||||||
|
W: tokio::io::AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
let mut last_info: Option<String> = None;
|
||||||
|
let mut line = String::new();
|
||||||
|
loop {
|
||||||
|
line.clear();
|
||||||
|
let n = reader.read_line(&mut line).await?;
|
||||||
|
if n == 0 {
|
||||||
|
return Ok(Outcome::Failure {
|
||||||
|
message: last_info.take(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
match parse_helper_line(&line) {
|
||||||
|
HelperLine::PromptEchoOff(_) => {
|
||||||
|
writer.write_all(password.as_bytes()).await?;
|
||||||
|
writer.write_all(b"\n").await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
}
|
||||||
|
HelperLine::PromptEchoOn(_) => {
|
||||||
|
// Visible prompt (username, etc.) — we already sent the
|
||||||
|
// identity in the handshake. An empty line is safer than
|
||||||
|
// echoing the password.
|
||||||
|
writer.write_all(b"\n").await?;
|
||||||
|
writer.flush().await?;
|
||||||
|
}
|
||||||
|
HelperLine::ErrorMsg(msg) | HelperLine::TextInfo(msg) => {
|
||||||
|
if !msg.is_empty() {
|
||||||
|
last_info = Some(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HelperLine::Success => return Ok(Outcome::Success),
|
||||||
|
HelperLine::Failure => {
|
||||||
|
return Ok(Outcome::Failure {
|
||||||
|
message: last_info.take(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
HelperLine::Other(other) => {
|
||||||
|
if !other.is_empty() {
|
||||||
|
tracing::debug!("helper: {other}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared default when the helper gives no `PAM_*` text on failure.
|
||||||
|
pub fn default_failure_message() -> &'static str {
|
||||||
|
"Authentication failed. Try again."
|
||||||
|
}
|
||||||
205
bread-polkit/src/helper.rs
Normal file
205
bread-polkit/src/helper.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
//! `polkit-agent-helper-1` transport and PAM line parser.
|
||||||
|
//!
|
||||||
|
//! Arch polkit 127+ talks over `/run/polkit/agent-helper.socket`. Older
|
||||||
|
//! builds still spawn the setuid helper at
|
||||||
|
//! `/usr/lib/polkit-1/polkit-agent-helper-1`. Prefer the socket when it
|
||||||
|
//! exists.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// How this agent will talk to polkit's helper.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Transport {
|
||||||
|
/// systemd socket-activated helper (polkit 127+).
|
||||||
|
Socket(PathBuf),
|
||||||
|
/// Legacy setuid helper binary.
|
||||||
|
Exec(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
const SOCKET_CANDIDATES: &[&str] = &["/run/polkit/agent-helper.socket"];
|
||||||
|
const HELPER_CANDIDATES: &[&str] = &[
|
||||||
|
"/usr/lib/polkit-1/polkit-agent-helper-1",
|
||||||
|
"/usr/libexec/polkit-1/polkit-agent-helper-1",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// One stdout line from the helper after the cookie handshake.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum HelperLine {
|
||||||
|
PromptEchoOff(String),
|
||||||
|
PromptEchoOn(String),
|
||||||
|
ErrorMsg(String),
|
||||||
|
TextInfo(String),
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick a live transport: `BREAD_POLKIT_SOCKET` / `BREAD_POLKIT_HELPER`
|
||||||
|
/// if set and present, otherwise the first existing well-known path.
|
||||||
|
pub fn discover_transport() -> Option<Transport> {
|
||||||
|
discover_transport_from(
|
||||||
|
std::env::var_os("BREAD_POLKIT_SOCKET")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.as_deref(),
|
||||||
|
std::env::var_os("BREAD_POLKIT_HELPER")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.as_deref(),
|
||||||
|
SOCKET_CANDIDATES,
|
||||||
|
HELPER_CANDIDATES,
|
||||||
|
|p| p.exists(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Testable discovery: `exists` is injected so unit tests do not need a
|
||||||
|
/// real `/run/polkit` socket.
|
||||||
|
pub fn discover_transport_from(
|
||||||
|
socket_override: Option<&Path>,
|
||||||
|
helper_override: Option<&Path>,
|
||||||
|
sockets: &[&str],
|
||||||
|
helpers: &[&str],
|
||||||
|
exists: impl Fn(&Path) -> bool,
|
||||||
|
) -> Option<Transport> {
|
||||||
|
if let Some(path) = socket_override {
|
||||||
|
if exists(path) {
|
||||||
|
return Some(Transport::Socket(path.to_path_buf()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for candidate in sockets {
|
||||||
|
let path = Path::new(candidate);
|
||||||
|
if exists(path) {
|
||||||
|
return Some(Transport::Socket(path.to_path_buf()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(path) = helper_override {
|
||||||
|
if exists(path) {
|
||||||
|
return Some(Transport::Exec(path.to_path_buf()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for candidate in helpers {
|
||||||
|
let path = Path::new(candidate);
|
||||||
|
if exists(path) {
|
||||||
|
return Some(Transport::Exec(path.to_path_buf()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse one helper protocol line. Prefix match is case-sensitive and
|
||||||
|
/// matches polkit's own `PAM_*` / `SUCCESS` / `FAILURE` tokens.
|
||||||
|
pub fn parse_helper_line(line: &str) -> HelperLine {
|
||||||
|
let line = line.trim_end_matches(['\r', '\n']);
|
||||||
|
if line == "SUCCESS" || line.starts_with("SUCCESS") {
|
||||||
|
return HelperLine::Success;
|
||||||
|
}
|
||||||
|
if line == "FAILURE" || line.starts_with("FAILURE") {
|
||||||
|
return HelperLine::Failure;
|
||||||
|
}
|
||||||
|
if let Some(rest) = line.strip_prefix("PAM_PROMPT_ECHO_OFF") {
|
||||||
|
return HelperLine::PromptEchoOff(rest.trim().to_string());
|
||||||
|
}
|
||||||
|
if let Some(rest) = line.strip_prefix("PAM_PROMPT_ECHO_ON") {
|
||||||
|
return HelperLine::PromptEchoOn(rest.trim().to_string());
|
||||||
|
}
|
||||||
|
if let Some(rest) = line.strip_prefix("PAM_ERROR_MSG") {
|
||||||
|
return HelperLine::ErrorMsg(rest.trim().to_string());
|
||||||
|
}
|
||||||
|
if let Some(rest) = line.strip_prefix("PAM_TEXT_INFO") {
|
||||||
|
return HelperLine::TextInfo(rest.trim().to_string());
|
||||||
|
}
|
||||||
|
HelperLine::Other(line.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_helper_line_known_tokens() {
|
||||||
|
assert_eq!(parse_helper_line("SUCCESS"), HelperLine::Success);
|
||||||
|
assert_eq!(parse_helper_line("SUCCESS\n"), HelperLine::Success);
|
||||||
|
assert_eq!(parse_helper_line("FAILURE"), HelperLine::Failure);
|
||||||
|
assert_eq!(
|
||||||
|
parse_helper_line("PAM_PROMPT_ECHO_OFF Password:"),
|
||||||
|
HelperLine::PromptEchoOff("Password:".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_helper_line("PAM_PROMPT_ECHO_OFF"),
|
||||||
|
HelperLine::PromptEchoOff(String::new())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_helper_line("PAM_PROMPT_ECHO_ON login:"),
|
||||||
|
HelperLine::PromptEchoOn("login:".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_helper_line("PAM_ERROR_MSG Authentication failure"),
|
||||||
|
HelperLine::ErrorMsg("Authentication failure".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_helper_line("PAM_TEXT_INFO Account locked"),
|
||||||
|
HelperLine::TextInfo("Account locked".into())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_helper_line("garbage"),
|
||||||
|
HelperLine::Other("garbage".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_prefers_socket_over_exec() {
|
||||||
|
let present: HashSet<PathBuf> = [
|
||||||
|
"/run/polkit/agent-helper.socket",
|
||||||
|
"/usr/lib/polkit-1/polkit-agent-helper-1",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.collect();
|
||||||
|
let got = discover_transport_from(None, None, SOCKET_CANDIDATES, HELPER_CANDIDATES, |p| {
|
||||||
|
present.contains(p)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
got,
|
||||||
|
Some(Transport::Socket(PathBuf::from(
|
||||||
|
"/run/polkit/agent-helper.socket"
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_falls_back_to_helper_binary() {
|
||||||
|
let present: HashSet<PathBuf> = ["/usr/lib/polkit-1/polkit-agent-helper-1"]
|
||||||
|
.into_iter()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.collect();
|
||||||
|
let got = discover_transport_from(None, None, SOCKET_CANDIDATES, HELPER_CANDIDATES, |p| {
|
||||||
|
present.contains(p)
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
got,
|
||||||
|
Some(Transport::Exec(PathBuf::from(
|
||||||
|
"/usr/lib/polkit-1/polkit-agent-helper-1"
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_override_socket_wins_when_present() {
|
||||||
|
let override_path = Path::new("/tmp/bread-polkit-test.sock");
|
||||||
|
let got = discover_transport_from(
|
||||||
|
Some(override_path),
|
||||||
|
None,
|
||||||
|
SOCKET_CANDIDATES,
|
||||||
|
HELPER_CANDIDATES,
|
||||||
|
|p| p == override_path,
|
||||||
|
);
|
||||||
|
assert_eq!(got, Some(Transport::Socket(override_path.to_path_buf())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_none_when_nothing_exists() {
|
||||||
|
let got =
|
||||||
|
discover_transport_from(None, None, SOCKET_CANDIDATES, HELPER_CANDIDATES, |_| false);
|
||||||
|
assert_eq!(got, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
128
bread-polkit/src/identity.rs
Normal file
128
bread-polkit/src/identity.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
//! Unix-user identities from a PolicyKit `BeginAuthentication` call.
|
||||||
|
|
||||||
|
/// A `unix-user` identity the agent can authenticate as.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UnixUser {
|
||||||
|
pub uid: u32,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up `uid` in a passwd-file dump (`name:x:uid:...` lines).
|
||||||
|
pub fn name_for_uid(uid: u32, passwd: &str) -> Option<String> {
|
||||||
|
for line in passwd.lines() {
|
||||||
|
if line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut parts = line.split(':');
|
||||||
|
let name = parts.next()?;
|
||||||
|
let _pw = parts.next()?;
|
||||||
|
let id = parts.next()?.parse::<u32>().ok()?;
|
||||||
|
if id == uid && !name.is_empty() {
|
||||||
|
return Some(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve each uid to a [`UnixUser`], falling back to `uid N` when
|
||||||
|
/// `/etc/passwd` has no name.
|
||||||
|
pub fn users_from_uids(uids: &[u32], passwd: &str) -> Vec<UnixUser> {
|
||||||
|
uids.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|uid| UnixUser {
|
||||||
|
uid,
|
||||||
|
name: name_for_uid(uid, passwd).unwrap_or_else(|| format!("uid {uid}")),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefer the process's own uid when it is in `users`, otherwise the first.
|
||||||
|
pub fn pick_user<'a>(users: &'a [UnixUser], current_uid: Option<u32>) -> Option<&'a UnixUser> {
|
||||||
|
if let Some(uid) = current_uid {
|
||||||
|
if let Some(user) = users.iter().find(|u| u.uid == uid) {
|
||||||
|
return Some(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Real uid from a `/proc/self/status` dump (`Uid:\t<real> ...`).
|
||||||
|
pub fn uid_from_status(status: &str) -> Option<u32> {
|
||||||
|
for line in status.lines() {
|
||||||
|
let Some(rest) = line.strip_prefix("Uid:") else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
return rest.split_whitespace().next()?.parse().ok();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current real uid, or `None` if `/proc/self/status` is unreadable.
|
||||||
|
pub fn current_uid() -> Option<u32> {
|
||||||
|
let status = std::fs::read_to_string("/proc/self/status").ok()?;
|
||||||
|
uid_from_status(&status)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contents of `/etc/passwd`, or empty if unreadable.
|
||||||
|
pub fn read_passwd() -> String {
|
||||||
|
std::fs::read_to_string("/etc/passwd").unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const PASSWD: &str = "\
|
||||||
|
# comment
|
||||||
|
root:x:0:0:root:/root:/bin/sh
|
||||||
|
alice:x:1000:1000:Alice:/home/alice:/bin/zsh
|
||||||
|
bob:x:1001:1001:Bob:/home/bob:/bin/bash
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn name_for_uid_reads_passwd_lines() {
|
||||||
|
assert_eq!(name_for_uid(0, PASSWD).as_deref(), Some("root"));
|
||||||
|
assert_eq!(name_for_uid(1000, PASSWD).as_deref(), Some("alice"));
|
||||||
|
assert_eq!(name_for_uid(99, PASSWD), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn users_from_uids_falls_back_to_uid_label() {
|
||||||
|
let users = users_from_uids(&[1000, 42], PASSWD);
|
||||||
|
assert_eq!(
|
||||||
|
users,
|
||||||
|
vec![
|
||||||
|
UnixUser {
|
||||||
|
uid: 1000,
|
||||||
|
name: "alice".into()
|
||||||
|
},
|
||||||
|
UnixUser {
|
||||||
|
uid: 42,
|
||||||
|
name: "uid 42".into()
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pick_user_prefers_current_uid() {
|
||||||
|
let users = users_from_uids(&[0, 1000], PASSWD);
|
||||||
|
let picked = pick_user(&users, Some(1000)).unwrap();
|
||||||
|
assert_eq!(picked.name, "alice");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pick_user_falls_back_to_first() {
|
||||||
|
let users = users_from_uids(&[0, 1000], PASSWD);
|
||||||
|
let picked = pick_user(&users, Some(7)).unwrap();
|
||||||
|
assert_eq!(picked.name, "root");
|
||||||
|
assert!(pick_user(&[], Some(1000)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uid_from_status_reads_real_uid() {
|
||||||
|
let status = "Name:\tbread-polkit\nUid:\t1000\t1000\t1000\t1000\n";
|
||||||
|
assert_eq!(uid_from_status(status), Some(1000));
|
||||||
|
assert_eq!(uid_from_status("Name:\tfoo\n"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
bread-polkit/src/lib.rs
Normal file
10
bread-polkit/src/lib.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
//! Non-GTK PolicyKit helper logic for `bread-polkit`.
|
||||||
|
//!
|
||||||
|
//! The binary (`bread-polkit`) registers as a session authentication
|
||||||
|
//! agent and shows a themed password prompt. This library is the
|
||||||
|
//! transport / identity / session parsing that can be unit-tested
|
||||||
|
//! without a display.
|
||||||
|
|
||||||
|
pub mod helper;
|
||||||
|
pub mod identity;
|
||||||
|
pub mod session;
|
||||||
94
bread-polkit/src/main.rs
Normal file
94
bread-polkit/src/main.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
//! bread-polkit — themed PolicyKit authentication agent.
|
||||||
|
//!
|
||||||
|
//! Registers on the `org.freedesktop.PolicyKit1.AuthenticationAgent`
|
||||||
|
//! interface and shows a bread-theme GTK4 password prompt. This is an
|
||||||
|
//! agent, not a wrapper that execs `polkit-gnome`.
|
||||||
|
//!
|
||||||
|
//! Autostart: copy `contrib/bread-polkit.desktop` to
|
||||||
|
//! `~/.config/autostart/`, or add `exec-once = bread-polkit` to Hyprland.
|
||||||
|
|
||||||
|
mod agent;
|
||||||
|
mod auth;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
use bread_app::singleton::Acquire;
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
|
||||||
|
const APP_NAME: &str = "bread-polkit";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let arg = std::env::args().nth(1);
|
||||||
|
match arg.as_deref() {
|
||||||
|
Some("-h") | Some("--help") => {
|
||||||
|
print_help();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some("-V") | Some("--version") => {
|
||||||
|
println!("bread-polkit {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(other) => {
|
||||||
|
eprintln!("bread-polkit: unknown argument '{other}'");
|
||||||
|
print_help();
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||||
|
)
|
||||||
|
.with_target(false)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let _guard = match bread_app::try_acquire(APP_NAME) {
|
||||||
|
Ok(Acquire::Acquired(g)) => Some(g),
|
||||||
|
Ok(Acquire::HeldByOther(pid)) => {
|
||||||
|
eprintln!("bread-polkit: already running (pid {pid:?})");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("bread-polkit: singleton lock unavailable ({e}); continuing");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_id = bread_app::application_id(APP_NAME).expect("static app name");
|
||||||
|
let app = gtk4::Application::builder().application_id(&app_id).build();
|
||||||
|
|
||||||
|
app.connect_activate(|app| {
|
||||||
|
bread_theme::gtk::apply_shared();
|
||||||
|
bread_theme::gtk::apply_app_css(ui::app_css);
|
||||||
|
// No window until polkit asks; hold so GApplication stays alive.
|
||||||
|
std::mem::forget(app.hold());
|
||||||
|
if let Err(e) = agent::spawn() {
|
||||||
|
eprintln!("bread-polkit: {e:#}");
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
print!(
|
||||||
|
"\
|
||||||
|
bread-polkit — themed PolicyKit authentication agent
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
bread-polkit
|
||||||
|
bread-polkit --help
|
||||||
|
bread-polkit --version
|
||||||
|
|
||||||
|
Autostart (pick one):
|
||||||
|
cp contrib/bread-polkit.desktop ~/.config/autostart/
|
||||||
|
exec-once = bread-polkit # Hyprland
|
||||||
|
|
||||||
|
The agent talks to the polkit1 AuthenticationAgent API and prompts for
|
||||||
|
a password. It does not exec polkit-gnome. Not a bakery product; not
|
||||||
|
on the BOS ISO lockfile.
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
49
bread-polkit/src/session.rs
Normal file
49
bread-polkit/src/session.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
//! Session subject for `RegisterAuthenticationAgent`.
|
||||||
|
|
||||||
|
/// Logind session id from `XDG_SESSION_ID`, falling back to
|
||||||
|
/// `/proc/self/sessionid` when the kernel has one.
|
||||||
|
pub fn session_id() -> Option<String> {
|
||||||
|
let xdg = std::env::var("XDG_SESSION_ID").ok();
|
||||||
|
let proc = std::fs::read_to_string("/proc/self/sessionid").ok();
|
||||||
|
session_id_from(xdg.as_deref(), proc.as_deref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `None` when both sources are empty or the kernel reports the
|
||||||
|
/// unsigned `-1` sentinel (`4294967295`) meaning "no session".
|
||||||
|
pub fn session_id_from(xdg: Option<&str>, proc_sessionid: Option<&str>) -> Option<String> {
|
||||||
|
if let Some(id) = xdg.map(str::trim).filter(|s| !s.is_empty()) {
|
||||||
|
return Some(id.to_string());
|
||||||
|
}
|
||||||
|
let raw = proc_sessionid?.trim();
|
||||||
|
if raw.is_empty() || raw == "4294967295" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(raw.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefers_xdg_session_id() {
|
||||||
|
assert_eq!(session_id_from(Some("3"), Some("7")).as_deref(), Some("3"));
|
||||||
|
assert_eq!(
|
||||||
|
session_id_from(Some(" 3 "), Some("7")).as_deref(),
|
||||||
|
Some("3")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_proc_sessionid() {
|
||||||
|
assert_eq!(session_id_from(Some(""), Some("7")).as_deref(), Some("7"));
|
||||||
|
assert_eq!(session_id_from(None, Some("7\n")).as_deref(), Some("7"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unset_kernel_session() {
|
||||||
|
assert_eq!(session_id_from(None, Some("4294967295")), None);
|
||||||
|
assert_eq!(session_id_from(Some(""), Some("")), None);
|
||||||
|
assert_eq!(session_id_from(None, None), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
285
bread-polkit/src/ui.rs
Normal file
285
bread-polkit/src/ui.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
//! GTK4 password prompt, themed with bread-theme.
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gtk4::gdk::Key;
|
||||||
|
use gtk4::glib::{self, Propagation};
|
||||||
|
use gtk4::prelude::*;
|
||||||
|
use gtk4::{
|
||||||
|
Align, Application, ApplicationWindow, Box as GBox, Button, Entry, EventControllerKey, Label,
|
||||||
|
Orientation,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bread_theme::tokens;
|
||||||
|
|
||||||
|
use crate::agent::UserAction;
|
||||||
|
|
||||||
|
const PANEL_WIDTH: i32 = 400;
|
||||||
|
|
||||||
|
struct Active {
|
||||||
|
cookie: String,
|
||||||
|
window: ApplicationWindow,
|
||||||
|
password: Entry,
|
||||||
|
error: Label,
|
||||||
|
reply: tokio::sync::mpsc::Sender<UserAction>,
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ACTIVE: RefCell<Option<Active>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App-specific rules layered on the shared bread-theme stylesheet.
|
||||||
|
pub fn app_css() -> String {
|
||||||
|
format!(
|
||||||
|
".polkit-panel {{\
|
||||||
|
background-color: @surface; color: @on-surface;\
|
||||||
|
border-radius: {r}px; padding: {pad}px;\
|
||||||
|
min-width: {w}px;\
|
||||||
|
}}\n\
|
||||||
|
.polkit-title {{ font-size: 1.4em; font-weight: bold; }}\n\
|
||||||
|
.polkit-message {{ opacity: 0.85; }}\n\
|
||||||
|
.polkit-identity {{ opacity: 0.7; font-size: {sec}px; }}\n\
|
||||||
|
.polkit-error {{ color: @on-red; }}\n\
|
||||||
|
.polkit-buttons {{ padding-top: {sm}px; }}\n",
|
||||||
|
r = tokens::RADIUS_PRIMARY,
|
||||||
|
pad = tokens::SPACE_XL,
|
||||||
|
w = PANEL_WIDTH,
|
||||||
|
sec = tokens::FONT_SIZE_SECONDARY,
|
||||||
|
sm = tokens::SPACE_SM,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Prompt {
|
||||||
|
pub cookie: String,
|
||||||
|
pub message: String,
|
||||||
|
pub action_id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub reply: tokio::sync::mpsc::Sender<UserAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show (or replace) the password overlay for this cookie.
|
||||||
|
pub fn show_prompt(app: &Application, prompt: Prompt) {
|
||||||
|
close_if_other_cookie(&prompt.cookie);
|
||||||
|
|
||||||
|
if ACTIVE.with(|a| {
|
||||||
|
a.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|active| active.cookie == prompt.cookie)
|
||||||
|
}) {
|
||||||
|
present_existing(&prompt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = bread_app::gtk_popup::new_overlay_window(app, "bread-polkit");
|
||||||
|
|
||||||
|
let panel = GBox::new(Orientation::Vertical, tokens::SPACE_MD as i32);
|
||||||
|
panel.add_css_class("polkit-panel");
|
||||||
|
panel.add_css_class("card");
|
||||||
|
panel.set_halign(Align::Center);
|
||||||
|
panel.set_valign(Align::Center);
|
||||||
|
panel.set_size_request(PANEL_WIDTH, -1);
|
||||||
|
|
||||||
|
let title = Label::new(Some("Authentication required"));
|
||||||
|
title.add_css_class("polkit-title");
|
||||||
|
title.add_css_class("page-title");
|
||||||
|
title.set_halign(Align::Start);
|
||||||
|
title.set_wrap(true);
|
||||||
|
panel.append(&title);
|
||||||
|
|
||||||
|
let message = if prompt.message.trim().is_empty() {
|
||||||
|
prompt.action_id.clone()
|
||||||
|
} else {
|
||||||
|
prompt.message.clone()
|
||||||
|
};
|
||||||
|
let msg = Label::new(Some(&message));
|
||||||
|
msg.add_css_class("polkit-message");
|
||||||
|
msg.set_halign(Align::Start);
|
||||||
|
msg.set_wrap(true);
|
||||||
|
msg.set_xalign(0.0);
|
||||||
|
panel.append(&msg);
|
||||||
|
|
||||||
|
if !prompt.username.is_empty() {
|
||||||
|
let identity = Label::new(Some(&format!("Authenticating as {}", prompt.username)));
|
||||||
|
identity.add_css_class("polkit-identity");
|
||||||
|
identity.add_css_class("dim-label");
|
||||||
|
identity.set_halign(Align::Start);
|
||||||
|
panel.append(&identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = Label::new(None);
|
||||||
|
error.add_css_class("polkit-error");
|
||||||
|
error.set_halign(Align::Start);
|
||||||
|
error.set_wrap(true);
|
||||||
|
error.set_visible(false);
|
||||||
|
panel.append(&error);
|
||||||
|
|
||||||
|
let password = Entry::builder()
|
||||||
|
.visibility(false)
|
||||||
|
.input_purpose(gtk4::InputPurpose::Password)
|
||||||
|
.placeholder_text("Password")
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
panel.append(&password);
|
||||||
|
|
||||||
|
let buttons = GBox::new(Orientation::Horizontal, tokens::SPACE_SM as i32);
|
||||||
|
buttons.add_css_class("polkit-buttons");
|
||||||
|
buttons.set_halign(Align::End);
|
||||||
|
let cancel = Button::with_label("Cancel");
|
||||||
|
cancel.add_css_class("flat");
|
||||||
|
let confirm = Button::with_label("Authenticate");
|
||||||
|
confirm.add_css_class("suggested-action");
|
||||||
|
buttons.append(&cancel);
|
||||||
|
buttons.append(&confirm);
|
||||||
|
panel.append(&buttons);
|
||||||
|
|
||||||
|
window.set_child(Some(&panel));
|
||||||
|
|
||||||
|
let reply = prompt.reply.clone();
|
||||||
|
let cookie = prompt.cookie.clone();
|
||||||
|
let username = prompt.username.clone();
|
||||||
|
|
||||||
|
let submit = {
|
||||||
|
let password = password.clone();
|
||||||
|
let reply = reply.clone();
|
||||||
|
let username = username.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
let secret = password.text().to_string();
|
||||||
|
password.set_text("");
|
||||||
|
let _ = reply.try_send(UserAction::Submit {
|
||||||
|
username: username.clone(),
|
||||||
|
password: secret,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let cancel_fn = {
|
||||||
|
let reply = reply.clone();
|
||||||
|
let window = window.clone();
|
||||||
|
Rc::new(move || {
|
||||||
|
let _ = reply.try_send(UserAction::Cancel);
|
||||||
|
window.close();
|
||||||
|
ACTIVE.with(|a| a.replace(None));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
confirm.connect_clicked({
|
||||||
|
let submit = submit.clone();
|
||||||
|
move |_| submit()
|
||||||
|
});
|
||||||
|
password.connect_activate({
|
||||||
|
let submit = submit.clone();
|
||||||
|
move |_| submit()
|
||||||
|
});
|
||||||
|
cancel.connect_clicked({
|
||||||
|
let cancel_fn = cancel_fn.clone();
|
||||||
|
move |_| cancel_fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
let keys = EventControllerKey::new();
|
||||||
|
keys.connect_key_pressed({
|
||||||
|
let cancel_fn = cancel_fn.clone();
|
||||||
|
move |_, key, _, _| {
|
||||||
|
if key == Key::Escape {
|
||||||
|
cancel_fn();
|
||||||
|
Propagation::Stop
|
||||||
|
} else {
|
||||||
|
Propagation::Proceed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.add_controller(keys);
|
||||||
|
|
||||||
|
bread_app::gtk_popup::close_on_outside_click(&window, &panel, {
|
||||||
|
let cancel_fn = cancel_fn.clone();
|
||||||
|
move || cancel_fn()
|
||||||
|
});
|
||||||
|
|
||||||
|
window.connect_close_request({
|
||||||
|
let reply = reply.clone();
|
||||||
|
move |_| {
|
||||||
|
let closing_ours = ACTIVE.with(|a| {
|
||||||
|
a.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|active| active.cookie == cookie)
|
||||||
|
});
|
||||||
|
if closing_ours {
|
||||||
|
let _ = reply.try_send(UserAction::Cancel);
|
||||||
|
ACTIVE.with(|a| a.replace(None));
|
||||||
|
}
|
||||||
|
glib::Propagation::Proceed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ACTIVE.with(|a| {
|
||||||
|
*a.borrow_mut() = Some(Active {
|
||||||
|
cookie: prompt.cookie,
|
||||||
|
window: window.clone(),
|
||||||
|
password: password.clone(),
|
||||||
|
error,
|
||||||
|
reply,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.present();
|
||||||
|
password.grab_focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn present_existing(prompt: &Prompt) {
|
||||||
|
ACTIVE.with(|a| {
|
||||||
|
if let Some(active) = a.borrow_mut().as_mut() {
|
||||||
|
active.reply = prompt.reply.clone();
|
||||||
|
active.username = prompt.username.clone();
|
||||||
|
active.error.set_visible(false);
|
||||||
|
active.password.set_text("");
|
||||||
|
active.window.present();
|
||||||
|
active.password.grab_focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show a retry message on the open dialog for `cookie`.
|
||||||
|
pub fn show_retry(cookie: &str, message: &str) {
|
||||||
|
ACTIVE.with(|a| {
|
||||||
|
let mut guard = a.borrow_mut();
|
||||||
|
let Some(active) = guard.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if active.cookie != cookie {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active.error.set_label(message);
|
||||||
|
active.error.set_visible(true);
|
||||||
|
active.password.set_text("");
|
||||||
|
active.window.present();
|
||||||
|
active.password.grab_focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the dialog if it is still showing `cookie`.
|
||||||
|
pub fn close_prompt(cookie: &str) {
|
||||||
|
ACTIVE.with(|a| {
|
||||||
|
let Some(active) = a.borrow_mut().take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if active.cookie == cookie {
|
||||||
|
active.window.close();
|
||||||
|
} else {
|
||||||
|
*a.borrow_mut() = Some(active);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_if_other_cookie(cookie: &str) {
|
||||||
|
ACTIVE.with(|a| {
|
||||||
|
let Some(active) = a.borrow_mut().take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if active.cookie == cookie {
|
||||||
|
*a.borrow_mut() = Some(active);
|
||||||
|
} else {
|
||||||
|
active.window.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue