aboutsummaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 14:21:20 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 14:21:20 +0200
commitd046bfa70d4276aecc2e80eea1924a7c0a5dc3ce (patch)
tree7227c71135621af575ce00ba9b0ea8f2b4cc4cb0 /bin
parentc474d7bff63496750c88039c4bd55f81dd1d3541 (diff)
downloadunified-desktop-theme-d046bfa70d4276aecc2e80eea1924a7c0a5dc3ce.tar.gz
unified-desktop-theme-d046bfa70d4276aecc2e80eea1924a7c0a5dc3ce.zip
feat: resolve SDDM power icons from the selected icon theme
The greeter runs as the sddm user and cannot read the login user's dconf, so udt-accent resolves the four FreeDesktop power icon names in the icon theme gsettings reports and bakes absolute paths into the generated sddm-theme.conf. An unresolved key stays empty and the theme falls back to its bundled SVGs.
Diffstat (limited to 'bin')
-rwxr-xr-xbin/udt-accent103
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")