From 04bd03db72c7165debb79d1858f39121757ca358 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 19:19:58 +0100 Subject: feat: animate the UDT border gradient on the cards --- deckit/theme.py | 19 +++++++++++++++++-- static/deck.css | 30 +++++++++++++++++++++++++++++- tests/test_theme.py | 10 ++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/deckit/theme.py b/deckit/theme.py index 7aa347b..5603d5b 100644 --- a/deckit/theme.py +++ b/deckit/theme.py @@ -10,9 +10,11 @@ from pathlib import Path PALETTE_LINE = re.compile(r"^\s*([a-z0-9]+)\s*:\s*(#[0-9a-fA-F]{6,8});", re.M) ACCENT = re.compile(r"accent:\s*(#[0-9a-fA-F]{6,8})") +BORDER = re.compile(r"rgb\(\s*([0-9a-fA-F]{6})\s*\)") PALETTE_PATH = Path.home() / ".config" / "rofi" / "udt" / "palette.rasi" ACCENT_PATH = Path.home() / ".cache" / "udt" / "udt-accent.rasi" +BORDER_PATH = Path.home() / ".cache" / "udt" / "udt-border.lua" # Catppuccin Macchiato, used before UDT has ever generated its files. FALLBACK = { @@ -35,10 +37,19 @@ def parse_accent(text: str) -> str | None: return m[1] if m else None -def css(colors: dict, accent: str | None) -> str: +def parse_border(text: str) -> list[str]: + """The three hues Hyprland rotates in the window border, as hex.""" + return ["#" + value.lower() for value in BORDER.findall(text)] + + +def css(colors: dict, accent: str | None, border: list[str] | None = None) -> str: lines = [":root {"] lines += [f" --udt-{name}: {value};" for name, value in colors.items()] lines.append(f" --udt-accent: {accent or FALLBACK['accent']};") + stops = border if border and len(border) >= 3 else [ + FALLBACK["mauve"], FALLBACK["lavender"], FALLBACK["blue"]] + for index, value in enumerate(stops[:3], 1): + lines.append(f" --udt-border-{index}: {value};") lines.append("}") return "\n".join(lines) + "\n" @@ -55,4 +66,8 @@ def load() -> str: accent = parse_accent(ACCENT_PATH.read_text()) except OSError: accent = None - return css(colors, accent) + try: + border = parse_border(BORDER_PATH.read_text()) + except OSError: + border = None + return css(colors, accent, border) diff --git a/static/deck.css b/static/deck.css index 8b2416c..e200507 100644 --- a/static/deck.css +++ b/static/deck.css @@ -42,6 +42,12 @@ body { .panel { min-height: 0; min-width: 0; } +@property --border-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + .grid-stack-item-content.panel { position: absolute; display: flex; @@ -50,10 +56,32 @@ body { overflow: hidden; border-radius: 16px; background: var(--udt-surface0); - border: 1px solid var(--udt-surface1); + border: 1px solid transparent; box-shadow: 0 10px 30px rgba(0, 0, 0, .18); } +/* The same rotating three-hue gradient Hyprland draws on window borders, + read from ~/.cache/udt/udt-border.lua via /theme.css. */ +.grid-stack-item-content.panel::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1.5px; + background: linear-gradient(var(--border-angle), + var(--udt-border-1), var(--udt-border-2), var(--udt-border-3), var(--udt-border-1)); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask-composite: exclude; + pointer-events: none; + animation: border-rotate 8s linear infinite; +} + +@keyframes border-rotate { + to { --border-angle: 360deg; } +} + .panel-head { flex: none; display: flex; diff --git a/tests/test_theme.py b/tests/test_theme.py index 6530af5..4b8b2de 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -29,6 +29,16 @@ class Theme(unittest.TestCase): self.assertIn("--udt-base: #24273a", out) self.assertIn("--udt-accent:", out) # fallback accent + def test_parses_border_hues(self): + self.assertEqual( + theme.parse_border('return { "rgb(58a6ff)", "rgb(a371f7)", "rgb(db61a2)" }'), + ["#58a6ff", "#a371f7", "#db61a2"]) + + def test_css_exposes_border_vars(self): + out = theme.css({"base": "#24273a"}, None, ["#111111", "#222222", "#333333"]) + for index, value in enumerate(["#111111", "#222222", "#333333"], 1): + self.assertIn(f"--udt-border-{index}: {value}", out) + def test_load_falls_back_when_files_are_missing(self): saved = (theme.PALETTE_PATH, theme.ACCENT_PATH) theme.PALETTE_PATH = Path("/nonexistent/palette.rasi") -- cgit v1.2.3