Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
297207a7aa | ||
|
|
3049a20be3 | ||
|
|
700fc3ed16 | ||
|
|
9c4205b1b2 | ||
|
|
cd7465a18b | ||
|
|
e4c12e9b62 | ||
|
|
241dfd17a7 | ||
|
|
96d666b3cb | ||
|
|
1806c6f912 |
17 changed files with 1998 additions and 775 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -35,3 +35,6 @@ logs/
|
|||
|
||||
# Internal design documents (not for distribution)
|
||||
aster-brief.md
|
||||
|
||||
# graphify knowledge-graph output (local tool cache, not for commit)
|
||||
graphify-out/
|
||||
|
|
|
|||
197
Cargo.lock
generated
197
Cargo.lock
generated
|
|
@ -123,9 +123,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.91"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -152,8 +152,8 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
|||
|
||||
[[package]]
|
||||
name = "bread-screenshots"
|
||||
version = "0.7.2"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||
version = "0.7.4"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#a9754d90ed32efcc26765abd01c9f441bfb01b1e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bread-utils",
|
||||
|
|
@ -185,8 +185,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bread-theme"
|
||||
version = "0.7.2"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||
version = "0.7.4"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"gtk4",
|
||||
|
|
@ -196,8 +196,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bread-utils"
|
||||
version = "0.7.2"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||
version = "0.7.4"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#a9754d90ed32efcc26765abd01c9f441bfb01b1e"
|
||||
dependencies = [
|
||||
"bread-shared 0.7.0",
|
||||
"dirs 5.0.1",
|
||||
|
|
@ -207,7 +207,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "breadbar"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bread-screenshots",
|
||||
|
|
@ -265,7 +265,7 @@ checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
|
|||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -286,9 +286,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.4"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -296,9 +296,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.2"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
|
@ -564,9 +564,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
|
||||
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
|
@ -579,9 +579,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
|
@ -589,15 +589,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
|
|
@ -606,9 +606,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
|
|
@ -625,32 +625,32 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
|
||||
checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
|
@ -685,7 +685,7 @@ dependencies = [
|
|||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -718,7 +718,7 @@ dependencies = [
|
|||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -771,7 +771,7 @@ dependencies = [
|
|||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -835,7 +835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -846,7 +846,7 @@ checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
|
|||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -867,7 +867,7 @@ checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04"
|
|||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -898,7 +898,7 @@ dependencies = [
|
|||
"graphene-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -924,9 +924,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gtk4-layer-shell"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a"
|
||||
checksum = "17c28ea0f4676fdaaae7ff2413a24d0d35c8657424f84856c1103c73454c9da4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"gdk4",
|
||||
|
|
@ -939,15 +939,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gtk4-layer-shell-sys"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9"
|
||||
checksum = "bcf19bb884ef0ef55b9e6b2b369c39b4fcc0c41e3a0c1cbc8c267720338b690b"
|
||||
dependencies = [
|
||||
"gdk4-sys",
|
||||
"glib-sys",
|
||||
"gtk4-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -978,7 +978,7 @@ dependencies = [
|
|||
"gsk4-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1058,9 +1058,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.103"
|
||||
version = "0.3.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
|
|
@ -1093,9 +1093,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.18"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||
checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
|
@ -1214,7 +1214,7 @@ dependencies = [
|
|||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"system-deps 7.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1243,9 +1243,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
|||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
|
|
@ -1321,7 +1321,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
|||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"libredox",
|
||||
"thiserror 2.0.19",
|
||||
"thiserror 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1624,6 +1624,19 @@ dependencies = [
|
|||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83779a5c956bcb6ba627a4ecf0a9d7625db47d7537e0892d97f712ac995648a3"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
"pkg-config",
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
|
|
@ -1654,11 +1667,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.19"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.19",
|
||||
"thiserror-impl 2.0.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1674,9 +1687,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.19"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -1728,13 +1741,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.1"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1919,9 +1932,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.24.0"
|
||||
version = "1.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
||||
checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
|
|
@ -1943,9 +1956,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
|
|
@ -1956,9 +1969,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -1966,9 +1979,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
|
|
@ -1979,9 +1992,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.126"
|
||||
version = "0.2.127"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -2087,9 +2100,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.28"
|
||||
version = "0.8.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xmlwriter"
|
||||
|
|
@ -2099,9 +2112,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
|||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.18.0"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a"
|
||||
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-recursion",
|
||||
|
|
@ -2129,14 +2142,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.18.0"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119"
|
||||
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
|
|
@ -2153,6 +2166,15 @@ dependencies = [
|
|||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcheapstr"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
|
@ -2161,40 +2183,41 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
|||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.13.1"
|
||||
version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911"
|
||||
checksum = "b5e28c25bd8bb8da5a1f3e7065d0c156b9ee9a7973adf78b0e35eaefdf3b1b5c"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 1.0.4",
|
||||
"zcheapstr",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.13.1"
|
||||
version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12"
|
||||
checksum = "d496a145685283b67e232bd9e47377f6b60ad9d51e3601b23867f77c42477f96"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.5.0"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
|
||||
checksum = "629d80ece222cad20fe0e8741be493c4ab166acf3b85341bdc2cdbcfd8f3c2d6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.119",
|
||||
"syn 3.0.3",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "breadbar"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2021"
|
||||
description = "Minimal status bar and notification daemon for Hyprland on Wayland"
|
||||
license = "MIT"
|
||||
|
|
@ -10,17 +10,17 @@ keywords = ["wayland", "hyprland", "bar", "status-bar", "gtk4"]
|
|||
categories = ["gui"]
|
||||
|
||||
[dependencies]
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] }
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] }
|
||||
# Widget rendering client: bread-utils::BreadClient (emit/request/subscribe)
|
||||
# for talking to breadd's IPC socket, and bread-shared purely for the
|
||||
# WidgetSpec/WidgetNode wire types so we deserialize into real structs
|
||||
# instead of hand-parsing serde_json::Value. See src/widgets/.
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["bread-client"] }
|
||||
# v0.8.0-rc.1 carries bread_shared::widget; keep this bread tag even if
|
||||
# ecosystem crates move independently.
|
||||
bread-shared = { git = "https://git.breadway.dev/Breadway/bread", tag = "v0.8.0-rc.1" }
|
||||
# Capture primitives for `--screenshot` mode — see src/screenshot.rs.
|
||||
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
|
||||
bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4" }
|
||||
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||
gtk4-layer-shell = "0.8"
|
||||
relm4 = { version = "0.11", features = ["macros"] }
|
||||
|
|
|
|||
1
assets/GPU.svg
Normal file
1
assets/GPU.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="18" height="12" rx="2"/><rect x="8" y="9" width="6" height="6" rx="1"/><path d="M7 18v2"/><path d="M12 18v2"/><path d="M17 18v2"/><path d="M17 6V4"/></svg>
|
||||
|
After Width: | Height: | Size: 362 B |
|
|
@ -1,11 +1,21 @@
|
|||
use crate::{App, AppInput};
|
||||
use relm4::ComponentSender;
|
||||
|
||||
pub fn now() -> gtk4::glib::DateTime {
|
||||
gtk4::glib::DateTime::now_local().expect("local time")
|
||||
}
|
||||
|
||||
pub fn time() -> String {
|
||||
let dt = now();
|
||||
format!("{:02}:{:02}", dt.hour(), dt.minute())
|
||||
}
|
||||
|
||||
pub fn date() -> String {
|
||||
now().format("%a %d/%m").expect("date format").to_string()
|
||||
}
|
||||
|
||||
pub fn current() -> String {
|
||||
let dt = gtk4::glib::DateTime::now_local().expect("local time");
|
||||
let date = dt.format("%a %d/%m").expect("date format");
|
||||
let time = format!("{:02}:{:02}", dt.hour(), dt.minute());
|
||||
format!("{} {}", date, time)
|
||||
format!("{} {}", date(), time())
|
||||
}
|
||||
|
||||
pub fn spawn_ticker(sender: ComponentSender<App>) {
|
||||
|
|
|
|||
|
|
@ -121,11 +121,29 @@ pub fn spawn_set_brightness(v: f64) {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn spawn_set_sink(name: String) {
|
||||
pub fn spawn_set_sink(name: String, sender: ComponentSender<App>) {
|
||||
relm4::spawn(async move {
|
||||
let _ = tokio::process::Command::new("pactl")
|
||||
.args(["set-default-sink", &name])
|
||||
.output()
|
||||
.await;
|
||||
// Default sink alone leaves already-playing streams on the old
|
||||
// device — move them too so the switch is audible immediately.
|
||||
if let Ok(o) = tokio::process::Command::new("pactl")
|
||||
.args(["list", "short", "sink-inputs"])
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
for line in String::from_utf8_lossy(&o.stdout).lines() {
|
||||
let Some(id) = line.split_whitespace().next() else {
|
||||
continue;
|
||||
};
|
||||
let _ = tokio::process::Command::new("pactl")
|
||||
.args(["move-sink-input", id, &name])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
spawn_load(sender);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ pub const WIFI_MEDIUM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "
|
|||
pub const WIFI_WEAK: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Weak.svg"));
|
||||
pub const WIFI_OFF: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Disconnect.svg"));
|
||||
|
||||
/// Adwaita symbolic names — these are drawn for 16px status bars, not our
|
||||
/// hand-cropped Lucide arcs.
|
||||
pub const WIFI_ICON_EXCELLENT: &str = "network-wireless-signal-excellent-symbolic";
|
||||
pub const WIFI_ICON_GOOD: &str = "network-wireless-signal-good-symbolic";
|
||||
pub const WIFI_ICON_OK: &str = "network-wireless-signal-ok-symbolic";
|
||||
pub const WIFI_ICON_WEAK: &str = "network-wireless-signal-weak-symbolic";
|
||||
pub const WIFI_ICON_OFF: &str = "network-wireless-offline-symbolic";
|
||||
|
||||
pub const BAT_HIGH: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 3 Bars.svg"));
|
||||
pub const BAT_MID: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 2 Bars.svg"));
|
||||
pub const BAT_LOW: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 1 Bar.svg"));
|
||||
|
|
@ -65,6 +73,7 @@ pub struct Stats {
|
|||
pub gpu_temp: Option<f32>,
|
||||
pub net_rx_kbs: f32,
|
||||
pub net_tx_kbs: f32,
|
||||
pub volume_pct: u8,
|
||||
}
|
||||
|
||||
struct CpuSnapshot {
|
||||
|
|
@ -76,7 +85,7 @@ static PREV_CPU: OnceLock<Mutex<CpuSnapshot>> = OnceLock::new();
|
|||
static BAT_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
static AC_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
static WIFI_CACHE: LazyLock<Mutex<(String, &'static str)>> =
|
||||
LazyLock::new(|| Mutex::new(("—".to_string(), WIFI_OFF)));
|
||||
LazyLock::new(|| Mutex::new(("—".to_string(), WIFI_ICON_OFF)));
|
||||
static WIFI_TICK: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
fn read_cpu() -> f32 {
|
||||
|
|
@ -271,7 +280,7 @@ fn wifi_iface() -> Option<&'static str> {
|
|||
|
||||
async fn read_wifi() -> (String, &'static str) {
|
||||
let Some(iface) = wifi_iface() else {
|
||||
return ("—".into(), WIFI_OFF);
|
||||
return ("—".into(), WIFI_ICON_OFF);
|
||||
};
|
||||
|
||||
let link_out = tokio::process::Command::new("iw")
|
||||
|
|
@ -281,7 +290,7 @@ async fn read_wifi() -> (String, &'static str) {
|
|||
.ok();
|
||||
let link_stdout = match link_out {
|
||||
Some(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(),
|
||||
_ => return ("—".into(), WIFI_OFF),
|
||||
_ => return ("—".into(), WIFI_ICON_OFF),
|
||||
};
|
||||
|
||||
let mut ssid = None;
|
||||
|
|
@ -296,13 +305,14 @@ async fn read_wifi() -> (String, &'static str) {
|
|||
}
|
||||
|
||||
let Some(ssid) = ssid else {
|
||||
return ("—".into(), WIFI_OFF);
|
||||
return ("—".into(), WIFI_ICON_OFF);
|
||||
};
|
||||
|
||||
let icon = match rssi {
|
||||
Some(r) if r >= -55 => WIFI_STRONG,
|
||||
Some(r) if r >= -70 => WIFI_MEDIUM,
|
||||
_ => WIFI_WEAK,
|
||||
Some(r) if r >= -55 => WIFI_ICON_EXCELLENT,
|
||||
Some(r) if r >= -70 => WIFI_ICON_GOOD,
|
||||
Some(r) if r >= -80 => WIFI_ICON_OK,
|
||||
_ => WIFI_ICON_WEAK,
|
||||
};
|
||||
|
||||
(ssid, icon)
|
||||
|
|
@ -392,7 +402,7 @@ fn read_crumbs_profile() -> Option<String> {
|
|||
for line in text.lines() {
|
||||
if let Some(rest) = line.trim().strip_prefix("profile") {
|
||||
let val = rest
|
||||
.trim_start_matches(|c: char| c == ' ' || c == '=')
|
||||
.trim_start_matches([' ', '='])
|
||||
.trim_matches('"');
|
||||
if !val.is_empty() {
|
||||
return Some(val.to_string());
|
||||
|
|
@ -413,7 +423,8 @@ pub async fn poll() -> Stats {
|
|||
let power_watts = read_power();
|
||||
let power = power_watts.map_or_else(|| "—W".into(), |w| format!("{w:.1}W"));
|
||||
let pct = read_battery();
|
||||
let bat = pct.map_or_else(|| "—".into(), |p| format!("{p}%"));
|
||||
// Demo bar prints the bare number ("83"), not "83%".
|
||||
let bat = pct.map_or_else(|| "—".into(), |p| format!("{p}"));
|
||||
let bat_icon = pct.map_or(BAT_MID, bat_level_icon);
|
||||
let ac_connected = read_ac();
|
||||
// BT and WiFi both refresh every 8 cycles (~16 s); cache in between.
|
||||
|
|
@ -442,6 +453,7 @@ pub async fn poll() -> Stats {
|
|||
let gpu_usage = read_gpu_usage();
|
||||
let gpu_temp = read_gpu_temp();
|
||||
let (net_rx_kbs, net_tx_kbs) = read_net_throughput();
|
||||
let volume_pct = read_volume_pct();
|
||||
Stats {
|
||||
cpu: format!("{cpu:.0}%"),
|
||||
cpu_pct: cpu,
|
||||
|
|
@ -465,9 +477,30 @@ pub async fn poll() -> Stats {
|
|||
gpu_temp,
|
||||
net_rx_kbs,
|
||||
net_tx_kbs,
|
||||
volume_pct,
|
||||
}
|
||||
}
|
||||
|
||||
/// `wpctl get-volume` prints `Volume: 0.44 [MUTED]`. Scale to a 0–150 percent
|
||||
/// for the bar chip. Missing pipewire / wpctl degrades to 0 rather than
|
||||
/// blocking the rest of the poll.
|
||||
fn read_volume_pct() -> u8 {
|
||||
let out = std::process::Command::new("wpctl")
|
||||
.args(["get-volume", "@DEFAULT_AUDIO_SINK@"])
|
||||
.output()
|
||||
.ok();
|
||||
let Some(o) = out.filter(|o| o.status.success()) else {
|
||||
return 0;
|
||||
};
|
||||
String::from_utf8_lossy(&o.stdout)
|
||||
.trim()
|
||||
.strip_prefix("Volume:")
|
||||
.and_then(|s| s.split_whitespace().next())
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.map(|v| (v * 100.0).round().clamp(0.0, 150.0) as u8)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn spawn_poller(sender: ComponentSender<App>) {
|
||||
relm4::spawn(async move {
|
||||
loop {
|
||||
|
|
|
|||
111
src/bar/wifi.rs
111
src/bar/wifi.rs
|
|
@ -24,6 +24,8 @@ pub struct ScanEntry {
|
|||
pub struct WifiPopoverData {
|
||||
pub profiles: Vec<(String, bool)>, // (name, is_active)
|
||||
pub scan: Vec<ScanEntry>,
|
||||
/// False while nmcli is still listing APs — profiles must still be usable.
|
||||
pub scan_ready: bool,
|
||||
}
|
||||
|
||||
async fn fetch_status() -> Option<CrumbsStatus> {
|
||||
|
|
@ -73,31 +75,57 @@ async fn fetch_profile_list() -> Vec<(String, bool)> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
async fn saved_ssids() -> std::collections::HashSet<String> {
|
||||
let out = tokio::process::Command::new("nmcli")
|
||||
.args(["-t", "-f", "NAME,TYPE", "connection", "show"])
|
||||
.output()
|
||||
.await;
|
||||
let Ok(o) = out else {
|
||||
return std::collections::HashSet::new();
|
||||
};
|
||||
String::from_utf8_lossy(&o.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let (name, ty) = line.rsplit_once(':')?;
|
||||
if ty == "802-11-wireless" || ty == "wifi" {
|
||||
Some(name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Cached AP list (no rescan). Fast enough to paint next to profiles.
|
||||
async fn fetch_scan() -> Vec<ScanEntry> {
|
||||
let Ok(Ok(out)) = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
tokio::process::Command::new("breadcrumbs")
|
||||
.args(["scan-list", "--json"])
|
||||
let out = tokio::time::timeout(
|
||||
Duration::from_secs(4),
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(["-t", "-f", "SSID,SIGNAL,IN-USE", "device", "wifi", "list"])
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
.await;
|
||||
let Ok(Ok(o)) = out else {
|
||||
return vec![];
|
||||
};
|
||||
let arr: Vec<serde_json::Value> =
|
||||
serde_json::from_slice(&out.stdout).unwrap_or_default();
|
||||
arr.into_iter()
|
||||
.filter_map(|v| {
|
||||
let ssid = v["ssid"].as_str()?.to_string();
|
||||
if ssid.is_empty() {
|
||||
let saved = saved_ssids().await;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
String::from_utf8_lossy(&o.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let mut parts = line.rsplitn(3, ':');
|
||||
let _in_use = parts.next()?;
|
||||
let signal = parts.next()?.parse::<u8>().ok().unwrap_or(0);
|
||||
let ssid = parts.next()?.replace("\\:", ":");
|
||||
if ssid.is_empty() || ssid == "--" || !seen.insert(ssid.clone()) {
|
||||
return None;
|
||||
}
|
||||
let signal = v["signal"]
|
||||
.as_str()
|
||||
.and_then(|s| s.parse::<u8>().ok())
|
||||
.unwrap_or(0);
|
||||
let saved = v["saved"].as_bool().unwrap_or(false);
|
||||
Some(ScanEntry { ssid, signal, saved })
|
||||
let saved = saved.contains(&ssid);
|
||||
Some(ScanEntry {
|
||||
ssid,
|
||||
signal,
|
||||
saved,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -114,11 +142,32 @@ pub fn spawn_status_poller(sender: ComponentSender<App>) {
|
|||
});
|
||||
}
|
||||
|
||||
/// Called when the popover opens — loads profiles + scan in parallel.
|
||||
/// Profiles first (so you can switch Home/Away immediately), then the
|
||||
/// cached AP list. A background rescan refreshes the list if it finds more.
|
||||
pub fn spawn_popover_load(sender: ComponentSender<App>) {
|
||||
relm4::spawn(async move {
|
||||
let (profiles, scan) = tokio::join!(fetch_profile_list(), fetch_scan());
|
||||
sender.input(AppInput::WifiPopoverData(WifiPopoverData { profiles, scan }));
|
||||
let profiles = fetch_profile_list().await;
|
||||
sender.input(AppInput::WifiPopoverData(WifiPopoverData {
|
||||
profiles: profiles.clone(),
|
||||
scan: vec![],
|
||||
scan_ready: false,
|
||||
}));
|
||||
let scan = fetch_scan().await;
|
||||
sender.input(AppInput::WifiPopoverData(WifiPopoverData {
|
||||
profiles: profiles.clone(),
|
||||
scan: scan.clone(),
|
||||
scan_ready: true,
|
||||
}));
|
||||
let _ = tokio::process::Command::new("nmcli")
|
||||
.args(["device", "wifi", "rescan"])
|
||||
.output()
|
||||
.await;
|
||||
let scan = fetch_scan().await;
|
||||
sender.input(AppInput::WifiPopoverData(WifiPopoverData {
|
||||
profiles,
|
||||
scan,
|
||||
scan_ready: true,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -132,29 +181,27 @@ pub fn spawn_profile_set(name: String) {
|
|||
});
|
||||
}
|
||||
|
||||
/// Fire-and-forget: connect to a specific saved SSID via `breadcrumbs join`.
|
||||
/// Fire-and-forget: connect to a known SSID via NetworkManager.
|
||||
pub fn spawn_join(ssid: String) {
|
||||
relm4::spawn(async move {
|
||||
let _ = tokio::process::Command::new("breadcrumbs")
|
||||
.args(["join", &ssid])
|
||||
let _ = tokio::process::Command::new("nmcli")
|
||||
.args(["device", "wifi", "connect", &ssid])
|
||||
.output()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Fire-and-forget: save a new network with its password, then join it.
|
||||
/// Save in breadcrumbs (if the CLI still accepts `add`) and connect with nmcli.
|
||||
pub fn spawn_add_and_join(ssid: String, password: String) {
|
||||
relm4::spawn(async move {
|
||||
let added = tokio::process::Command::new("breadcrumbs")
|
||||
let _ = tokio::process::Command::new("breadcrumbs")
|
||||
.args(["add", &ssid, &password])
|
||||
.output()
|
||||
.await;
|
||||
if matches!(added, Ok(o) if o.status.success()) {
|
||||
let _ = tokio::process::Command::new("breadcrumbs")
|
||||
.args(["join", &ssid])
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
let _ = tokio::process::Command::new("nmcli")
|
||||
.args(["device", "wifi", "connect", &ssid, "password", &password])
|
||||
.output()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Instant;
|
||||
|
||||
use futures_lite::StreamExt;
|
||||
use gtk4::glib::ControlFlow;
|
||||
use gtk4::prelude::*;
|
||||
use hyprland::{
|
||||
data::{Workspace, Workspaces},
|
||||
data::{Monitors, Workspaces},
|
||||
event_listener::{Event, EventStream},
|
||||
prelude::*,
|
||||
shared::WorkspaceId,
|
||||
|
|
@ -10,16 +15,62 @@ use relm4::ComponentSender;
|
|||
|
||||
use crate::AppInput;
|
||||
|
||||
/// Fetches the current workspace list + active workspace and pushes both to
|
||||
/// the app — used both for the initial state and to re-sync after the event
|
||||
/// stream reconnects (state may have changed while we were disconnected).
|
||||
/// Stock Hyprland accepts `hyprctl dispatch workspace N`. Lua-config
|
||||
/// Hyprland (BOS) rewrites that as `hl.dispatch(workspace N)`, which is
|
||||
/// a syntax error — the working form is `hl.dsp.focus({workspace=N})`.
|
||||
async fn switch_workspace(id: hyprland::shared::WorkspaceId) {
|
||||
let arg = id.to_string();
|
||||
let stock = tokio::process::Command::new("hyprctl")
|
||||
.args(["dispatch", "workspace", &arg])
|
||||
.output()
|
||||
.await;
|
||||
if let Ok(o) = &stock {
|
||||
let err = String::from_utf8_lossy(&o.stderr);
|
||||
let out = String::from_utf8_lossy(&o.stdout);
|
||||
if o.status.success() && !err.contains("hl.dispatch") && !out.contains("hl.dispatch") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let expr = format!("hl.dispatch(hl.dsp.focus({{workspace={arg}}}))");
|
||||
let lua = tokio::process::Command::new("hyprctl")
|
||||
.args(["eval", &expr])
|
||||
.output()
|
||||
.await;
|
||||
match lua {
|
||||
Ok(o) if o.status.success() => {}
|
||||
Ok(o) => eprintln!(
|
||||
"breadbar: workspace {arg}: {}",
|
||||
String::from_utf8_lossy(&o.stderr)
|
||||
),
|
||||
Err(e) => eprintln!("breadbar: workspace {arg}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stretch to the old→new span, then snap onto the destination — CSS
|
||||
/// transitions cannot widen a pill across two buttons, so the trail's
|
||||
/// Fixed allocation is interpolated on the frame clock instead.
|
||||
const STRETCH_MS: f64 = 220.0;
|
||||
const SNAP_MS: f64 = 380.0;
|
||||
|
||||
/// Full workspace + per-monitor active snapshot. Each bar filters this to
|
||||
/// its own output so a second display does not inherit the laptop's set.
|
||||
async fn sync_state(sender: &ComponentSender<crate::App>) {
|
||||
if let Ok(ws) = Workspaces::get_async().await {
|
||||
sender.input(AppInput::WorkspaceList(ws.to_vec()));
|
||||
}
|
||||
if let Ok(active) = Workspace::get_active_async().await {
|
||||
sender.input(AppInput::ActiveWorkspace(active.id));
|
||||
let workspaces = Workspaces::get_async()
|
||||
.await
|
||||
.map(|w| w.to_vec())
|
||||
.unwrap_or_default();
|
||||
let mut actives = std::collections::HashMap::new();
|
||||
if let Ok(mons) = Monitors::get_async().await {
|
||||
for m in mons {
|
||||
if !m.disabled {
|
||||
actives.insert(m.name, m.active_workspace.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
sender.input(AppInput::WorkspaceSync {
|
||||
workspaces,
|
||||
actives,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn spawn_watcher(sender: ComponentSender<crate::App>) {
|
||||
|
|
@ -41,13 +92,21 @@ pub fn spawn_watcher(sender: ComponentSender<crate::App>) {
|
|||
while let Some(Ok(event)) = stream.next().await {
|
||||
backoff = std::time::Duration::from_millis(500);
|
||||
match event {
|
||||
Event::WorkspaceChanged(data) => {
|
||||
sender.input(AppInput::ActiveWorkspace(data.id));
|
||||
Event::WorkspaceChanged(_)
|
||||
| Event::WorkspaceAdded(_)
|
||||
| Event::WorkspaceDeleted(_) => {
|
||||
sync_state(&sender).await;
|
||||
}
|
||||
Event::WorkspaceAdded(_) | Event::WorkspaceDeleted(_) => {
|
||||
if let Ok(ws) = Workspaces::get_async().await {
|
||||
sender.input(AppInput::WorkspaceList(ws.to_vec()));
|
||||
}
|
||||
Event::MonitorAdded(data) => {
|
||||
sender.input(AppInput::MonitorAdded(data.name));
|
||||
sync_state(&sender).await;
|
||||
}
|
||||
Event::MonitorRemoved(name) => {
|
||||
sender.input(AppInput::MonitorRemoved(name));
|
||||
sync_state(&sender).await;
|
||||
}
|
||||
Event::ActiveWindowChanged(_) => {
|
||||
sender.input(AppInput::DismissPanels);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -65,17 +124,290 @@ pub fn spawn_watcher(sender: ComponentSender<crate::App>) {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn make_button(id: WorkspaceId, name: &str, active: WorkspaceId) -> gtk4::Button {
|
||||
pub fn make_button(
|
||||
id: WorkspaceId,
|
||||
name: &str,
|
||||
active: WorkspaceId,
|
||||
occupied: bool,
|
||||
) -> gtk4::Button {
|
||||
let btn = gtk4::Button::with_label(name);
|
||||
btn.add_css_class("workspace-btn");
|
||||
if occupied {
|
||||
btn.add_css_class("occupied");
|
||||
}
|
||||
if id == active {
|
||||
btn.add_css_class("active");
|
||||
}
|
||||
btn.set_valign(gtk4::Align::Center);
|
||||
btn.set_halign(gtk4::Align::Center);
|
||||
btn.set_vexpand(false);
|
||||
btn.set_hexpand(false);
|
||||
btn.set_size_request(-1, crate::CHIP_HEIGHT);
|
||||
if let Some(child) = btn.child() {
|
||||
child.set_halign(gtk4::Align::Center);
|
||||
child.set_valign(gtk4::Align::Center);
|
||||
}
|
||||
btn.connect_clicked(move |_| {
|
||||
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||
let _ = Dispatch::call(DispatchType::Workspace(WorkspaceIdentifierWithSpecial::Id(
|
||||
id,
|
||||
)));
|
||||
relm4::spawn(async move {
|
||||
switch_workspace(id).await;
|
||||
});
|
||||
});
|
||||
btn
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Geom {
|
||||
x: f64,
|
||||
y: f64,
|
||||
w: f64,
|
||||
h: f64,
|
||||
}
|
||||
|
||||
struct TrailInner {
|
||||
tick: Option<gtk4::TickCallbackId>,
|
||||
geom: Geom,
|
||||
}
|
||||
|
||||
/// Overlay + Fixed pill sitting *behind* the workspace buttons. The
|
||||
/// Overlay's measured size comes from the button row; the pill is the
|
||||
/// main child so it paints underneath and never steals clicks.
|
||||
pub struct WorkspaceTrail {
|
||||
pub overlay: gtk4::Overlay,
|
||||
pub buttons: gtk4::Box,
|
||||
host: gtk4::Fixed,
|
||||
pill: gtk4::Box,
|
||||
inner: Rc<RefCell<TrailInner>>,
|
||||
}
|
||||
|
||||
impl WorkspaceTrail {
|
||||
pub fn new() -> Self {
|
||||
let overlay = gtk4::Overlay::new();
|
||||
overlay.add_css_class("workspace-overlay");
|
||||
overlay.set_valign(gtk4::Align::Center);
|
||||
overlay.set_vexpand(false);
|
||||
|
||||
let host = gtk4::Fixed::new();
|
||||
host.set_can_target(false);
|
||||
|
||||
let pill = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
||||
pill.add_css_class("workspace-trail");
|
||||
pill.set_can_target(false);
|
||||
pill.set_visible(false);
|
||||
host.put(&pill, 0.0, 0.0);
|
||||
|
||||
let buttons = gtk4::Box::new(gtk4::Orientation::Horizontal, 1);
|
||||
buttons.set_halign(gtk4::Align::Fill);
|
||||
buttons.set_valign(gtk4::Align::Center);
|
||||
buttons.set_vexpand(false);
|
||||
|
||||
overlay.set_child(Some(&host));
|
||||
overlay.add_overlay(&buttons);
|
||||
overlay.set_measure_overlay(&buttons, true);
|
||||
|
||||
let inner = Rc::new(RefCell::new(TrailInner {
|
||||
tick: None,
|
||||
geom: Geom {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 0.0,
|
||||
h: 0.0,
|
||||
},
|
||||
}));
|
||||
|
||||
Self {
|
||||
overlay,
|
||||
buttons,
|
||||
host,
|
||||
pill,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
if let Some(id) = self.inner.borrow_mut().tick.take() {
|
||||
id.remove();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.cancel();
|
||||
self.pill.set_visible(false);
|
||||
self.inner.borrow_mut().geom.w = 0.0;
|
||||
}
|
||||
|
||||
pub fn place(&self, btn: >k4::Button) {
|
||||
self.cancel();
|
||||
if let Some(g) = button_geom(btn, &self.host) {
|
||||
apply_geom(&self.host, &self.pill, &self.inner, &inset_pill(g));
|
||||
return;
|
||||
}
|
||||
let pill = self.pill.clone();
|
||||
let host = self.host.clone();
|
||||
let inner = self.inner.clone();
|
||||
let btn = btn.clone();
|
||||
let id = self.overlay.add_tick_callback(move |_, _| {
|
||||
let Some(g) = button_geom(&btn, &host) else {
|
||||
return ControlFlow::Continue;
|
||||
};
|
||||
apply_geom(&host, &pill, &inner, &inset_pill(g));
|
||||
inner.borrow_mut().tick = None;
|
||||
ControlFlow::Break
|
||||
});
|
||||
self.inner.borrow_mut().tick = Some(id);
|
||||
}
|
||||
|
||||
pub fn stretch(&self, from: Option<>k4::Button>, to: >k4::Button) {
|
||||
self.cancel();
|
||||
let Some(from_g) = self.from_geom(from) else {
|
||||
self.place(to);
|
||||
return;
|
||||
};
|
||||
let dest = to.clone();
|
||||
let pill = self.pill.clone();
|
||||
let host = self.host.clone();
|
||||
let inner = self.inner.clone();
|
||||
let started = Instant::now();
|
||||
let id = self.overlay.add_tick_callback(move |_, _| {
|
||||
let to_g = resolved_dest(&dest, &host, &from_g);
|
||||
let mid = {
|
||||
let span_x = from_g.x.min(to_g.x);
|
||||
let span_w = (from_g.x + from_g.w).max(to_g.x + to_g.w) - span_x;
|
||||
Geom {
|
||||
x: span_x,
|
||||
y: to_g.y,
|
||||
w: span_w,
|
||||
h: to_g.h,
|
||||
}
|
||||
};
|
||||
let elapsed = started.elapsed().as_secs_f64() * 1000.0;
|
||||
let (g, done) = if elapsed < STRETCH_MS {
|
||||
let t = ease(elapsed / STRETCH_MS);
|
||||
(lerp_geom(&from_g, &mid, t), false)
|
||||
} else if elapsed < STRETCH_MS + SNAP_MS {
|
||||
let t = ease_overshoot((elapsed - STRETCH_MS) / SNAP_MS);
|
||||
(lerp_geom(&mid, &to_g, t), false)
|
||||
} else {
|
||||
(to_g, true)
|
||||
};
|
||||
apply_geom(&host, &pill, &inner, &g);
|
||||
if done {
|
||||
inner.borrow_mut().tick = None;
|
||||
ControlFlow::Break
|
||||
} else {
|
||||
ControlFlow::Continue
|
||||
}
|
||||
});
|
||||
self.inner.borrow_mut().tick = Some(id);
|
||||
}
|
||||
|
||||
fn from_geom(&self, from: Option<>k4::Button>) -> Option<Geom> {
|
||||
let st = self.inner.borrow();
|
||||
// A leftover mid-stretch can be as wide as the whole row — never
|
||||
// treat that as the start of the next animation.
|
||||
if self.pill.is_visible() && st.geom.w > 0.5 && st.geom.w <= MAX_CHIP_W {
|
||||
return Some(st.geom);
|
||||
}
|
||||
drop(st);
|
||||
from.and_then(|b| button_geom(b, &self.host).map(inset_pill))
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the trail slimmer than the hit target so the fill doesn't look
|
||||
/// like a second, fatter button.
|
||||
const PILL_INSET_X: f64 = 5.0;
|
||||
const PILL_INSET_Y: f64 = 3.0;
|
||||
/// One workspace chip is a digit + padding. Wider than this is the overlay
|
||||
/// or the whole button row leaking through `compute_bounds`.
|
||||
const MAX_CHIP_W: f64 = 72.0;
|
||||
|
||||
fn inset_pill(g: Geom) -> Geom {
|
||||
let w = (g.w - PILL_INSET_X * 2.0).max(10.0);
|
||||
let h = (g.h - PILL_INSET_Y * 2.0).max(18.0);
|
||||
Geom {
|
||||
x: g.x + (g.w - w) * 0.5,
|
||||
y: g.y + (g.h - h) * 0.5,
|
||||
w,
|
||||
h,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolved_dest(btn: >k4::Button, host: >k4::Fixed, from: &Geom) -> Geom {
|
||||
match button_geom(btn, host) {
|
||||
Some(g) if !still_placeholder(btn, &g) => inset_pill(g),
|
||||
Some(g) => {
|
||||
let centered = inset_pill(g);
|
||||
Geom {
|
||||
x: centered.x + (centered.w - from.w) * 0.5,
|
||||
y: centered.y + (centered.h - from.h) * 0.5,
|
||||
w: from.w,
|
||||
h: from.h,
|
||||
}
|
||||
}
|
||||
None => *from,
|
||||
}
|
||||
}
|
||||
|
||||
/// Position in the Fixed host's space — that's what `host.move_` uses.
|
||||
/// Measuring against the Overlay instead left the pill a few px left of
|
||||
/// the digit whenever the host and overlay origins disagreed.
|
||||
fn button_geom(btn: >k4::Button, host: >k4::Fixed) -> Option<Geom> {
|
||||
let r = btn.compute_bounds(host)?;
|
||||
let w = f64::from(r.width());
|
||||
let h = f64::from(r.height());
|
||||
if w < 8.0 || h < 8.0 || w > MAX_CHIP_W {
|
||||
return None;
|
||||
}
|
||||
Some(Geom {
|
||||
x: f64::from(r.x()),
|
||||
y: f64::from(r.y()),
|
||||
w,
|
||||
h,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_geom(host: >k4::Fixed, pill: >k4::Box, inner: &Rc<RefCell<TrailInner>>, g: &Geom) {
|
||||
inner.borrow_mut().geom = Geom {
|
||||
x: g.x,
|
||||
y: g.y,
|
||||
w: g.w,
|
||||
h: g.h,
|
||||
};
|
||||
let w = g.w.max(1.0).round() as i32;
|
||||
let h = g.h.max(1.0).round() as i32;
|
||||
// Clearing first lets GTK shrink; size-request is a minimum.
|
||||
pill.set_size_request(-1, -1);
|
||||
pill.set_size_request(w, h);
|
||||
host.move_(pill, g.x, g.y);
|
||||
pill.set_visible(true);
|
||||
}
|
||||
|
||||
fn still_placeholder(btn: >k4::Button, g: &Geom) -> bool {
|
||||
let (min_w, nat_w, _, _) = btn.measure(gtk4::Orientation::Horizontal, -1);
|
||||
g.w <= f64::from(min_w) + 1.0 || g.w + 0.5 < f64::from(nat_w)
|
||||
}
|
||||
|
||||
fn lerp(a: f64, b: f64, t: f64) -> f64 {
|
||||
a + (b - a) * t
|
||||
}
|
||||
|
||||
fn lerp_geom(a: &Geom, b: &Geom, t: f64) -> Geom {
|
||||
Geom {
|
||||
x: lerp(a.x, b.x, t),
|
||||
y: lerp(a.y, b.y, t),
|
||||
w: lerp(a.w, b.w, t),
|
||||
h: lerp(a.h, b.h, t),
|
||||
}
|
||||
}
|
||||
|
||||
fn ease(t: f64) -> f64 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
|
||||
/// Approximates the demo's cubic-bezier(.22, 1.4, .36, 1) snap.
|
||||
fn ease_overshoot(t: f64) -> f64 {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let c = 1.4;
|
||||
let t1 = t - 1.0;
|
||||
1.0 + t1 * t1 * ((c + 1.0) * t1 + c)
|
||||
}
|
||||
|
|
|
|||
1449
src/main.rs
1449
src/main.rs
File diff suppressed because it is too large
Load diff
|
|
@ -175,13 +175,15 @@ pub fn build_window(store: Store) -> Ui {
|
|||
let window = gtk4::Window::new();
|
||||
window.add_css_class("breadbar-history");
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadbar-notif"));
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, 48);
|
||||
window.set_margin(Edge::Right, 20);
|
||||
window.set_margin(Edge::Top, crate::BAR_MARGIN_TOP + crate::BAR_HEIGHT + 8);
|
||||
window.set_margin(Edge::Right, crate::BAR_MARGIN_SIDES);
|
||||
window.set_default_width(360);
|
||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
||||
crate::theme::bind_auto(&window);
|
||||
|
||||
let outer = gtk4::Box::new(gtk4::Orientation::Vertical, 8);
|
||||
outer.set_margin_top(10);
|
||||
|
|
@ -284,8 +286,8 @@ fn make_row(entry: &Entry) -> gtk4::Box {
|
|||
}
|
||||
|
||||
let top = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
||||
let show_app = !entry.app_name.is_empty()
|
||||
&& !entry.app_name.eq_ignore_ascii_case(&entry.summary);
|
||||
let show_app =
|
||||
!entry.app_name.is_empty() && !entry.app_name.eq_ignore_ascii_case(&entry.summary);
|
||||
if show_app {
|
||||
let app = gtk4::Label::new(Some(&entry.app_name));
|
||||
app.add_css_class("notification-app");
|
||||
|
|
|
|||
|
|
@ -170,15 +170,24 @@ fn create_window() -> gtk4::Window {
|
|||
let window = gtk4::Window::new();
|
||||
window.add_css_class("breadbar-notification");
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadbar-notif"));
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, 20);
|
||||
window.set_margin(Edge::Right, 20);
|
||||
window.set_margin(Edge::Top, crate::BAR_MARGIN_TOP + crate::BAR_HEIGHT + 8);
|
||||
window.set_margin(Edge::Right, crate::BAR_MARGIN_SIDES);
|
||||
window.set_default_width(320);
|
||||
// OnDemand so an inline-reply GtkEntry can take keys without the popup
|
||||
// stealing every keystroke the rest of the time.
|
||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
||||
// Toasts are purely informational for now: never grab keyboard focus...
|
||||
window.set_keyboard_mode(KeyboardMode::None);
|
||||
// ...and click through entirely — an empty input region means every
|
||||
// pointer event passes straight to whatever's underneath instead of
|
||||
// hitting the toast.
|
||||
window.connect_map(|win| {
|
||||
if let Some(surface) = win.surface() {
|
||||
surface.set_input_region(Some(>k4::cairo::Region::create()));
|
||||
}
|
||||
});
|
||||
crate::theme::bind_auto(&window);
|
||||
window
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -163,9 +163,7 @@ async fn run_osd(window: gtk4::Window, mut rx: mpsc::Receiver<OsdEvent>) {
|
|||
container.set_margin_end(14);
|
||||
window.set_child(Some(&container));
|
||||
|
||||
let icon = gtk4::Image::from_paintable(Some(&crate::svg_texture(
|
||||
crate::bar::stats::ICON_VOLUME,
|
||||
)));
|
||||
let icon = crate::svg_image(crate::bar::stats::ICON_VOLUME);
|
||||
icon.add_css_class("osd-icon");
|
||||
container.append(&icon);
|
||||
|
||||
|
|
@ -184,6 +182,7 @@ async fn run_osd(window: gtk4::Window, mut rx: mpsc::Receiver<OsdEvent>) {
|
|||
};
|
||||
|
||||
icon.set_paintable(Some(&crate::svg_texture(icon_svg)));
|
||||
crate::prepare_icon(&icon, crate::ICON_PX);
|
||||
if muted {
|
||||
icon.add_css_class("osd-icon-muted");
|
||||
} else {
|
||||
|
|
@ -209,9 +208,11 @@ fn create_window() -> gtk4::Window {
|
|||
let window = gtk4::Window::new();
|
||||
window.add_css_class("breadbar-osd");
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadbar-osd"));
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Bottom, true);
|
||||
window.set_margin(Edge::Bottom, 80);
|
||||
window.set_default_width(180);
|
||||
crate::theme::bind_auto(&window);
|
||||
window
|
||||
}
|
||||
|
|
|
|||
154
src/panel.rs
Normal file
154
src/panel.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
//! Standalone layer-shell panels for wifi / control / media.
|
||||
//!
|
||||
//! GTK `Popover` is an xdg_popup child of the island, so it paints over the
|
||||
//! bar and Hyprland can only fade it. These are their own surfaces, parked
|
||||
//! *below* the exclusive zone, and Hyprland slides `breadbar-panel` in from
|
||||
//! the right.
|
||||
|
||||
use gtk4::gdk::Key;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||
|
||||
use crate::{bind_layer_monitor, theme, BAR_HEIGHT, BAR_MARGIN_SIDES, BAR_MARGIN_TOP};
|
||||
|
||||
const BELOW_BAR: i32 = BAR_MARGIN_TOP + BAR_HEIGHT + 8;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PanelSet {
|
||||
pub connectivity: gtk4::Window,
|
||||
pub control: gtk4::Window,
|
||||
pub media: gtk4::Window,
|
||||
dismiss: gtk4::Window,
|
||||
}
|
||||
|
||||
impl PanelSet {
|
||||
pub fn new(
|
||||
monitor: &str,
|
||||
connectivity_child: &impl IsA<gtk4::Widget>,
|
||||
control_child: &impl IsA<gtk4::Widget>,
|
||||
media_child: &impl IsA<gtk4::Widget>,
|
||||
) -> Self {
|
||||
let connectivity = make_panel("wifi-popover", connectivity_child, monitor);
|
||||
let control = make_panel("control-panel", control_child, monitor);
|
||||
let media = make_panel("media-popover", media_child, monitor);
|
||||
let dismiss = make_dismiss(monitor);
|
||||
|
||||
let set = Self {
|
||||
connectivity,
|
||||
control,
|
||||
media,
|
||||
dismiss,
|
||||
};
|
||||
set.wire_dismiss();
|
||||
set.wire_escape();
|
||||
set
|
||||
}
|
||||
|
||||
pub fn toggle(&self, which: >k4::Window) {
|
||||
if which.is_visible() {
|
||||
self.hide_all();
|
||||
} else {
|
||||
self.show(which);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&self, which: >k4::Window) {
|
||||
self.hide_panels();
|
||||
// Dismiss first so the panel maps above it (same Overlay layer).
|
||||
self.dismiss.set_visible(true);
|
||||
self.dismiss.present();
|
||||
which.set_visible(true);
|
||||
which.present();
|
||||
}
|
||||
|
||||
pub fn hide_all(&self) {
|
||||
self.hide_panels();
|
||||
self.dismiss.set_visible(false);
|
||||
}
|
||||
|
||||
fn hide_panels(&self) {
|
||||
self.connectivity.set_visible(false);
|
||||
self.control.set_visible(false);
|
||||
self.media.set_visible(false);
|
||||
}
|
||||
|
||||
fn wire_dismiss(&self) {
|
||||
let set = self.clone();
|
||||
let click = gtk4::GestureClick::new();
|
||||
click.set_button(0);
|
||||
click.connect_pressed(move |_, _, _, _| {
|
||||
set.hide_all();
|
||||
});
|
||||
if let Some(child) = self.dismiss.child() {
|
||||
child.add_controller(click);
|
||||
} else {
|
||||
self.dismiss.add_controller(click);
|
||||
}
|
||||
}
|
||||
|
||||
fn wire_escape(&self) {
|
||||
for win in [&self.connectivity, &self.control, &self.media] {
|
||||
let set = self.clone();
|
||||
let keys = gtk4::EventControllerKey::new();
|
||||
keys.connect_key_pressed(move |_, key, _, _| {
|
||||
if key == Key::Escape {
|
||||
set.hide_all();
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
gtk4::glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
win.add_controller(keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_panel(class: &str, child: &impl IsA<gtk4::Widget>, monitor: &str) -> gtk4::Window {
|
||||
let window = gtk4::Window::new();
|
||||
window.add_css_class("breadbar-panel");
|
||||
window.add_css_class(class);
|
||||
window.set_decorated(false);
|
||||
window.set_resizable(false);
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadbar-panel"));
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, BELOW_BAR);
|
||||
window.set_margin(Edge::Right, BAR_MARGIN_SIDES);
|
||||
window.set_exclusive_zone(-1);
|
||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
||||
window.set_child(Some(child));
|
||||
bind_layer_monitor(&window, monitor);
|
||||
theme::bind_output(&window, monitor);
|
||||
window.set_visible(false);
|
||||
window
|
||||
}
|
||||
|
||||
fn make_dismiss(monitor: &str) -> gtk4::Window {
|
||||
let window = gtk4::Window::new();
|
||||
window.add_css_class("breadbar-dismiss");
|
||||
window.init_layer_shell();
|
||||
window.set_namespace(Some("breadbar-dismiss"));
|
||||
// Overlay with the panels, but mapped first so they sit above it.
|
||||
// Top margin keeps the island's chips clickable.
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Bottom, true);
|
||||
window.set_anchor(Edge::Left, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, BAR_MARGIN_TOP + BAR_HEIGHT);
|
||||
window.set_exclusive_zone(-1);
|
||||
window.set_keyboard_mode(KeyboardMode::None);
|
||||
// An empty window never maps a hit region. A filling child + a hair of
|
||||
// alpha is what actually receives the click-away.
|
||||
let hit = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
|
||||
hit.add_css_class("breadbar-dismiss-hit");
|
||||
hit.set_hexpand(true);
|
||||
hit.set_vexpand(true);
|
||||
window.set_child(Some(&hit));
|
||||
bind_layer_monitor(&window, monitor);
|
||||
theme::bind_output(&window, monitor);
|
||||
window.set_visible(false);
|
||||
window
|
||||
}
|
||||
|
|
@ -20,11 +20,9 @@ use gtk4::prelude::*;
|
|||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Settle time for views whose content depends on `bar::stats::spawn_poller`'s
|
||||
/// 2-second background loop (control-panel's CPU/RAM/PWR/GPU/network labels,
|
||||
/// gated on popover visibility) or a similar live-data popover load
|
||||
/// (connectivity's wifi/bluetooth scan) — capturing any sooner leaves
|
||||
/// placeholder dashes/"Scanning…" instead of real content.
|
||||
/// Settle time for views whose content depends on a live-data popover load
|
||||
/// (connectivity's wifi/bluetooth scan, control-panel sliders) — capturing
|
||||
/// any sooner leaves placeholder dashes/"Scanning…" instead of real content.
|
||||
const LIVE_DATA_SETTLE_DELAY: Duration = Duration::from_millis(2_200);
|
||||
|
||||
/// Delay between the bar's own `map` and calling `popover.popup()`. Calling
|
||||
|
|
@ -107,11 +105,11 @@ impl Cli {
|
|||
/// outlive `init()` (never stored on `App`), so they have to be cloned out
|
||||
/// before dispatch time same as `control_popover` always was.
|
||||
pub struct Handles {
|
||||
pub control_popover: gtk4::Popover,
|
||||
pub connectivity_popover: gtk4::Popover,
|
||||
pub control_panel: gtk4::Window,
|
||||
pub connectivity_panel: gtk4::Window,
|
||||
pub wifi_tab_btn: gtk4::ToggleButton,
|
||||
pub bt_tab_btn: gtk4::ToggleButton,
|
||||
pub media_popover: gtk4::Popover,
|
||||
pub media_panel: gtk4::Window,
|
||||
pub media_widget: gtk4::Box,
|
||||
pub media_track_lbl: gtk4::Label,
|
||||
/// Already built and primed with sample content by `main.rs` (via
|
||||
|
|
@ -122,10 +120,10 @@ pub struct Handles {
|
|||
pub osd_window: Option<gtk4::Window>,
|
||||
}
|
||||
|
||||
/// The bar's fixed height — matches `root.set_exclusive_zone(32)` /
|
||||
/// `set_default_height: 32` in `main.rs`. Unlike the other views' full
|
||||
/// canvas, this never varies with `--width`/`--height`.
|
||||
const BAR_HEIGHT: i32 = 32;
|
||||
/// Capture height for the `bar` view: layer-shell top margin + widget
|
||||
/// height (the exclusive zone). Unlike the other views' full canvas,
|
||||
/// this never varies with `--width`/`--height`.
|
||||
const BAR_HEIGHT: i32 = crate::BAR_HEIGHT + crate::BAR_MARGIN_TOP;
|
||||
|
||||
pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, handles: Handles) {
|
||||
let output = req.output;
|
||||
|
|
@ -141,15 +139,15 @@ pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, handles:
|
|||
});
|
||||
}
|
||||
"control-panel" => {
|
||||
open_popover_on_root_map(root, handles.control_popover, LIVE_DATA_SETTLE_DELAY, output, width, height);
|
||||
open_panel_on_root_map(root, handles.control_panel, LIVE_DATA_SETTLE_DELAY, output, width, height);
|
||||
}
|
||||
"connectivity-wifi" => {
|
||||
handles.wifi_tab_btn.set_active(true);
|
||||
open_popover_on_root_map(root, handles.connectivity_popover, LIVE_DATA_SETTLE_DELAY, output, width, height);
|
||||
open_panel_on_root_map(root, handles.connectivity_panel, LIVE_DATA_SETTLE_DELAY, output, width, height);
|
||||
}
|
||||
"connectivity-bluetooth" => {
|
||||
handles.bt_tab_btn.set_active(true);
|
||||
open_popover_on_root_map(root, handles.connectivity_popover, LIVE_DATA_SETTLE_DELAY, output, width, height);
|
||||
open_panel_on_root_map(root, handles.connectivity_panel, LIVE_DATA_SETTLE_DELAY, output, width, height);
|
||||
}
|
||||
"media-popover" => {
|
||||
// Real media state only shows the widget/text when something's
|
||||
|
|
@ -157,8 +155,9 @@ pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, handles:
|
|||
// run has nothing playing, so fake enough of it directly on the
|
||||
// widgets to get a representative capture.
|
||||
handles.media_widget.set_visible(true);
|
||||
handles.media_widget.add_css_class("playing");
|
||||
handles.media_track_lbl.set_text("Sample Track — Sample Artist");
|
||||
open_popover_on_root_map(root, handles.media_popover, SETTLE_DELAY, output, width, height);
|
||||
open_panel_on_root_map(root, handles.media_panel, SETTLE_DELAY, output, width, height);
|
||||
}
|
||||
"notification" | "notification-critical" => {
|
||||
let Some(window) = handles.notification_window else {
|
||||
|
|
@ -196,27 +195,25 @@ pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, handles:
|
|||
}
|
||||
}
|
||||
|
||||
/// Shared shape for every popover view: force it open shortly after the bar
|
||||
/// maps (autohide disabled — a programmatic `popup()` has no real input
|
||||
/// event serial to grab the Wayland seat with), then capture the whole
|
||||
/// canvas after `settle` once the popover itself maps.
|
||||
fn open_popover_on_root_map(
|
||||
/// Shared shape for panel views: present the standalone layer window after
|
||||
/// the bar maps, then capture the canvas once the panel itself maps.
|
||||
fn open_panel_on_root_map(
|
||||
root: >k4::ApplicationWindow,
|
||||
popover: gtk4::Popover,
|
||||
panel: gtk4::Window,
|
||||
settle: Duration,
|
||||
output: PathBuf,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) {
|
||||
let popover_to_open = popover.clone();
|
||||
let panel_to_open = panel.clone();
|
||||
root.connect_map(move |_| {
|
||||
popover_to_open.set_autohide(false);
|
||||
let popover_to_open = popover_to_open.clone();
|
||||
let panel_to_open = panel_to_open.clone();
|
||||
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
|
||||
popover_to_open.popup();
|
||||
panel_to_open.set_visible(true);
|
||||
panel_to_open.present();
|
||||
});
|
||||
});
|
||||
popover.connect_map(move |_| {
|
||||
panel.connect_map(move |_| {
|
||||
let output = output.clone();
|
||||
gtk4::glib::timeout_add_local_once(settle, move || {
|
||||
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||
|
|
|
|||
293
src/theme.rs
293
src/theme.rs
|
|
@ -1,4 +1,5 @@
|
|||
use bread_theme::{gtk as bgtk, hex_to_rgba, ink_on, load_palette};
|
||||
use bread_theme::{gtk as bgtk, ink_on, load_palette, load_palette_for, Palette};
|
||||
use gtk4::prelude::IsA;
|
||||
use gtk4::CssProvider;
|
||||
use std::cell::RefCell;
|
||||
|
||||
|
|
@ -7,7 +8,6 @@ thread_local! {
|
|||
}
|
||||
|
||||
fn load_css() -> String {
|
||||
let p = load_palette();
|
||||
// breadbar-specific rules only — fonts, base colours, and generic widgets
|
||||
// come from the shared ecosystem stylesheet (applied first in `apply()`).
|
||||
// Colour is set on each surface (bar, active workspace pill, notification
|
||||
|
|
@ -15,37 +15,79 @@ fn load_css() -> String {
|
|||
// pywal hands a given slot. `on_*` are luminance-picked ink (black/white) for
|
||||
// that background — the pywal hues themselves are untouched.
|
||||
//
|
||||
// Shared tokens: one radius and one padding rhythm reused across every
|
||||
// popover/card/OSD surface so they read as one design system rather than
|
||||
// four different ones. `radius_pill` is only for the tiny transient OSD.
|
||||
let radius = "10px";
|
||||
let radius_sm = "0px";
|
||||
let radius_pill = "20px";
|
||||
let pad = "10px";
|
||||
// Glass workbench: 16px island on the bar, 12px cards/popovers, pill OSD.
|
||||
// Hyprland `layerrule = blur, breadbar` frosts the translucent fills —
|
||||
// the CSS just leaves alpha. Colours are bread-theme tokens so pywal
|
||||
// accents (`@accent`) flow through on SIGHUP / `bread-theme reload`.
|
||||
let radius = "12px";
|
||||
let radius_bar = "16px";
|
||||
let radius_sm = "9px";
|
||||
let radius_pill = "999px";
|
||||
let pad = "12px";
|
||||
|
||||
format!(
|
||||
"window.breadbar {{ background-color: {bg_rgba}; color: {on_bg}; border-radius: 0; }}\
|
||||
.workspace-btn {{ background: transparent; opacity: 0.45; color: {on_bg};\
|
||||
border-radius: {radius_sm}; border: none; outline: none; box-shadow: none;\
|
||||
min-width: 20px; margin: 5px 2px; padding: 2px 9px; }}\
|
||||
.workspace-btn:hover {{ opacity: 0.8; }}\
|
||||
.workspace-btn.active {{ background: alpha({accent}, 0.18); color: {accent}; opacity: 1; }}\
|
||||
.stats-box {{ margin-right: 8px; }}\
|
||||
.stat-pair {{ margin-right: 14px; }}\
|
||||
.stat-icon {{ margin-right: 2px; }}\
|
||||
.bt-icon {{ margin-right: 14px; }}\
|
||||
separator.bar-sep {{ min-height: 14px; margin: 0 8px 0 0; background: alpha({on_bg}, 0.14); }}\
|
||||
window.breadbar-notification {{ background-color: alpha({bg_plain}, 0.95); color: {on_bg}; }}\
|
||||
window.breadbar-history {{ background-color: alpha({bg_plain}, 0.95); color: {on_bg};\
|
||||
border-radius: {radius}; }}\
|
||||
.notification-card {{ background: {surface}; color: {on_surface}; border-radius: {radius};\
|
||||
padding: {pad}; margin-bottom: 8px; border-left: 3px solid transparent; }}\
|
||||
.notification-card.urgency-critical {{ border-left-color: {critical}; }}\
|
||||
.notification-card.urgency-normal {{ border-left-color: {accent}; }}\
|
||||
"@keyframes notif-in {{ from {{ opacity: 0; margin-right: -16px; }} }}\
|
||||
@keyframes osd-in {{ from {{ opacity: 0; margin-bottom: -8px; }} }}\
|
||||
@keyframes media-eq {{ to {{ min-height: 14px; }} }}\
|
||||
@keyframes pop-in {{ from {{ opacity: 0; margin-top: -10px; }} to {{ opacity: 1; margin-top: 0; }} }}\
|
||||
@keyframes pop-out {{ from {{ opacity: 1; margin-top: 0; }} to {{ opacity: 0; margin-top: -6px; }} }}\
|
||||
@keyframes row-in {{ from {{ opacity: 0; margin-top: 8px; }} to {{ opacity: 1; margin-top: 0; }} }}\
|
||||
@keyframes digit-flip {{ from {{ opacity: 0; margin-top: 7px; }} to {{ opacity: 1; margin-top: 0; }} }}\
|
||||
@keyframes caret-draw {{ from {{ margin-right: 200px; opacity: 0.2; }} to {{ margin-right: 4px; opacity: 1; }} }}\
|
||||
window.breadbar {{ background-color: alpha(@bg, 0.72); color: @on-bg;\
|
||||
border-radius: {radius_bar}; border: 1px solid alpha(@on-bg, 0.08); }}\
|
||||
window.breadbar > centerbox {{ padding: 0 8px 0 6px; }}\
|
||||
window.breadbar button {{ min-height: 0; min-width: 0; }}\
|
||||
.workspace-trail {{ background-image: linear-gradient(90deg, @accent, @teal);\
|
||||
background-color: @accent; border-radius: 12px; }}\
|
||||
.workspace-btn {{ background: transparent; opacity: 0.36; color: @on-bg;\
|
||||
border-radius: 12px; border: none; outline: none; box-shadow: none;\
|
||||
min-width: 28px; min-height: 28px; margin: 0; padding: 0 7px;\
|
||||
font-size: 22px; font-weight: bold;\
|
||||
transition: opacity 0.22s cubic-bezier(0.22, 1.2, 0.36, 1),\
|
||||
background-color 0.22s cubic-bezier(0.22, 1.2, 0.36, 1); }}\
|
||||
.workspace-btn:hover {{ opacity: 0.85; background: alpha(@on-bg, 0.08); }}\
|
||||
.workspace-btn.occupied {{ opacity: 0.78; }}\
|
||||
.workspace-btn.active {{ background: transparent; color: @on-accent; opacity: 1; }}\
|
||||
.workspace-btn.active:hover {{ background: transparent; }}\
|
||||
.workspace-btn.ws-in {{ animation: row-in 0.32s cubic-bezier(0.22, 1.2, 0.36, 1) both; }}\
|
||||
.clock-box {{ padding: 0 4px; }}\
|
||||
.clock-label {{ font-size: 24px; font-weight: bold; letter-spacing: 0.04em;\
|
||||
min-height: 0; padding: 0; margin-top: 3px; }}\
|
||||
.clock-digit {{ font-size: 24px; font-weight: bold; letter-spacing: 0.04em;\
|
||||
min-width: 15px; min-height: 0; padding: 0; margin: 0; }}\
|
||||
.clock-colon {{ min-width: 10px; opacity: 0.7; }}\
|
||||
.clock-digit.flip {{ animation: digit-flip 0.45s cubic-bezier(0.22, 1.35, 0.36, 1) both; }}\
|
||||
.date-label {{ font-size: 14px; opacity: 0.52; letter-spacing: 0.04em; }}\
|
||||
.stat-label {{ font-size: 14px; letter-spacing: 0.02em; opacity: 0.92; }}\
|
||||
.stat-label.tick {{ animation: digit-flip 0.35s cubic-bezier(0.22, 1.35, 0.36, 1) both; }}\
|
||||
.stats-box {{ margin-right: 0; }}\
|
||||
.stat-pair {{ margin: 0; border-radius: 10px; padding: 5px 9px; min-height: 0;\
|
||||
transition: background-color 0.22s cubic-bezier(0.22, 1.2, 0.36, 1),\
|
||||
opacity 0.18s ease; }}\
|
||||
.stat-pair:hover {{ background: alpha(@on-bg, 0.12); }}\
|
||||
.stat-pair:active {{ background: alpha(@on-bg, 0.18); }}\
|
||||
.stat-pair.icon-only {{ padding: 4px; border-radius: 999px;\
|
||||
min-width: 32px; min-height: 32px; }}\
|
||||
.stat-icon {{ margin-right: 6px; }}\
|
||||
.stat-pair.icon-only .stat-icon {{ margin: 0; }}\
|
||||
.bt-icon {{ margin-right: 8px; }}
|
||||
separator.bar-sep {{ min-height: 12px; min-width: 1px; margin: 0 10px 0 2px;\
|
||||
background: alpha(@on-bg, 0.10); }}\
|
||||
window.breadbar-notification {{ background-color: transparent; color: @on-bg; }}\
|
||||
window.breadbar-history {{ background-color: alpha(@bg, 0.70); color: @on-bg;\
|
||||
border-radius: {radius}; border: 1px solid alpha(@on-bg, 0.10);\
|
||||
animation: pop-in 0.45s cubic-bezier(0.22, 1.35, 0.36, 1) both; }}\
|
||||
.notification-card {{ background: alpha(@bg, 0.70); color: @on-bg; border-radius: {radius};\
|
||||
padding: {pad}; margin-bottom: 8px; border: 1px solid alpha(@on-bg, 0.10);\
|
||||
border-left: 3px solid transparent;\
|
||||
animation: notif-in 0.45s cubic-bezier(0.22, 1.2, 0.36, 1) both; }}\
|
||||
.notification-card.urgency-critical {{ border-left-color: @red; }}\
|
||||
.notification-card.urgency-normal {{ border-left-color: @accent; }}\
|
||||
.notification-summary {{ font-weight: bold; }}\
|
||||
.notification-app {{ opacity: 0.6; }}\
|
||||
.notification-app {{ opacity: 0.55; font-size: 11px; letter-spacing: 0.04em; }}\
|
||||
.notification-actions {{ margin-top: 6px; }}\
|
||||
.notification-action {{ padding: 2px 8px; font-size: 11px; }}\
|
||||
.notification-action {{ padding: 2px 8px; font-size: 11px; border-radius: {radius_sm}; }}\
|
||||
.notification-reply {{ margin-top: 6px; }}\
|
||||
.notification-reply-entry {{ min-width: 0; }}\
|
||||
.history-title {{ font-weight: bold; font-size: 13px; }}\
|
||||
|
|
@ -54,59 +96,130 @@ fn load_css() -> String {
|
|||
.history-time {{ opacity: 0.5; font-size: 11px; }}\
|
||||
.history-body {{ opacity: 0.75; }}\
|
||||
.history-card {{ margin-bottom: 6px; }}\
|
||||
window.breadbar-osd {{ background-color: alpha({bg_plain}, 0.95); color: {on_bg}; border-radius: {radius_pill}; }}\
|
||||
window.breadbar-osd {{ background-color: alpha(@bg, 0.70); color: @on-bg;\
|
||||
border-radius: {radius_pill}; border: 1px solid alpha(@on-bg, 0.10);\
|
||||
animation: osd-in 0.4s cubic-bezier(0.22, 1.2, 0.36, 1) both; }}\
|
||||
.osd-icon {{ opacity: 0.85; margin-right: 8px; }}\
|
||||
.osd-icon-muted {{ opacity: 0.35; }}\
|
||||
progressbar.osd-bar {{ min-height: 6px; }}\
|
||||
progressbar.osd-bar trough {{ background-image: none; background-color: {trough}; border-radius: 3px; min-height: 6px; }}\
|
||||
progressbar.osd-bar trough progress {{ background-image: none; background-color: {accent}; border-radius: 3px; min-height: 6px; }}\
|
||||
.wifi-pair {{ border-radius: {radius_sm}; padding: 0 2px; }}\
|
||||
.wifi-pair:hover {{ background: alpha({on_bg}, 0.12); }}\
|
||||
.wifi-popover-inner {{ min-width: 200px; padding: {pad}; }}\
|
||||
.popover-tab-row {{ margin-bottom: {pad}; }}\
|
||||
.popover-tab {{ background: transparent; color: {on_bg}; border: none; box-shadow: none;\
|
||||
outline: none; border-radius: {radius_sm}; padding: 4px 10px; font-size: 11px;\
|
||||
font-weight: bold; opacity: 0.55; }}\
|
||||
progressbar.osd-bar trough {{ background-image: none; background-color: alpha(@accent, 0.25);\
|
||||
border-radius: 3px; min-height: 6px; }}\
|
||||
progressbar.osd-bar trough progress {{ background-image: none; background-color: @accent;\
|
||||
border-radius: 3px; min-height: 6px; }}\
|
||||
.wifi-pair {{ padding: 6px; }}\
|
||||
window.breadbar-panel {{ background-color: alpha(@bg, 0.72); color: @on-bg;\
|
||||
border-radius: 14px; border: 1px solid alpha(@on-bg, 0.12); }}\
|
||||
window.breadbar-dismiss, .breadbar-dismiss-hit {{\
|
||||
background-color: alpha(#000000, 0.02); }}\
|
||||
.popover-caret {{ min-height: 2px; margin: 2px 4px 10px; border-radius: 2px;\
|
||||
background-color: @accent;\
|
||||
background-image: linear-gradient(90deg, @accent, @teal);\
|
||||
animation: caret-draw 0.45s cubic-bezier(0.22, 1.35, 0.36, 1) both; }}\
|
||||
.wifi-popover-inner {{ min-width: 228px; padding: {pad}; }}\
|
||||
window.wifi-popover button {{ min-height: 0; min-width: 0; }}\
|
||||
.popover-tab-row {{ background: alpha(@on-bg, 0.06); border-radius: 10px;\
|
||||
padding: 3px; margin-bottom: 10px; }}\
|
||||
.popover-tab {{ background: transparent; color: @on-bg; border: none; box-shadow: none;\
|
||||
outline: none; border-radius: 999px; padding: 0 14px; min-height: 32px;\
|
||||
font-size: 17px; font-weight: bold; opacity: 0.55;\
|
||||
transition: background-color 0.22s cubic-bezier(0.22, 1.2, 0.36, 1),\
|
||||
opacity 0.22s ease, color 0.22s ease; }}\
|
||||
.popover-tab:hover {{ opacity: 0.8; }}\
|
||||
.popover-tab:checked {{ background: alpha({accent}, 0.18); color: {accent}; opacity: 1; }}\
|
||||
.wifi-popover-ssid {{ font-weight: bold; font-size: 13px; }}\
|
||||
.wifi-popover-ip {{ opacity: 0.6; font-size: 11px; }}\
|
||||
.wifi-popover-status {{ font-size: 11px; margin-top: 2px; }}\
|
||||
.wifi-popover-section {{ font-size: 10px; font-weight: bold; opacity: 0.5; letter-spacing: 0.08em; }}\
|
||||
.popover-tab:checked {{ background: alpha(@accent, 0.22); color: @accent; opacity: 1; }}\
|
||||
.popover-tab label {{ padding: 0; margin: 0; }}\
|
||||
.wifi-popover-ssid {{ font-weight: bold; font-size: 18px; }}\
|
||||
.wifi-popover-ip {{ opacity: 0.6; font-size: 16px; }}\
|
||||
.wifi-popover-status {{ font-size: 16px; margin-top: 2px; }}\
|
||||
.wifi-popover-section {{ font-size: 13px; font-weight: bold; opacity: 0.45;\
|
||||
letter-spacing: 0.12em; }}\
|
||||
.wifi-popover-row {{ background: transparent; border: none; box-shadow: none;\
|
||||
border-radius: {radius_sm}; padding: 4px 6px; }}\
|
||||
.wifi-popover-row:hover {{ background: alpha({on_bg}, 0.08); }}\
|
||||
.wifi-popover-row-active {{ color: {accent}; }}\
|
||||
outline: none; border-radius: 10px; padding: 0 12px; min-height: 42px;\
|
||||
transition: background-color 0.18s cubic-bezier(0.22, 1.2, 0.36, 1); }}\
|
||||
.wifi-popover-row label {{ font-size: 18px; }}\
|
||||
.wifi-popover-row:hover {{ background: alpha(@on-bg, 0.08); }}\
|
||||
.wifi-popover-row-active {{ background: alpha(@accent, 0.14); color: @accent; }}\
|
||||
.wifi-popover-row-active:hover {{ background: alpha(@accent, 0.20); }}\
|
||||
.row-in {{ animation: row-in 0.32s cubic-bezier(0.22, 1.35, 0.36, 1) both; }}\
|
||||
.stagger-0 {{ animation-delay: 0ms; }} .stagger-1 {{ animation-delay: 28ms; }}\
|
||||
.stagger-2 {{ animation-delay: 56ms; }} .stagger-3 {{ animation-delay: 84ms; }}\
|
||||
.stagger-4 {{ animation-delay: 112ms; }} .stagger-5 {{ animation-delay: 140ms; }}\
|
||||
.stagger-6 {{ animation-delay: 168ms; }} .stagger-7 {{ animation-delay: 196ms; }}\
|
||||
.stagger-8 {{ animation-delay: 224ms; }} .stagger-9 {{ animation-delay: 252ms; }}\
|
||||
.stagger-10 {{ animation-delay: 280ms; }} .stagger-11 {{ animation-delay: 308ms; }}\
|
||||
.wifi-popover-row-unsaved {{ opacity: 0.4; }}\
|
||||
.wifi-popover-loading {{ opacity: 0.5; padding: 8px; }}\
|
||||
window.wifi-add-dialog {{ background-color: {bg_rgba}; color: {on_bg}; min-width: 240px;\
|
||||
border-radius: {radius}; }}\
|
||||
window.wifi-add-dialog headerbar {{ background-color: {bg_rgba}; color: {on_bg};\
|
||||
switch.bt-switch, switch.bt-switch:hover, switch.bt-switch:checked,\
|
||||
switch.bt-switch:checked:hover {{ min-width: 42px; min-height: 24px; padding: 2px;\
|
||||
border: none; outline: none; box-shadow: none; background-image: none;\
|
||||
border-radius: 99px; }}\
|
||||
switch.bt-switch {{ background-color: alpha(@on-bg, 0.14);\
|
||||
transition: background-color 0.25s cubic-bezier(0.22, 1.2, 0.36, 1); }}\
|
||||
switch.bt-switch:checked {{ background-color: @accent; }}\
|
||||
switch.bt-switch slider {{ min-width: 20px; min-height: 20px; margin: 0;\
|
||||
border-radius: 99px; border: none; outline: none; box-shadow: none;\
|
||||
background-image: none; background-color: @on-bg; }}\
|
||||
window.wifi-add-dialog {{ background-color: alpha(@bg, 0.70); color: @on-bg; min-width: 240px;\
|
||||
border-radius: {radius}; border: 1px solid alpha(@on-bg, 0.10);\
|
||||
animation: pop-in 0.45s cubic-bezier(0.22, 1.35, 0.36, 1) both; }}\
|
||||
window.wifi-add-dialog headerbar {{ background-color: alpha(@bg, 0.70); color: @on-bg;\
|
||||
border-top-left-radius: {radius}; border-top-right-radius: {radius};\
|
||||
border-bottom: 1px solid alpha({on_bg}, 0.08); box-shadow: none; }}\
|
||||
border-bottom: 1px solid alpha(@on-bg, 0.10); box-shadow: none; }}\
|
||||
.confirm-button {{ background-color: @accent; color: @on-accent; }}\
|
||||
.confirm-button:hover {{ background-color: alpha(@accent, 0.85); }}\
|
||||
.media-widget {{ border-radius: {radius_sm}; padding: 0 6px; }}\
|
||||
.media-widget:hover {{ background: alpha({on_bg}, 0.10); }}\
|
||||
.media-indicator {{ font-size: 11px; opacity: 0.7; margin-right: 2px; }}\
|
||||
.media-track-lbl {{ font-size: 12px; }}\
|
||||
.media-controls {{ padding: 2px; }}\
|
||||
.media-btn {{ min-width: 32px; padding: 4px 8px; }}\
|
||||
.control-panel-btn {{ padding: 0 6px; margin-left: 6px; border-radius: {radius_sm}; }}\
|
||||
.media-widget {{ border-radius: 10px; padding: 4px 8px; min-height: 0;\
|
||||
transition: background-color 0.22s cubic-bezier(0.22, 1.2, 0.36, 1); }}\
|
||||
.media-widget:hover {{ background: alpha(@on-bg, 0.08); }}\
|
||||
.media-widget.media-in {{ animation: row-in 0.4s cubic-bezier(0.22, 1.35, 0.36, 1) both; }}\
|
||||
.media-eq {{ min-height: 14px; margin-right: 4px; }}\
|
||||
.media-eq-bar {{ min-width: 3px; min-height: 5px; background-color: @accent;\
|
||||
border-radius: 2px; }}\
|
||||
.media-widget.playing .media-eq-bar {{\
|
||||
animation: media-eq 0.85s ease-in-out infinite alternate; }}\
|
||||
.media-widget.playing .media-eq-bar:nth-child(2) {{ animation-delay: 0.1s; min-height: 11px; }}\
|
||||
.media-widget.playing .media-eq-bar:nth-child(3) {{ animation-delay: 0.22s; min-height: 7px; }}\
|
||||
.media-widget.playing .media-eq-bar:nth-child(4) {{ animation-delay: 0.06s; min-height: 13px; }}\
|
||||
.media-track-lbl {{ font-size: 17px; }}\
|
||||
.media-controls {{ padding: 4px; }}\
|
||||
.media-btn {{ min-width: 32px; padding: 4px 8px; border-radius: {radius_sm};\
|
||||
transition: background-color 0.18s ease; }}\
|
||||
.media-btn:hover {{ background: alpha(@on-bg, 0.10); }}\
|
||||
.control-panel-btn {{ padding: 5px 8px; margin: 0; border-radius: 10px;\
|
||||
opacity: 0.92; font-size: 18px; line-height: 1; min-width: 0; min-height: 0;\
|
||||
background: transparent; border: none; outline: none; box-shadow: none;\
|
||||
transition: background-color 0.22s cubic-bezier(0.22, 1.2, 0.36, 1),\
|
||||
opacity 0.18s ease; }}\
|
||||
.control-panel-btn:hover {{ opacity: 1; background: alpha(@on-bg, 0.10); }}\
|
||||
.control-panel-btn:active {{ background: alpha(@on-bg, 0.16); }}\
|
||||
.control-panel {{ }}\
|
||||
.control-panel-inner {{ min-width: 220px; padding: {pad}; }}\
|
||||
.control-panel-row {{ margin: 4px 0; }}\
|
||||
.control-panel-row-icon {{ opacity: 1; margin-right: 4px; }}\
|
||||
.control-panel-slider {{ margin: 0; }}\
|
||||
.control-panel-stats {{ margin: {pad} 0; }}\
|
||||
.control-panel-stat {{ font-size: 12px; opacity: 0.85; margin: 1px 0; }}\
|
||||
.control-panel-section {{ margin: {pad} 0; }}\
|
||||
.control-panel-section-header {{ font-size: 10px; font-weight: bold; opacity: 0.5;\
|
||||
letter-spacing: 0.08em; margin-bottom: 4px; }}\
|
||||
.control-panel-sink-dropdown {{ }}\
|
||||
.power-row {{ margin-top: 2px; }}\
|
||||
.power-btn {{ min-width: 40px; padding: 8px; border-radius: {radius_sm}; }}\
|
||||
separator {{ margin: 4px 0; }}\
|
||||
.control-panel-inner {{ min-width: 248px; padding: {pad}; }}\
|
||||
.sys-grid {{ margin: 2px 0 6px; }}\
|
||||
.sys-stat {{ padding: 4px 2px; background: transparent; }}\
|
||||
.sys-stat:hover {{ background: transparent; }}\
|
||||
.control-panel-header {{ font-size: 12px; font-weight: bold; letter-spacing: 0.12em;\
|
||||
opacity: 0.45; margin-bottom: 8px; }}\
|
||||
.control-panel-row {{ margin: 8px 0; }}\
|
||||
.control-panel-row-label {{ font-size: 16px; opacity: 0.78; }}\
|
||||
.control-panel-slider {{ margin: 0; padding: 0; min-height: 18px; }}\
|
||||
scale.control-panel-slider trough {{ min-height: 6px; border-radius: 99px;\
|
||||
background-image: none; background-color: alpha(@on-bg, 0.12);\
|
||||
border: none; outline: none; box-shadow: none; }}\
|
||||
scale.control-panel-slider highlight {{ min-height: 6px; border-radius: 99px;\
|
||||
background-image: none; background-color: @accent; }}\
|
||||
scale.control-panel-slider slider {{ min-width: 0; min-height: 0; margin: 0;\
|
||||
padding: 0; opacity: 0; background: transparent; border: none;\
|
||||
outline: none; box-shadow: none; }}\
|
||||
.control-panel-section {{ margin: 8px 0 0; }}\
|
||||
.sink-row label {{ font-size: 15px; }}\
|
||||
.power-row {{ margin-top: 8px; }}\
|
||||
.power-btn {{ min-width: 0; min-height: 0; padding: 8px 10px; border-radius: 8px;\
|
||||
background: alpha(@on-bg, 0.08); font-size: 13px; border: none;\
|
||||
outline: none; box-shadow: none;\
|
||||
transition: background-color 0.2s cubic-bezier(0.22, 1.2, 0.36, 1); }}\
|
||||
.power-btn:hover {{ background: alpha(@on-bg, 0.14); }}\
|
||||
.power-btn:active {{ background: alpha(@accent, 0.22); }}\
|
||||
.notification-action {{ transition: background-color 0.18s ease; }}\
|
||||
.tray-btn {{ transition: opacity 0.2s ease, background-color 0.2s ease; }}\
|
||||
separator {{ margin: 4px 0; background: alpha(@on-bg, 0.10); }}\
|
||||
/* Lua-declared widgets (see Documentation.md's Widgets §style): the\
|
||||
slot rule below is what the four inline `.bread-widget-slot`\
|
||||
containers in main.rs rely on for the same 12px stat-pair rhythm\
|
||||
|
|
@ -157,14 +270,11 @@ fn load_css() -> String {
|
|||
.bread-padding-xs {{ padding: 4px; }}\
|
||||
.bread-padding-sm {{ padding: 8px; }}\
|
||||
.bread-padding-md {{ padding: 12px; }}",
|
||||
bg_plain = p.background,
|
||||
bg_rgba = hex_to_rgba(&p.background, 0.92),
|
||||
surface = p.color0,
|
||||
accent = p.color4,
|
||||
critical = p.color1,
|
||||
on_bg = ink_on(&p.background),
|
||||
on_surface = ink_on(&p.color0),
|
||||
trough = hex_to_rgba(&p.color4, 0.25),
|
||||
radius = radius,
|
||||
radius_bar = radius_bar,
|
||||
radius_sm = radius_sm,
|
||||
radius_pill = radius_pill,
|
||||
pad = pad,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -175,6 +285,31 @@ pub fn fg_color() -> String {
|
|||
ink_on(&load_palette().background).to_string()
|
||||
}
|
||||
|
||||
/// Ink colour for the given Hyprland output's wallpaper palette.
|
||||
#[allow(dead_code)]
|
||||
pub fn fg_color_for(output: &str) -> String {
|
||||
ink_on(&load_palette_for(output).background).to_string()
|
||||
}
|
||||
|
||||
/// Bind this window (and its popover children) to `output`'s palette.
|
||||
///
|
||||
/// App CSS still uses `@accent` / `@on-bg` tokens; `bind_window_with_app_css`
|
||||
/// resolves them against that output. Display-level [`apply`] stays as the
|
||||
/// SIGHUP / single-output fallback.
|
||||
pub fn bind_output(widget: &impl IsA<gtk4::Widget>, output: &str) {
|
||||
bgtk::bind_window_with_app_css(widget, output, load_css_for);
|
||||
}
|
||||
|
||||
/// Bind a satellite window (notification, history, OSD, wifi dialog) to
|
||||
/// whichever output it is actually rendered on.
|
||||
pub fn bind_auto(window: &impl IsA<gtk4::Native>) {
|
||||
bgtk::bind_window_auto_with_app_css(window, load_css_for);
|
||||
}
|
||||
|
||||
fn load_css_for(_palette: &Palette) -> String {
|
||||
load_css()
|
||||
}
|
||||
|
||||
/// Apply (or reload) the theme CSS. Safe to call from `glib::MainContext::invoke`.
|
||||
pub fn apply() {
|
||||
// Shared ecosystem base (fonts, palette, generic widgets) — applied first
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ const DEFAULT_LABEL_MAX_WIDTH_CHARS: i32 = 32;
|
|||
fn bundled_icon(name: &str) -> Option<&'static str> {
|
||||
use crate::bar::stats::{
|
||||
AC_POWER, BAT_HIGH, BAT_LOW, BAT_MID, BT_OFF, BT_ON, ICON_BRIGHTNESS, ICON_LOCK,
|
||||
ICON_VOLUME, WIFI_OFF, WIFI_STRONG,
|
||||
ICON_RESTART, ICON_SHUTDOWN, ICON_SLEEP, ICON_VOLUME, WIFI_MEDIUM, WIFI_OFF, WIFI_STRONG,
|
||||
WIFI_WEAK,
|
||||
};
|
||||
Some(match name {
|
||||
"ac-power" => AC_POWER,
|
||||
|
|
@ -34,8 +35,13 @@ fn bundled_icon(name: &str) -> Option<&'static str> {
|
|||
"bluetooth-on" => BT_ON,
|
||||
"bluetooth-off" => BT_OFF,
|
||||
"wifi-strong" => WIFI_STRONG,
|
||||
"wifi-medium" => WIFI_MEDIUM,
|
||||
"wifi-weak" => WIFI_WEAK,
|
||||
"wifi-off" => WIFI_OFF,
|
||||
"lock" => ICON_LOCK,
|
||||
"sleep" => ICON_SLEEP,
|
||||
"restart" => ICON_RESTART,
|
||||
"shutdown" => ICON_SHUTDOWN,
|
||||
"volume" => ICON_VOLUME,
|
||||
"brightness" => ICON_BRIGHTNESS,
|
||||
_ => return None,
|
||||
|
|
@ -171,6 +177,7 @@ pub fn build_node(node: &WidgetNode, widget_id: &str) -> gtk4::Widget {
|
|||
let px = size.unwrap_or(16).max(1) as u32;
|
||||
let texture = icon_texture(widget_id, name.as_deref(), path.as_deref(), px);
|
||||
let image = gtk4::Image::from_paintable(texture.as_ref());
|
||||
crate::prepare_icon(&image, px as i32);
|
||||
image.upcast()
|
||||
}
|
||||
WidgetNode::Progress { value, .. } => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue