aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 19:19:58 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 19:19:58 +0100
commit04bd03db72c7165debb79d1858f39121757ca358 (patch)
tree1cf8f6d7be63731cf5ddedcecfabdd29670c1d6e
parent67e64e964a85562300b463378f0e260d6978867f (diff)
downloaddeck-it-04bd03db72c7165debb79d1858f39121757ca358.tar.gz
deck-it-04bd03db72c7165debb79d1858f39121757ca358.zip
feat: animate the UDT border gradient on the cards
-rw-r--r--deckit/theme.py19
-rw-r--r--static/deck.css30
-rw-r--r--tests/test_theme.py10
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: "<angle>";
+ 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")