From 65c62734e79f7dfe782c77602f19b0b7c702fbef Mon Sep 17 00:00:00 2001 From: server2 Date: Sat, 10 Oct 2026 00:24:48 +0200 Subject: [PATCH] glass: blur strength as a config value shell.conf blur (1 to 30, default 3; Hyprland's default is 8) sets how much Hyprland frosts what is behind the glass, applied the moment shell.conf is saved. Hyprland's blur strength is global (a layer rule can only switch blur on), so the shell sets decoration.blur.size through hyprctl eval, and again on every configreloaded>> event, since a config reload puts Hyprland's own value back. leaving blur out keeps Hyprland's --- README.md | 3 +++ shell.conf | 5 +++++ src/theme.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/README.md b/README.md index 98f6a9f..9abf59d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The Material You scheme type is Skwd-wall's setting (`matugen.schemeType`). cover_colors = false # true: the open media card and equalizer take the song cover's colours as background transition_ms = 400 # how long a colour change cross-fades; 0 switches instantly glass = 1.0 # surface opacity, 0.05 to 1; below 1 the desktop shows through +blur = 3 # Hyprland's blur strength behind the glass, 1 to 30 (Hyprland default 8) ``` ## Glass @@ -48,6 +49,8 @@ amane_glass = hl.layer_rule({ The look of the surfaces (fill, rim, highlight, shadow) lives in `src/surface.rs`. +`blur` in `shell.conf` sets how much the glass frosts: low values leave the background nearly readable. Hyprland's blur strength is one global setting (a layer rule can only turn blur on), so the shell sets `decoration.blur.size` for everything Hyprland blurs, and sets it again after every Hyprland config reload. Leave `blur` out to keep Hyprland's own value. + ## Layout ``` diff --git a/shell.conf b/shell.conf index 7f105fe..e908b45 100644 --- a/shell.conf +++ b/shell.conf @@ -8,6 +8,11 @@ cover_colors = false # how long a colour change cross-fades, in milliseconds; 0 switches instantly transition_ms = 400 +# how strongly Hyprland blurs what is behind the glass: 1 (barely, nearly readable) to 30; +# Hyprland's own default is 8. its blur strength is global, so this also changes the blur +# of any other translucent window. leave it out to keep Hyprland's value +blur = 3 + # how opaque the shell's surfaces are, 0.05 to 1 (1 = solid). below 1 the desktop shows # through; to frost it, Hyprland has to blur behind the shell (README: Glass) glass = 0.4 diff --git a/src/theme.rs b/src/theme.rs index edf9528..dcf406c 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,5 +1,8 @@ use std::cell::Cell; +use std::io::{BufRead, BufReader}; +use std::os::unix::net::UnixStream; use std::path::PathBuf; +use std::process::Command; use std::sync::{Mutex, PoisonError}; use std::time::Duration; use std::{env, fs, thread}; @@ -97,6 +100,9 @@ impl Service for Theme { thread::spawn(|| follow("shell.conf")); + // a hyprland config reload puts its own blur back: ask for ours again + thread::spawn(follow_hyprland); + follow("theme"); } } @@ -130,6 +136,59 @@ fn reload() { *last = Some(read); } +/* + * shell.conf `blur`: how strongly Hyprland blurs behind the glass. hyprland's blur + * strength is global (a layer rule can only switch blur on), so this sets + * decoration.blur.size for everything it blurs. none leaves hyprland's own value + */ +static BLUR: Mutex> = Mutex::new(None); + +fn set_blur(size: Option) { + let changed = { + let mut blur = BLUR.lock().unwrap_or_else(PoisonError::into_inner); + + let changed = *blur != size; + + *blur = size; + + changed + }; + + if changed { + send_blur(); + } +} + +// on its own thread, so hyprctl never holds up the theme +fn send_blur() { + let Some(size) = *BLUR.lock().unwrap_or_else(PoisonError::into_inner) else { + return; + }; + + thread::spawn(move || { + let lua = format!("hl.config({{ [\"decoration.blur.size\"] = {size} }})"); + + let _ = Command::new("hyprctl").arg("eval").arg(lua).output(); + }); +} + +// hyprland's event socket says configreloaded>> after every reload, which resets the blur +fn follow_hyprland() { + let (Some(runtime), Some(instance)) = (env::var_os("XDG_RUNTIME_DIR"), env::var_os("HYPRLAND_INSTANCE_SIGNATURE")) else { + return; + }; + + let Ok(events) = UnixStream::connect(PathBuf::from(runtime).join("hypr").join(instance).join(".socket2.sock")) else { + return; + }; + + for line in BufReader::new(events).lines().map_while(Result::ok) { + if line.starts_with("configreloaded") { + send_blur(); + } + } +} + fn read(name: &str) -> String { fs::read_to_string(config_file(name)).unwrap_or_default() } @@ -180,6 +239,8 @@ fn apply(theme: &str, config: &str, startup: bool) { theme.cover_colors = setting("cover_colors") == Some("true"); theme.glass = setting("glass").and_then(|glass| glass.parse::().ok()).map_or(1.0, |glass| glass.clamp(0.05, 1.0)); + set_blur(setting("blur").and_then(|size| size.parse::().ok()).map(|size| size.clamp(1, 30))); + // a change in the middle of a fade starts from the colours on screen now let now = theme.colors.value();