diff options
| -rwxr-xr-x | bin/udt-accent | 103 |
1 files changed, 101 insertions, 2 deletions
diff --git a/bin/udt-accent b/bin/udt-accent index 4dc7db3..53839b1 100755 --- a/bin/udt-accent +++ b/bin/udt-accent @@ -59,6 +59,21 @@ SDDM_KEYS = ["base", "mantle", "crust", "surface0", "surface1", "surface2", "text", "subtext0", "subtext1", "overlay0", "overlay1", "overlay2", "red", "yellow", "green", "teal", "blue", "lavender"] +# The SDDM panel's power icons. The greeter runs as the sddm user and cannot +# read the login user's dconf, so udt-accent resolves the selected icon theme +# to absolute paths and bakes them into the INI. An unresolved key stays empty +# and the theme draws its bundled SVG instead. +ICON_SCHEMA = "org.gnome.desktop.interface" +ICON_KEY = "icon-theme" +ICON_DIRS = [Path.home() / ".local" / "share" / "icons", + Path("/usr/share/icons")] +POWER_ICONS = { + "powerIcon": ["system-shutdown"], + "rebootIcon": ["system-reboot"], + "suspendIcon": ["system-suspend"], + "hibernateIcon": ["system-hibernate", "system-suspend-hibernate"], +} + def write_atomic(path, content, mode=None, dir_mode=None): """Write a file atomically, so no reader ever sees it half-written. @@ -308,7 +323,55 @@ def write_sublime(name): write_atomic(SUBLIME, new) -def sddm_conf(palette, name, image): +def icon_theme(): + """The icon theme the appearance drawer selected, or "" if unreadable. + + gsettings runs as the login user, so it reads the user's dconf. Empty on + any failure: the theme then keeps its bundled power icons. + """ + try: + got = subprocess.run(["gsettings", "get", ICON_SCHEMA, ICON_KEY], + capture_output=True, text=True, check=True) + except (OSError, subprocess.CalledProcessError): + return "" + return got.stdout.strip().strip("'") + + +def find_icon(theme, names, dirs=None): + """Absolute path to the first of `names` found in `theme`, else "". + + A theme may ship an icon flat or as a symbolic variant at several sizes. + Prefer the symbolic variant, then the largest size directory, so the panel + gets a tintable silhouette. `dirs` is injectable for the selftest. + """ + if not theme: + return "" + dirs = ICON_DIRS if dirs is None else dirs + wanted = set(names) | {f"{name}-symbolic" for name in names} + matches = [] + for base in dirs: + root = base / theme + if not root.is_dir(): + continue + matches = [p for p in root.rglob("*.svg") if p.stem in wanted] + if matches: + break + + def rank(path): + symbolic = path.stem.endswith("-symbolic") + sizes = [int(part) for part in path.parts if part.isdigit()] + return (symbolic, max(sizes, default=0)) + + return str(max(matches, key=rank)) if matches else "" + + +def power_icons(): + """The four power icon paths, resolved from the selected icon theme.""" + theme = icon_theme() + return {key: find_icon(theme, names) for key, names in POWER_ICONS.items()} + + +def sddm_conf(palette, name, image, icons=None): """The SDDM theme config, as an INI fragment. theme.conf.user overrides theme.conf and is exposed to the theme as the @@ -323,6 +386,8 @@ def sddm_conf(palette, name, image): f"accent={ACCENTS[name]}", ] lines += [f"{key}={palette.get(key, '#000000')}" for key in SDDM_KEYS] + icons = icons or {} + lines += [f"{key}={icons.get(key, '')}" for key in POWER_ICONS] return "\n".join(lines) + "\n" @@ -338,7 +403,7 @@ def write_sddm(name, image): print("udt-accent: palette.rasi unreadable, skipping SDDM theme", file=sys.stderr) return - write_atomic(SDDM_OUTPUT, sddm_conf(palette, name, image), + write_atomic(SDDM_OUTPUT, sddm_conf(palette, name, image, power_icons()), mode=0o644, dir_mode=0o755) @@ -449,6 +514,40 @@ def selftest(): assert outdir.stat().st_mode & 0o777 == 0o755, \ oct(outdir.stat().st_mode & 0o777) + # Power icons: an unknown or empty theme resolves to empty, so the greeter + # falls back to its bundled SVGs; a real theme prefers the symbolic variant + # and a name list is honoured in order. + assert find_icon("", ["system-shutdown"]) == "" + assert find_icon("no-such-theme-udt", ["system-shutdown"]) == "" + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) / "icons" + base = root / "probe" + (base / "actions" / "16").mkdir(parents=True) + (base / "actions" / "symbolic").mkdir(parents=True) + (base / "actions" / "16" / "system-shutdown.svg").write_text("<svg/>") + (base / "actions" / "symbolic" / "system-shutdown-symbolic.svg").write_text("<svg/>") + got = find_icon("probe", ["system-shutdown"], dirs=[root]) + assert got.endswith("actions/symbolic/system-shutdown-symbolic.svg"), got + (base / "actions" / "symbolic" / "system-shutdown-symbolic.svg").unlink() + got = find_icon("probe", ["system-shutdown"], dirs=[root]) + assert got.endswith("actions/16/system-shutdown.svg"), got + assert find_icon("probe", ["system-hibernate", "system-suspend-hibernate"], + dirs=[root]) == "" + + with tempfile.TemporaryDirectory() as tmpdir: + probe_image = Path(tmpdir) / "wall.png" + probe_image.write_bytes(b"") + palette = {k: "#1e2030" for k in SDDM_KEYS} + icons = {"powerIcon": "/tmp/power.svg", "rebootIcon": "", + "suspendIcon": "", "hibernateIcon": ""} + text = sddm_conf(palette, FALLBACK, str(probe_image), icons) + for key in ("powerIcon", "rebootIcon", "suspendIcon", "hibernateIcon"): + assert f"\n{key}=" in "\n" + text, f"sddm conf missing {key}" + assert "\npowerIcon=/tmp/power.svg" in "\n" + text + + got = power_icons() + assert set(got) == {"powerIcon", "rebootIcon", "suspendIcon", "hibernateIcon"} + print("selftest OK") |
