bread-theme: generate Hyprland layer rules from the shell theme (Phase 4a)

Adds `bread-theme layerrules`, which writes the active shell theme's
[compositor] table to ~/.config/hypr/layerrules.json (atomic write). This
lets ~/.config/hypr/scripts/ui/rules.lua read theme-driven blur/transparency/
animation for the breadbar/breadbox layer-shell namespaces instead of having
them hardcoded, following THEME_SYSTEM_PLAN.md §9. rules.lua keeps its
previous hardcoded rules as a pcall-guarded fallback for when the JSON is
missing or malformed (lives outside this repo, so not part of this commit).

Also fixes a pre-existing test race: shell::tests and the new
layerrules::tests both mutate XDG_CONFIG_HOME in parallel `cargo test`
threads but previously used separate, unrelated locks (or none), so they
could observe each other's env var changes mid-test. Both now share
bread_theme::test_support::XDG_CONFIG_HOME_LOCK.
This commit is contained in:
Breadway 2026-08-24 18:57:35 +08:00
parent 92d3362a6b
commit ea9758a5d5
6 changed files with 241 additions and 14 deletions

View file

@ -403,13 +403,6 @@ pub fn list() -> Vec<ThemeSummary> {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
// Guards mutation of XDG_CONFIG_HOME / BREAD_SHELL_THEME, which are
// process-global — mirrors bread_theme::output's XDG_ENV_LOCK pattern
// (a different env var, same reason: cargo test runs a module's tests
// in parallel by default).
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
@ -437,7 +430,12 @@ mod tests {
/// whatever's set in the outer test-runner environment. Held for the
/// guard's lifetime.
fn isolated_xdg() -> EnvGuard {
let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
// Shared with `layerrules::tests`, which also isolates
// XDG_CONFIG_HOME — see `crate::test_support` for why this must be
// the *same* lock rather than a module-private one.
let lock = crate::test_support::XDG_CONFIG_HOME_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir().join(format!(
"bread-theme-shell-test-{}-{}",
std::process::id(),

View file

@ -181,9 +181,18 @@ pub struct Surface {
/// ignore_alpha, blur_popups, animation, no_anim) — that Lua API isn't in
/// hyprland-api.lua's type annotations, so this field set is evidenced by
/// working usage, not documentation (plan §12 risk 3).
#[derive(Debug, Clone, PartialEq, Default)]
///
/// Also `Serialize`: this is the per-namespace shape written to
/// `~/.config/hypr/layerrules.json` by `bread_theme::layerrules` (plan §9
/// step 3-4), which `scripts/ui/rules.lua` parses back into `hl.layer_rule`
/// calls. `Option::None` fields are omitted rather than emitted as `null` —
/// the Lua JSON reader treats a missing key and a `null` value identically
/// (assigning `nil` into a table key is a no-op), so either encoding is
/// correct, but omitting keeps the file legible for hand inspection.
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize)]
pub struct LayerRule {
pub blur: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_alpha: Option<f64>,
pub blur_popups: bool,
/// Passed through verbatim to `hl.layer_rule`'s `animation` field
@ -192,6 +201,7 @@ pub struct LayerRule {
/// evidenced by working usage in `rules.lua`, not documented (plan §12
/// risk 3); a closed Rust enum here would need updating in lockstep
/// with Hyprland additions this crate has no way to know about.
#[serde(skip_serializing_if = "Option::is_none")]
pub animation: Option<String>,
pub no_anim: bool,
}