diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua index f03e5b5..cfa488b 100644 --- a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua +++ b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua @@ -63,6 +63,16 @@ end hl.window_rule({ name = "breadhelp", match = { class = "^(com\\.breadway\\.breadhelp)$" }, float = true, size = { 880, 600 } }) hl.window_rule({ name = "bos-netsetup", match = { class = "^(bos-netsetup)$" }, float = true, size = { 700, 560 } }) +-- --------------------------------------------------------------------------- +-- Layer-shell rules — blur / ignore-alpha / per-namespace motion for the +-- bread shell (breadbar island + osd + notif + panel, breadbox launcher, +-- breadclip popup). Theme-driven via ~/.config/hypr/layerrules.json (written +-- by `bread-theme layerrules` in the bootstrap below), with a hardcoded +-- fallback so a missing/broken theme file can never leave the shell +-- unblurred. Without these the launcher and popups get no compositor blur. +-- --------------------------------------------------------------------------- +pcall(dofile, script_dir .. "ui/rules.lua") + -- --------------------------------------------------------------------------- -- Environment (vendor-neutral; no GPU-specific vars so it works on Intel/AMD). -- --------------------------------------------------------------------------- @@ -76,6 +86,15 @@ hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1") hl.env("SDL_VIDEODRIVER", "wayland") hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") hl.env("_JAVA_AWT_WM_NONREPARENTING", "1") +-- GTK4's default renderer (ngl/vulkan on GTK 4.14+) renders transparent +-- layer-shell surfaces as opaque black on wlroots — the whole bread shell +-- (breadbox launcher, breadclip popup, breadbar, breadhelp, bos-settings) +-- goes black-on-black, and it's worse under a VM's virtio-gpu where no real +-- GL/Vulkan context is available. cairo (software) always composites +-- transparency correctly; the shell is flat colour/text/icons so there's no +-- visible perf cost, and idle memory drops (~40%, no Mesa driver resident). +-- Set GSK_RENDERER=gl in the session to override for a GPU-heavy GTK4 app. +hl.env("GSK_RENDERER", "cairo") -- Optional NVIDIA env from bos-nvidia-setup. Mesa machines have no file. -- bos-nvidia-setup: optional proprietary env; no-op when the file is absent @@ -128,6 +147,12 @@ hl.on("hyprland.start", function() -- Generate the shared bread GUI stylesheet first, so breadbar/breadbox/ -- bos-settings load it on start (they also live-reload if it changes). "bread-theme generate", + -- Write ~/.config/hypr/layerrules.json from the active shell theme's + -- [compositor] table. scripts/ui/rules.lua (loaded above) already ran + -- with the hardcoded fallback; this refreshes the JSON for the next + -- `hyprctl reload` / theme switch. Harmless if bread-theme lacks the + -- verb on an older build. + "sh -c 'bread-theme layerrules 2>/dev/null || true'", -- Global dark theme: GTK4/libadwaita + GTK3 theme + icon + cursor. "gsettings set org.gnome.desktop.interface color-scheme prefer-dark", "gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark", diff --git a/iso/airootfs/etc/skel/.config/hypr/scripts/ui/rules.lua b/iso/airootfs/etc/skel/.config/hypr/scripts/ui/rules.lua new file mode 100644 index 0000000..79e0a2f --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/scripts/ui/rules.lua @@ -0,0 +1,136 @@ +-- scripts/ui/rules.lua — layer-shell (compositor) rules for the bread shell. +-- +-- Blur / ignore-alpha / per-namespace motion for breadbar (island + osd + +-- notif + panel + dismiss), the breadbox launcher, and the breadclip popup. +-- Window rules (float/centre for onboarding popups) live in hyprland.lua; +-- this file is layer rules only. +-- +-- Theme-driven (bos-ui-demos THEME_SYSTEM_PLAN.md §9): `bread-theme +-- layerrules` writes ~/.config/hypr/layerrules.json from the active shell +-- theme's [compositor] table — blur / ignore_alpha / blur_popups / +-- animation / no_anim, keyed by layer-shell namespace. That table owns +-- *appearance* only; placement / workspace / focus are never touched here. +-- If the JSON is missing, unreadable, malformed, or anything goes wrong +-- while parsing it, apply_hardcoded_layer_rules() runs instead — the exact +-- set this file hardcoded before the theme system existed — so a broken or +-- absent theme file can never disable compositor blur. + +local LAYERRULES_JSON = os.getenv("HOME") .. "/.config/hypr/layerrules.json" + +local function apply_hardcoded_layer_rules() + hl.layer_rule({ + name = "breadbar-island", + match = { namespace = "^breadbar$" }, + blur = true, + ignore_alpha = 0.2, + blur_popups = true, + animation = "slide top", + }) + + hl.layer_rule({ + name = "breadbar-osd", + match = { namespace = "^breadbar-osd$" }, + blur = true, + ignore_alpha = 0.2, + animation = "slide bottom", + }) + + hl.layer_rule({ + name = "breadbar-notif", + match = { namespace = "^breadbar-notif$" }, + blur = true, + ignore_alpha = 0.2, + animation = "slide right", + }) + + hl.layer_rule({ + name = "breadbar-panel", + match = { namespace = "^breadbar-panel$" }, + blur = true, + ignore_alpha = 0.2, + animation = "slide right", + }) + + hl.layer_rule({ + name = "breadbar-dismiss", + match = { namespace = "^breadbar-dismiss$" }, + no_anim = true, + }) + + hl.layer_rule({ + match = "breadbox", + blur = true, + ignore_alpha = 0.2, + }) +end + +-- Parses ~/.config/hypr/layerrules.json into a plain array of per-namespace +-- rule tables, sorted by namespace for a deterministic emission order. +-- Returns nil (not an error) for anything short of a well-formed, non-empty +-- JSON object, so the pcall wrapping this only guards against a genuine Lua +-- error (e.g. json.lua failing to load) rather than every malformed case. +local function parsed_layer_rules() + local json = dofile(os.getenv("HOME") .. "/.config/hypr/scripts/lib/json.lua") + local parsed = json.load(LAYERRULES_JSON) + if type(parsed) ~= "table" then + return nil + end + + local namespaces = {} + for ns, rule in pairs(parsed) do + if type(ns) == "string" and type(rule) == "table" then + namespaces[#namespaces + 1] = ns + end + end + if #namespaces == 0 then + return nil + end + table.sort(namespaces) + + local rules = {} + for _, ns in ipairs(namespaces) do + local r = parsed[ns] + rules[#rules + 1] = { + namespace = ns, + blur = r.blur == true, + ignore_alpha = r.ignore_alpha, + blur_popups = r.blur_popups == true, + animation = r.animation, + no_anim = r.no_anim == true, + } + end + return rules +end + +-- Emits one hl.layer_rule per parsed namespace. Built as pure data by +-- parsed_layer_rules() first (no hl.layer_rule calls during parsing), so a +-- parse failure can never leave a partial JSON-derived rule set applied +-- before the caller falls back to apply_hardcoded_layer_rules(). +local function apply_json_layer_rules(rules) + for _, r in ipairs(rules) do + hl.layer_rule({ + name = r.namespace, + match = { namespace = "^" .. r.namespace .. "$" }, + blur = r.blur, + ignore_alpha = r.ignore_alpha, + blur_popups = r.blur_popups, + animation = r.animation, + no_anim = r.no_anim, + }) + end +end + +local ok, rules = pcall(parsed_layer_rules) +if ok and rules then + apply_json_layer_rules(rules) +else + apply_hardcoded_layer_rules() +end + +-- breadclip is not part of the shell theme's [compositor] table yet — always +-- hardcoded, regardless of the JSON above. +hl.layer_rule({ + match = "breadclip", + blur = true, + ignore_alpha = 0.0, +})