aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-11 17:20:50 +0200
committerDanilo M. <danix@danix.xyz>2026-09-11 17:20:50 +0200
commit7b55910a786c7652ac80a576399ebd9f81a8ac72 (patch)
tree4adaedb0f0ac5003ecc0e19093a2cb5871b18deb
parentc7cb9dad4e782ee0ff99773c3607ec3b7792c723 (diff)
downloadunified-desktop-theme-7b55910a786c7652ac80a576399ebd9f81a8ac72.tar.gz
unified-desktop-theme-7b55910a786c7652ac80a576399ebd9f81a8ac72.zip
feat(palette): generate every themed config from one scheme
Colours were written down five times: rofi's palette.rasi, the waybar theme, the kitty theme, the conky and dunst templates, and a MACCHIATO dict inside udt-accent. Changing scheme meant finding all five, and they had already drifted: the active waybar theme was Macchiato while theme.css next to it was Mocha, and nothing imported the latter. Now a scheme is two files. palette/<scheme>.conf holds the colours under the scheme's own names, and palette/roles-<scheme>.conf says what each is for. palette/roles.conf is one `scheme =` line selecting the pair. bin/udt-palette renders that into each consumer's syntax and install.sh runs it first. The role map is per-scheme rather than shared because the palettes are genuinely different shapes: Nord has no `base`, Dracula no `surface0`, and forcing Catppuccin's names onto them would have meant either renaming their colours or inventing values. A role names a palette colour with two optional modifiers, `name/75` for alpha and `name*50` for brightness. The second exists because GTK's shade() has no palette name to point at. Ships macchiato, tokyo-night, nord and dracula. --selftest resolves every role in every scheme and asserts they define the same set, so a scheme that cannot satisfy a consumer fails at install rather than when someone switches to it. Verified against the live configs: all 46 waybar colours resolve identically to the previously active theme including its alpha() and shade() derivations, the kitty theme is colour-for-colour identical, and the rofi palette has the same 26 values. Generated files are gitignored; switching scheme and switching back reproduces the originals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gbjA2bmswN8jyPDKzrvqe
-rw-r--r--.gitignore9
-rw-r--r--AGENTS.md33
-rw-r--r--README.md15
-rwxr-xr-xbin/udt-accent49
-rwxr-xr-xbin/udt-palette416
-rwxr-xr-xinstall.sh25
-rw-r--r--palette/dracula.conf29
-rw-r--r--palette/macchiato.conf40
-rw-r--r--palette/nord.conf41
-rw-r--r--palette/roles-dracula.conf101
-rw-r--r--palette/roles-macchiato.conf115
-rw-r--r--palette/roles-nord.conf101
-rw-r--r--palette/roles-tokyo-night.conf99
-rw-r--r--palette/roles.conf14
-rw-r--r--palette/tokyo-night.conf43
-rw-r--r--rofi/udt/palette.rasi41
-rw-r--r--templates/conky.conf.in102
-rw-r--r--templates/dunstrc.in75
-rw-r--r--templates/terminal/kitty-macchiato.conf84
-rw-r--r--templates/waybar/catppuccin-macchiato.css63
20 files changed, 1269 insertions, 226 deletions
diff --git a/.gitignore b/.gitignore
index 7277c82..e7bab11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,12 @@
HANDOFF.md
__pycache__/
*.pyc
+
+# Generated by udt-palette from palette/<scheme>.conf. Regenerated by
+# install.sh, so tracking them would turn every scheme switch into a diff.
+/rofi/udt/palette.rasi
+/templates/dunstrc
+/templates/conky.conf
+/templates/waybar/theme.css
+/templates/terminal/kitty-theme.conf
+/bin/udt_colors.py
diff --git a/AGENTS.md b/AGENTS.md
index cf9f914..fd40b57 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,6 +13,35 @@ the working config cannot drift from what is committed. Everything outside
`~/.config/rofi/udt/` and `~/bin/` was edited in place; `docs/MIGRATION.md` is
the record of what changed and where the backups are.
+## One palette, many consumers
+
+Colours are written down once. `palette/<scheme>.conf` holds them under the
+scheme's own names, `palette/roles-<scheme>.conf` says what each is for, and
+`palette/roles.conf` is a single `scheme = ` line selecting the pair.
+`bin/udt-palette` renders that into every consumer's own syntax; `install.sh`
+runs it first, before anything is linked.
+
+Four schemes ship: `macchiato`, `tokyo-night`, `nord`, `dracula`. Switching is
+one line plus `./install.sh`.
+
+Generated files are gitignored, because tracking them would turn every scheme
+switch into a diff. Editing one is pointless: the next install overwrites it.
+The generated set is `rofi/udt/palette.rasi`, `templates/dunstrc`,
+`templates/conky.conf`, `templates/waybar/theme.css`,
+`templates/terminal/kitty-theme.conf` and `bin/udt_colors.py`. The last is
+imported by `udt-accent`, which is why its accent table follows the scheme.
+
+Roles are a flat namespace, so a name collides across sections: conky's outline
+is `body_outline` because waybar already has `outline`. A role value is a
+palette name with two optional modifiers, `name/75` for 75% alpha and
+`name*50` for half brightness. The second exists only because GTK's
+`shade(@main-bg, 0.5)` has no palette name to point at: half-brightness crust
+is darker than the darkest colour any of these schemes ships.
+
+**Adding a scheme means adding two files, never editing a consumer.** Every
+scheme must define the same role set; `udt-palette --selftest` fails if one
+drifts, which is what stops a switch from breaking a config nobody looked at.
+
## The accent
One colour moves: `@accent`. `bin/udt-accent` reads the current wallpaper,
@@ -111,7 +140,9 @@ them by exit code: rofi exits non-zero for ordinary reasons such as Escape.
- **Themes that reference an image cannot be committed with a real path.**
`launcher.rasi` carries an `@WPAPER@` placeholder that `install.sh` rewrites.
rofi does not expand `~` inside a `url()`.
-- After editing anything under `rofi/udt/`, run `./install.sh`.
+- After editing anything under `rofi/udt/` or `palette/`, run `./install.sh`.
+- `bin/udt-palette --selftest` resolves every role in every scheme. Run it
+ after touching a palette, a role map, or the generator.
- `bin/udt-accent --selftest` covers the snapping logic. Run it after touching
the colour maths.
diff --git a/README.md b/README.md
index 772e969..2cd2a57 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,21 @@ the pywal cache.
GPLv2 only. See `LICENSE`.
+## Changing the colour scheme
+
+Edit one line in `palette/roles.conf`:
+
+ scheme = macchiato
+
+and run `./install.sh`. Ships with `macchiato`, `tokyo-night`, `nord` and
+`dracula`; rofi, dunst, conky, waybar, kitty, Firefox and quickshell all
+follow.
+
+A scheme is two files: `palette/<name>.conf` for the colours under the
+scheme's own names, and `palette/roles-<name>.conf` saying what each is for.
+Nothing else needs touching, and `bin/udt-palette --selftest` checks that a
+scheme can satisfy every role before you switch to it.
+
## Development Approach
This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
diff --git a/bin/udt-accent b/bin/udt-accent
index 7949597..1a222b5 100755
--- a/bin/udt-accent
+++ b/bin/udt-accent
@@ -21,25 +21,12 @@ import sys
import tempfile
from pathlib import Path
-# The candidate accents. rosewater and flamingo are excluded as near-neutral
-# tints that capture saturated inputs, and maroon and sapphire as near-duplicate
-# hues of red and sky. lavender sits close to mauve but is kept: it is the
-# desktop-wide fallback, used by Qt, GTK and anything else that needs one fixed
-# accent, so it has to be a value snap() can also return.
-ACCENTS = {
- "pink": "#f5bde6",
- "mauve": "#c6a0f6",
- "lavender": "#b7bdf8",
- "red": "#ed8796",
- "peach": "#f5a97f",
- "yellow": "#eed49f",
- "green": "#a6da95",
- "teal": "#8bd5ca",
- "sky": "#91d7e3",
- "blue": "#8aadf4",
-}
-
-FALLBACK = "lavender"
+# The colour tables are generated: udt-palette renders them from
+# palette/<scheme>.conf and palette/roles-<scheme>.conf, so the accents this
+# snaps to follow whatever scheme is selected. Regenerate with ./install.sh.
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from udt_colors import ACCENTS, FALLBACK, PALETTE # noqa: E402
+
MIN_CHROMA = 10.0
OUTPUT = Path.home() / ".cache" / "wal" / "udt-accent.rasi"
@@ -47,24 +34,12 @@ BORDER_OUTPUT = Path.home() / ".cache" / "wal" / "udt-border.lua"
DUNSTRC = Path.home() / ".cache" / "wal" / "dunstrc"
QML_OUTPUT = Path.home() / ".cache" / "wal" / "udt-palette.qml"
-# The installed palette, which is a symlink back into this repo. Read rather
-# than duplicated: palette.rasi is the one place the structural colours are
-# written down, and a second copy here would drift the first time one changed.
+# The installed rofi palette, itself generated by udt-palette. Read rather than
+# duplicated so the QML singleton carries exactly the colours rofi uses, under
+# the same names.
PALETTE_RASI = Path.home() / ".config" / "rofi" / "udt" / "palette.rasi"
COLORS_JSON = Path.home() / ".cache" / "wal" / "colors.json"
-# Catppuccin Macchiato, in pywal's slot order. Firefox is themed from this
-# via pywalfox, which reads colors.json and nothing else.
-MACCHIATO = {
- "background": "#24273a", "foreground": "#cad3f5", "cursor": "#f4dbd6",
- "colors": [
- "#494d64", "#ed8796", "#a6da95", "#eed49f",
- "#8aadf4", "#f5bde6", "#8bd5ca", "#b8c0e0",
- "#5b6078", "#ed8796", "#a6da95", "#eed49f",
- "#8aadf4", "#f5bde6", "#8bd5ca", "#a5adcb",
- ],
-}
-
def write_atomic(path, content):
"""Write a file atomically, so no reader ever sees it half-written.
@@ -256,7 +231,7 @@ def write_colors_json(name, image):
palette stays fixed Macchiato while only the accent moves.
"""
hexval = ACCENTS[name]
- colors = list(MACCHIATO["colors"])
+ colors = list(PALETTE["colors"])
# Slots 4 and 12 are pywalfox's link/highlight colour.
colors[4] = colors[12] = hexval
@@ -267,8 +242,8 @@ def write_colors_json(name, image):
"wallpaper": os.path.realpath(image),
"alpha": "100",
"special": {
- "background": MACCHIATO["background"],
- "foreground": MACCHIATO["foreground"],
+ "background": PALETTE["background"],
+ "foreground": PALETTE["foreground"],
"cursor": hexval,
},
"colors": {f"color{i}": c for i, c in enumerate(colors)},
diff --git a/bin/udt-palette b/bin/udt-palette
new file mode 100755
index 0000000..b2951c6
--- /dev/null
+++ b/bin/udt-palette
@@ -0,0 +1,416 @@
+#!/usr/bin/env python3
+# udt-palette: generate every themed config from one palette and one role map.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+"""Render the palette into each consumer's own syntax.
+
+Usage: udt-palette [--roles <file>] [--out <dir>]
+ udt-palette --selftest
+
+The point of this script is that a colour is written down once. palette/<scheme>
+.conf holds the colours under the scheme's own names, palette/roles.conf says
+what each is for, and everything else here is generated. Editing a generated
+file is pointless: the next install.sh overwrites it.
+"""
+
+import re
+import sys
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parent.parent
+PALETTE_DIR = REPO / "palette"
+
+
+def parse_conf(path):
+ """Parse `key = value` lines into a dict, preserving order.
+
+ [section] headers are read but not nested: roles are a flat namespace, and
+ the sections exist to group the file for a human reader. A duplicate key is
+ an error rather than a silent last-wins, because two roles quietly fighting
+ is exactly the drift this file exists to prevent.
+ """
+ out = {}
+ for lineno, raw in enumerate(path.read_text().splitlines(), 1):
+ # A comment is a whole line starting with #, or a trailing "# " after
+ # a value. Splitting on a bare "#" would eat every colour, since the
+ # values are hex literals that start with one.
+ line = raw.strip()
+ if line.startswith("#"):
+ continue
+ line = re.split(r"\s#\s", line, maxsplit=1)[0].strip()
+ if not line or line.startswith("["):
+ continue
+ if "=" not in line:
+ raise ValueError(f"{path}:{lineno}: expected 'key = value', got {raw!r}")
+ key, value = (p.strip() for p in line.split("=", 1))
+ if key in out:
+ raise ValueError(f"{path}:{lineno}: duplicate key {key!r}")
+ out[key] = value
+ return out
+
+
+def resolve(roles, palette, roles_path):
+ """Turn every role into (r, g, b, alpha). Unknown colour names are fatal.
+
+ A role names a palette colour, optionally with an alpha percentage:
+ `text/75`. Failing here is the whole safety story: a typo or a name the new
+ scheme does not carry stops the build, rather than emitting a config with a
+ black hole where a colour should be.
+ """
+ resolved = {}
+ for role, spec in roles.items():
+ if role in ("scheme", "snap"):
+ continue
+ # `name/75` is 75% alpha; `name*50` is the colour at 50% brightness.
+ # shade exists because GTK's shade() has no palette name to point at:
+ # half-brightness crust is darker than the darkest colour a scheme
+ # ships, so it can only be computed.
+ rest, _, alpha_s = spec.partition("/")
+ name, _, shade_s = rest.partition("*")
+ name = name.strip()
+ if name not in palette:
+ available = ", ".join(sorted(palette))
+ raise ValueError(
+ f"{roles_path}: role {role!r} wants colour {name!r}, which the "
+ f"{roles['scheme']} palette does not define.\nAvailable: {available}")
+ alpha = 100
+ if alpha_s:
+ alpha = int(alpha_s.strip())
+ if not 0 <= alpha <= 100:
+ raise ValueError(f"{roles_path}: role {role!r} alpha {alpha} out of 0-100")
+ hexval = palette[name]
+ r, g, b = (int(hexval[i:i + 2], 16) for i in (1, 3, 5))
+ if shade_s:
+ factor = int(shade_s.strip()) / 100
+ if not 0 <= factor <= 2:
+ raise ValueError(f"{roles_path}: role {role!r} shade out of 0-200")
+ r, g, b = (min(255, round(c * factor)) for c in (r, g, b))
+ resolved[role] = (r, g, b, alpha)
+ return resolved
+
+
+def hex6(c):
+ r, g, b, _ = c
+ return f"#{r:02x}{g:02x}{b:02x}"
+
+
+def hex8(c):
+ r, g, b, a = c
+ return f"#{r:02x}{g:02x}{b:02x}{round(a * 255 / 100):02x}"
+
+
+def rgba(c):
+ r, g, b, a = c
+ return f"rgb({r},{g},{b})" if a == 100 else f"rgba({r},{g},{b},{a / 100:.2f})"
+
+
+BANNER = "Generated by udt-palette from palette/{scheme}.conf. Do not edit."
+
+
+def gen_rofi(res, scheme, palette):
+ """rofi: the structural palette, under the scheme's own colour names.
+
+ Emits palette names rather than roles because the rofi themes were written
+ against Catppuccin names and still use them. The role layer reaches rofi
+ through accent.rasi, which udt-accent generates per wallpaper.
+ """
+ rows = "\n".join(f" {k + ':':11}{v}ff;" for k, v in sorted(palette.items()))
+ return (
+ f"/*\n * {BANNER.format(scheme=scheme)}\n *\n"
+ " * Structural colors only. The accent lives in accent.rasi.\n */\n\n"
+ f"* {{\n{rows}\n}}\n"
+ )
+
+
+def gen_waybar(res, scheme, palette):
+ """waybar: one file holding the palette and the role layer.
+
+ Both halves together because style.css imports a single themes/<scheme>.css,
+ and the stylesheets reference names from each: @lavender and @surface1 from
+ the palette, @cpu and @hover-bg from the roles.
+
+ Role names keep their existing hyphenated spelling (main-bg, not main_bg):
+ the stylesheets reference them and are not generated.
+ """
+ palette_rows = "\n".join(f"@define-color {k:<12}{v};"
+ for k, v in sorted(palette.items()))
+
+ def emit(role, css_name=None):
+ c = res[role]
+ return f"@define-color {(css_name or role.replace('_', '-')):<12}{rgba(c)};"
+
+ ui = ["main_br", "main_bg", "main_fg", "hover_bg", "hover_fg", "outline"]
+ modules = ["workspaces", "temperature", "memory", "cpu", "time", "date",
+ "tray", "volume", "backlight", "battery"]
+ states = ["warning", "critical", "charging"]
+
+ return "\n".join([
+ f"/* {BANNER.format(scheme=scheme)} */",
+ "",
+ palette_rows,
+ "",
+ "/* br - border, bg - background, fg - foreground */",
+ "",
+ "/* main colors */",
+ f"@define-color accent {rgba(res['accent'])};",
+ *(emit(r) for r in ui),
+ "",
+ "/* module colors */",
+ *(emit(r) for r in modules),
+ "",
+ "/* state colors */",
+ *(emit(r) for r in states),
+ "",
+ ])
+
+
+def gen_kitty(res, scheme, palette):
+ """kitty: the 16 ANSI slots plus chrome.
+
+ kitty has no include-with-override, so this is the whole colour section of
+ the theme file rather than a fragment.
+ """
+ ansi = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]
+ lines = [f"# {BANNER.format(scheme=scheme)}", ""]
+ for key, role in [("foreground", "fg"), ("background", "bg"),
+ ("selection_foreground", "selection_fg"),
+ ("selection_background", "selection_bg"),
+ ("cursor", "cursor"), ("cursor_text_color", "bg"),
+ ("url_color", "url"),
+ ("active_border_color", "border_active"),
+ ("inactive_border_color", "border_inactive"),
+ ("active_tab_foreground", "tab_active_fg"),
+ ("active_tab_background", "tab_active_bg"),
+ ("inactive_tab_foreground", "tab_inactive_fg"),
+ ("inactive_tab_background", "tab_inactive_bg"),
+ ("tab_bar_background", "tab_bar_bg"),
+ ("scrollbar_handle_color", "scrollbar_handle"),
+ ("scrollbar_track_color", "scrollbar_track"),
+ ("bell_border_color", "bell_border"),
+ ("mark1_foreground", "bg"), ("mark1_background", "mark1"),
+ ("mark2_foreground", "bg"), ("mark2_background", "mark2"),
+ ("mark3_foreground", "bg"), ("mark3_background", "mark3")]:
+ lines.append(f"{key:<24}{hex6(res[role])}")
+ lines.append("")
+ for i, name in enumerate(ansi):
+ lines.append(f"color{i:<3}{' ' * 16}{hex6(res[name])}")
+ for i, name in enumerate(ansi):
+ lines.append(f"color{i + 8:<3}{' ' * 16}{hex6(res['bright_' + name])}")
+ return "\n".join(lines) + "\n"
+
+
+def gen_dunst(res, scheme, palette, template):
+ """dunst: substitute colours into the shipped template.
+
+ Kept as a template rather than fully generated: dunstrc is mostly geometry
+ and behaviour that has nothing to do with colour. @ACCENT@ is left alone,
+ because udt-accent substitutes it per wallpaper after pywal renders it.
+ """
+ out = template.replace("@SCHEME@", scheme)
+ for role in ("bg", "bg_alt", "fg", "fg_dim", "border", "critical"):
+ out = out.replace(f"@{role.upper()}@", hex6(res[role]))
+ return out
+
+
+def gen_conky(res, scheme, palette, template):
+ """conky: substitute into the shipped template.
+
+ conky.text is laid out with absolute ${goto} offsets tuned to label widths,
+ so it is never regenerated, only its colour block is substituted.
+ """
+ out = template.replace("@SCHEME@", scheme)
+ for role in ("heading", "label", "rule", "value", "highlight", "ok",
+ "body", "body_outline", "body_shade"):
+ out = out.replace(f"@{role.upper()}@", hex6(res[role]))
+ return out
+
+
+def gen_accent_py(res, scheme, palette, snap_names):
+ """The accent table and Macchiato dict udt-accent carries for Firefox.
+
+ Written as a Python fragment that udt-accent imports, so the accent snapping
+ and the pywalfox palette both follow the scheme instead of hardcoding one.
+ """
+ # The snap candidates are declared per scheme, not derived: which hues make
+ # good accents is a judgement about the palette (drop near-neutrals, drop
+ # near-duplicate hues) that the colour values alone do not carry.
+ candidates = {n: palette[n] for n in snap_names}
+ rows = "\n".join(f' {n!r}: {v!r},' for n, v in candidates.items())
+
+ ansi = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]
+ normal = ", ".join(f'"{hex6(res[n])}"' for n in ansi)
+ bright = ", ".join(f'"{hex6(res["bright_" + n])}"' for n in ansi)
+
+ return (
+ f"# {BANNER.format(scheme=scheme)}\n"
+ '"""Generated colour tables. See palette/roles.conf."""\n\n'
+ f"SCHEME = {scheme!r}\n\n"
+ "# The candidate accents udt-accent snaps a wallpaper to.\n"
+ f"ACCENTS = {{\n{rows}\n}}\n\n"
+ f"FALLBACK = {accent_name(res, palette, candidates)!r}\n\n"
+ "# Firefox, via pywalfox, which reads colors.json and nothing else.\n"
+ "PALETTE = {\n"
+ f' "background": "{hex6(res["bg"])}",\n'
+ f' "foreground": "{hex6(res["fg"])}",\n'
+ f' "cursor": "{hex6(res["cursor"])}",\n'
+ f" \"colors\": [{normal},\n"
+ f" {bright}],\n"
+ "}\n"
+ )
+
+
+def accent_name(res, palette, candidates):
+ """Which candidate udt-accent falls back to when a wallpaper is too grey.
+
+ The accent role's own colour, so the fallback matches what every consumer
+ that does not track the wallpaper is already sitting on.
+ """
+ target = hex6(res["accent"])
+ for name, hexval in candidates.items():
+ if hexval == target:
+ return name
+ return next(iter(candidates))
+
+
+def schemes():
+ """Every scheme that ships both a palette and a role map."""
+ return sorted(p.stem for p in PALETTE_DIR.glob("*.conf")
+ if p.stem != "roles" and not p.stem.startswith("roles-"))
+
+
+def load(selector_path, scheme=None):
+ """Read the selected scheme's palette and role map.
+
+ The selector names a scheme; the scheme names two files. Keeping the role
+ map per-scheme is what lets a palette use its own colour names: Nord has no
+ `base` and Dracula no `surface0`, so one shared map could not satisfy both.
+ """
+ if scheme is None:
+ scheme = parse_conf(selector_path).get("scheme")
+ if not scheme:
+ raise ValueError(f"{selector_path}: no 'scheme' line")
+
+ palette_path = PALETTE_DIR / f"{scheme}.conf"
+ roles_path = PALETTE_DIR / f"roles-{scheme}.conf"
+ for needed in (palette_path, roles_path):
+ if not needed.exists():
+ raise ValueError(
+ f"scheme {scheme!r} is missing {needed.name}. "
+ f"Available: {', '.join(schemes())}")
+
+ roles = parse_conf(roles_path)
+ palette = parse_conf(palette_path)
+ for name, value in palette.items():
+ if not re.fullmatch(r"#[0-9a-fA-F]{6}", value):
+ raise ValueError(f"{palette_path}: {name} = {value!r} is not #rrggbb")
+ snap_names = roles.get("snap", "").split()
+ if not snap_names:
+ raise ValueError(f"{roles_path}: no [accents] snap list")
+ for name in snap_names:
+ if name not in palette:
+ raise ValueError(
+ f"{roles_path}: snap candidate {name!r} is not in the {scheme} palette")
+
+ roles["scheme"] = scheme
+ return scheme, palette, resolve(roles, palette, roles_path), snap_names
+
+
+# What gets written where. Templates are read from the repo, everything else is
+# generated whole.
+TARGETS = [
+ ("rofi/udt/palette.rasi", gen_rofi, None),
+ ("templates/waybar/theme.css", gen_waybar, None),
+ ("templates/terminal/kitty-theme.conf", gen_kitty, None),
+ ("templates/dunstrc", gen_dunst, "templates/dunstrc.in"),
+ ("templates/conky.conf", gen_conky, "templates/conky.conf.in"),
+ ("bin/udt_colors.py", gen_accent_py, None),
+]
+
+
+def generate(roles_path, out_dir):
+ scheme, palette, res, snap_names = load(roles_path)
+ written = []
+ for target, fn, template in TARGETS:
+ dest = out_dir / target
+ if template:
+ src = REPO / template
+ if not src.exists():
+ raise ValueError(f"missing template {src}")
+ content = fn(res, scheme, palette, src.read_text())
+ elif fn is gen_accent_py:
+ content = fn(res, scheme, palette, snap_names)
+ else:
+ content = fn(res, scheme, palette)
+ dest.parent.mkdir(parents=True, exist_ok=True)
+ dest.write_text(content)
+ written.append(target)
+ return scheme, written
+
+
+def selftest():
+ # Every shipped scheme must satisfy every role, or switching to it breaks
+ # at install time. load() raises on any role the palette cannot supply.
+ names = schemes()
+ assert names, "no palettes found"
+
+ seen = {}
+ for scheme in names:
+ _, _, res, snap = load(None, scheme=scheme)
+ assert res["bg"] != res["fg"], f"{scheme}: bg and fg are the same colour"
+ assert len(snap) >= 5, f"{scheme}: only {len(snap)} snap candidates"
+ seen[scheme] = set(res)
+ print(f" {scheme}: {len(res)} roles, {len(snap)} accents")
+
+ # Every scheme must define the SAME roles: a generator asks for a role by
+ # name, so one scheme missing it would fail only once that scheme was
+ # selected, which is exactly the late failure this check exists to prevent.
+ reference = seen[names[0]]
+ for scheme, roles in seen.items():
+ missing = reference - roles
+ extra = roles - reference
+ assert not missing, f"{scheme} is missing roles: {sorted(missing)}"
+ assert not extra, f"{scheme} has roles no other scheme has: {sorted(extra)}"
+
+ # Alpha survives the round trip into each syntax.
+ assert hex8((202, 211, 245, 75)) == "#cad3f5bf", hex8((202, 211, 245, 75))
+ assert hex8((202, 211, 245, 100)) == "#cad3f5ff"
+ assert rgba((202, 211, 245, 100)) == "rgb(202,211,245)"
+ assert rgba((202, 211, 245, 75)) == "rgba(202,211,245,0.75)"
+
+ # A role naming a colour the palette lacks must fail, not emit a hole.
+ try:
+ resolve({"scheme": "x", "bad": "nosuchcolour"}, {"base": "#000000"}, "probe")
+ except ValueError as exc:
+ assert "nosuchcolour" in str(exc)
+ else:
+ raise AssertionError("unknown colour did not raise")
+
+ print("selftest OK")
+
+
+def main(argv):
+ if "--selftest" in argv:
+ selftest()
+ return 0
+
+ roles_path = PALETTE_DIR / "roles.conf"
+ out_dir = REPO
+ if "--roles" in argv:
+ roles_path = Path(argv[argv.index("--roles") + 1]).expanduser()
+ if "--out" in argv:
+ out_dir = Path(argv[argv.index("--out") + 1]).expanduser()
+
+ try:
+ scheme, written = generate(roles_path, out_dir)
+ except ValueError as exc:
+ print(f"udt-palette: {exc}", file=sys.stderr)
+ return 1
+
+ print(f"{scheme}: {len(written)} files")
+ for w in written:
+ print(f" {w}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
diff --git a/install.sh b/install.sh
index 1b51947..5489f46 100755
--- a/install.sh
+++ b/install.sh
@@ -9,6 +9,12 @@ target="$HOME/.config/rofi/udt"
mkdir -p "$target" "$HOME/bin" "$HOME/.cache/wal"
+# Generate every themed config from the selected scheme first: the rofi palette
+# symlinked below is one of its outputs, so this has to run before anything is
+# linked. It fails loudly if a role names a colour the scheme does not define,
+# which is the whole point of keeping the colours in one place.
+"$repo/bin/udt-palette"
+
# Theme files are symlinked individually; accent.rasi is NOT, it points at the
# generated file in the wal cache instead.
for f in palette.rasi common.rasi menu.rasi list.rasi grid.rasi powermenu.rasi confirm.rasi; do
@@ -30,6 +36,25 @@ fi
ln -sfn "$HOME/.cache/wal/udt-accent.rasi" "$target/accent.rasi"
ln -sfn "$repo/bin/udt-accent" "$HOME/bin/udt-accent"
+ln -sfn "$repo/bin/udt-palette" "$HOME/bin/udt-palette"
+
+# The generated configs for everything that is not rofi. These are copied
+# rather than symlinked: each app owns its config file and mixes theme keys
+# with settings that are none of this repo's business.
+scheme="$(sed -n 's/^scheme *= *//p' "$repo/palette/roles.conf")"
+
+# waybar reads whichever themes/<name>.css style.css imports, so write the file
+# under the scheme's own name and repoint the import. The older per-flavour
+# files in themes/ are left alone; they are simply no longer imported.
+install -Dm644 "$repo/templates/waybar/theme.css" \
+ "$HOME/.config/waybar/themes/$scheme.css"
+if [ -f "$HOME/.config/waybar/style.css" ]; then
+ sed -i "s|@import \"themes/.*\.css\";|@import \"themes/$scheme.css\";|" \
+ "$HOME/.config/waybar/style.css"
+fi
+
+install -Dm644 "$repo/templates/terminal/kitty-theme.conf" \
+ "$HOME/.config/kitty/current-theme.conf"
echo "installed:"
ls -l "$target" "$HOME/bin/udt-accent"
diff --git a/palette/dracula.conf b/palette/dracula.conf
new file mode 100644
index 0000000..c1b41fb
--- /dev/null
+++ b/palette/dracula.conf
@@ -0,0 +1,29 @@
+# Dracula.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Source: https://draculatheme.com/contribute
+#
+# Dracula ships six accents and three greys, fewer than Catppuccin's ten and
+# nine. The two extra surfaces below are not upstream: they are interpolated
+# between background and current_line so roles that want a third depth have
+# one. Everything else is the published palette.
+
+background = #282a36
+current_line = #44475a
+selection = #44475a
+foreground = #f8f8f2
+comment = #6272a4
+
+cyan = #8be9fd
+green = #50fa7b
+orange = #ffb86c
+pink = #ff79c6
+purple = #bd93f9
+red = #ff5555
+yellow = #f1fa8c
+
+# Interpolated, not upstream. See the note above.
+surface_low = #21222c
+surface_mid = #343746
+surface_high = #565872
diff --git a/palette/macchiato.conf b/palette/macchiato.conf
new file mode 100644
index 0000000..8ba796f
--- /dev/null
+++ b/palette/macchiato.conf
@@ -0,0 +1,40 @@
+# Catppuccin Macchiato.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Source: https://github.com/catppuccin/palette
+#
+# Colour definitions only, in the scheme's own names. Nothing here says what a
+# colour is used for; roles.conf does that. Adding a scheme means adding a file
+# like this one, not editing any consumer.
+
+rosewater = #f4dbd6
+flamingo = #f0c6c6
+pink = #f5bde6
+mauve = #c6a0f6
+red = #ed8796
+maroon = #ee99a0
+peach = #f5a97f
+yellow = #eed49f
+green = #a6da95
+teal = #8bd5ca
+sky = #91d7e3
+sapphire = #7dc4e4
+blue = #8aadf4
+lavender = #b7bdf8
+
+text = #cad3f5
+subtext1 = #b8c0e0
+subtext0 = #a5adcb
+
+overlay2 = #939ab7
+overlay1 = #8087a2
+overlay0 = #6e738d
+
+surface2 = #5b6078
+surface1 = #494d64
+surface0 = #363a4f
+
+base = #24273a
+mantle = #1e2030
+crust = #181926
diff --git a/palette/nord.conf b/palette/nord.conf
new file mode 100644
index 0000000..a3454b7
--- /dev/null
+++ b/palette/nord.conf
@@ -0,0 +1,41 @@
+# Nord.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Source: https://www.nordtheme.com/docs/colors-and-palettes
+#
+# Nord names its colours nord0..nord15 and nothing else, so that is what this
+# file uses. The aurora and frost groupings below are Nord's own; the readable
+# aliases are a convenience for roles.conf and are the same colours.
+
+nord0 = #2e3440
+nord1 = #3b4252
+nord2 = #434c5e
+nord3 = #4c566a
+
+nord4 = #d8dee9
+nord5 = #e5e9f0
+nord6 = #eceff4
+
+nord7 = #8fbcbb
+nord8 = #88c0d0
+nord9 = #81a1c1
+nord10 = #5e81ac
+
+nord11 = #bf616a
+nord12 = #d08770
+nord13 = #ebcb8b
+nord14 = #a3be8c
+nord15 = #b48ead
+
+# Aliases, same values. Nord has no sixteenth hue, so the darker frost blue
+# stands in where a scheme with more accents would offer one.
+frost_teal = #8fbcbb
+frost_cyan = #88c0d0
+frost_blue = #81a1c1
+frost_deep = #5e81ac
+aurora_red = #bf616a
+aurora_orange = #d08770
+aurora_yellow = #ebcb8b
+aurora_green = #a3be8c
+aurora_purple = #b48ead
diff --git a/palette/roles-dracula.conf b/palette/roles-dracula.conf
new file mode 100644
index 0000000..40aa2a6
--- /dev/null
+++ b/palette/roles-dracula.conf
@@ -0,0 +1,101 @@
+# Role assignments for Dracula.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Dracula ships three greys where Catppuccin has nine, so this leans on the
+# interpolated surfaces in palette/dracula.conf. It has no distinct bright
+# variants, so bright_* name the same colours as normal.
+
+[ui]
+bg = background
+bg_alt = surface_low
+bg_deep = surface_low
+surface = surface_mid
+surface_alt = current_line
+border = current_line
+
+fg = foreground
+fg_dim = comment
+fg_faint = surface_high
+
+accent = purple
+
+[state]
+warning = yellow
+critical = red
+success = green
+info = cyan
+
+[waybar]
+main_br = comment
+main_bg = surface_low
+main_fg = foreground
+hover_bg = background
+hover_fg = foreground/75
+outline = surface_low*50
+
+workspaces = surface_low
+temperature = surface_low
+memory = background
+cpu = surface_mid
+time = surface_mid
+date = background
+tray = surface_low
+volume = surface_low
+backlight = background
+battery = surface_mid
+charging = green
+
+[conky]
+heading = purple
+label = comment
+rule = current_line
+value = cyan
+highlight = pink
+ok = green
+body = foreground
+body_outline = current_line
+body_shade = surface_low
+
+[terminal]
+cursor = foreground
+selection_bg = selection
+selection_fg = foreground
+url = cyan
+tab_active_bg = purple
+tab_active_fg = background
+tab_inactive_bg = surface_low
+tab_inactive_fg = foreground
+border_active = purple
+border_inactive = comment
+
+black = current_line
+red = red
+green = green
+yellow = yellow
+blue = purple
+magenta = pink
+cyan = cyan
+white = foreground
+
+bright_black = surface_high
+bright_red = red
+bright_green = green
+bright_yellow = orange
+bright_blue = purple
+bright_magenta = pink
+bright_cyan = cyan
+bright_white = foreground
+
+# kitty extras: marks, scrollbar, bell, tab bar ground.
+mark1 = purple
+mark2 = pink
+mark3 = cyan
+scrollbar_handle = comment
+scrollbar_track = current_line
+bell_border = yellow
+tab_bar_bg = surface_low
+
+[accents]
+# Dracula ships six accents.
+snap = pink purple red orange yellow green cyan
diff --git a/palette/roles-macchiato.conf b/palette/roles-macchiato.conf
new file mode 100644
index 0000000..0a34f03
--- /dev/null
+++ b/palette/roles-macchiato.conf
@@ -0,0 +1,115 @@
+# Role assignments for Catppuccin Macchiato.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Every role in here names a colour from palette/macchiato.conf. See
+# palette/roles.conf for what the roles mean and how to select a scheme.
+
+[ui]
+# Surfaces, darkest to lightest. Most consumers only need the first three.
+bg = base
+bg_alt = mantle
+bg_deep = crust
+surface = surface0
+surface_alt = surface1
+border = surface1
+
+# Text, brightest to dimmest.
+fg = text
+fg_dim = subtext0
+fg_faint = overlay0
+
+# The one colour that tracks the wallpaper. udt-accent overrides this per
+# wallpaper for rofi, dunst, Hyprland and Firefox; the value here is the
+# fallback, and what the consumers that do not track it sit on permanently.
+accent = lavender
+
+[state]
+# Notification urgency, waybar module states, conky warnings.
+warning = yellow
+critical = red
+success = green
+info = teal
+
+[waybar]
+# Bar chrome. These were already role names in ~/.config/waybar/theme.css.
+main_br = subtext0
+main_bg = crust
+main_fg = text
+hover_bg = base
+hover_fg = text/75
+outline = crust*50
+
+# Per-module backgrounds. Alternating depths, not semantic: the bar reads as
+# stripes, so neighbouring modules differ.
+workspaces = mantle
+temperature = mantle
+memory = base
+cpu = surface0
+time = surface0
+date = base
+tray = mantle
+volume = mantle
+backlight = base
+battery = surface0
+charging = green
+
+[conky]
+# conky indexes colours by number. The names say what each number draws.
+heading = lavender
+label = subtext0
+rule = surface1
+value = teal
+highlight = mauve
+ok = green
+body = text
+body_outline = surface1
+body_shade = mantle
+
+[terminal]
+# The 16 ANSI slots, plus kitty's own chrome. normal and bright are separate
+# because a scheme may offer distinct pairs; Catppuccin and Dracula do not, so
+# these name the same colours twice.
+cursor = rosewater
+selection_bg = rosewater
+selection_fg = base
+url = rosewater
+tab_active_bg = mauve
+tab_active_fg = crust
+tab_inactive_bg = mantle
+tab_inactive_fg = text
+border_active = lavender
+border_inactive = overlay0
+
+black = surface1
+red = red
+green = green
+yellow = yellow
+blue = blue
+magenta = pink
+cyan = teal
+white = subtext1
+
+bright_black = surface2
+bright_red = red
+bright_green = green
+bright_yellow = yellow
+bright_blue = blue
+bright_magenta = pink
+bright_cyan = teal
+bright_white = subtext0
+
+# kitty extras: marks, scrollbar, bell, tab bar ground.
+mark1 = lavender
+mark2 = mauve
+mark3 = sapphire
+scrollbar_handle = overlay2
+scrollbar_track = surface1
+bell_border = yellow
+tab_bar_bg = crust
+
+[accents]
+# The hues udt-accent snaps a wallpaper to. Near-neutral tints (rosewater,
+# flamingo) and near-duplicate hues (maroon, sapphire) are left out so the
+# snapping has one candidate per region of the wheel.
+snap = pink mauve lavender red peach yellow green teal sky blue
diff --git a/palette/roles-nord.conf b/palette/roles-nord.conf
new file mode 100644
index 0000000..b04e9c8
--- /dev/null
+++ b/palette/roles-nord.conf
@@ -0,0 +1,101 @@
+# Role assignments for Nord.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Nord's Polar Night (nord0..nord3) is the only dark group, so bg_deep reuses
+# nord0 rather than inventing a darker shade. Nord has no pink or magenta, so
+# the aurora purple fills both.
+
+[ui]
+bg = nord0
+bg_alt = nord1
+bg_deep = nord0
+surface = nord1
+surface_alt = nord2
+border = nord3
+
+fg = nord4
+fg_dim = nord4
+fg_faint = nord3
+
+accent = frost_cyan
+
+[state]
+warning = aurora_yellow
+critical = aurora_red
+success = aurora_green
+info = frost_teal
+
+[waybar]
+main_br = nord4
+main_bg = nord0
+main_fg = nord6
+hover_bg = nord1
+hover_fg = nord6/75
+outline = nord0*50
+
+workspaces = nord1
+temperature = nord1
+memory = nord0
+cpu = nord2
+time = nord2
+date = nord0
+tray = nord1
+volume = nord1
+backlight = nord0
+battery = nord2
+charging = aurora_green
+
+[conky]
+heading = frost_cyan
+label = nord4
+rule = nord3
+value = frost_teal
+highlight = aurora_purple
+ok = aurora_green
+body = nord6
+body_outline = nord3
+body_shade = nord1
+
+[terminal]
+cursor = nord4
+selection_bg = nord2
+selection_fg = nord6
+url = frost_cyan
+tab_active_bg = aurora_purple
+tab_active_fg = nord0
+tab_inactive_bg = nord1
+tab_inactive_fg = nord4
+border_active = frost_cyan
+border_inactive = nord3
+
+black = nord1
+red = aurora_red
+green = aurora_green
+yellow = aurora_yellow
+blue = frost_blue
+magenta = aurora_purple
+cyan = frost_teal
+white = nord5
+
+bright_black = nord3
+bright_red = aurora_red
+bright_green = aurora_green
+bright_yellow = aurora_orange
+bright_blue = frost_cyan
+bright_magenta = aurora_purple
+bright_cyan = frost_teal
+bright_white = nord6
+
+# kitty extras: marks, scrollbar, bell, tab bar ground.
+mark1 = frost_cyan
+mark2 = aurora_purple
+mark3 = frost_teal
+scrollbar_handle = nord3
+scrollbar_track = nord2
+bell_border = aurora_yellow
+tab_bar_bg = nord0
+
+[accents]
+# Nord has nine usable hues, one fewer than Catppuccin.
+snap = aurora_purple frost_blue aurora_red aurora_orange aurora_yellow aurora_green frost_teal frost_cyan frost_deep
diff --git a/palette/roles-tokyo-night.conf b/palette/roles-tokyo-night.conf
new file mode 100644
index 0000000..fb54926
--- /dev/null
+++ b/palette/roles-tokyo-night.conf
@@ -0,0 +1,99 @@
+# Role assignments for Tokyo Night.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Tokyo Night carries fewer surface depths than Catppuccin, so bg_highlight
+# does double duty as both surfaces. Its comment colour is the dim foreground.
+
+[ui]
+bg = bg
+bg_alt = bg_dark
+bg_deep = bg_float
+surface = bg_highlight
+surface_alt = bg_visual
+border = fg_gutter
+
+fg = fg
+fg_dim = fg_dark
+fg_faint = comment
+
+accent = blue
+
+[state]
+warning = yellow
+critical = red
+success = green
+info = cyan
+
+[waybar]
+main_br = fg_dark
+main_bg = bg_dark
+main_fg = fg
+hover_bg = bg
+hover_fg = fg/75
+outline = bg_dark*50
+
+workspaces = bg_dark
+temperature = bg_dark
+memory = bg
+cpu = bg_highlight
+time = bg_highlight
+date = bg
+tray = bg_dark
+volume = bg_dark
+backlight = bg
+battery = bg_highlight
+charging = green
+
+[conky]
+heading = blue
+label = fg_dark
+rule = fg_gutter
+value = cyan
+highlight = magenta
+ok = green
+body = fg
+body_outline = fg_gutter
+body_shade = bg_dark
+
+[terminal]
+cursor = orange
+selection_bg = bg_visual
+selection_fg = fg
+url = cyan
+tab_active_bg = magenta
+tab_active_fg = bg_dark
+tab_inactive_bg = bg_dark
+tab_inactive_fg = fg
+border_active = blue
+border_inactive = comment
+
+black = terminal_black
+red = red
+green = green
+yellow = yellow
+blue = blue
+magenta = magenta
+cyan = cyan
+white = fg_dark
+
+bright_black = comment
+bright_red = red1
+bright_green = green1
+bright_yellow = orange
+bright_blue = blue5
+bright_magenta = purple
+bright_cyan = blue1
+bright_white = fg
+
+# kitty extras: marks, scrollbar, bell, tab bar ground.
+mark1 = blue
+mark2 = magenta
+mark3 = cyan
+scrollbar_handle = comment
+scrollbar_track = fg_gutter
+bell_border = yellow
+tab_bar_bg = bg_dark
+
+[accents]
+snap = magenta purple blue red orange yellow green teal cyan blue1
diff --git a/palette/roles.conf b/palette/roles.conf
new file mode 100644
index 0000000..3b789b8
--- /dev/null
+++ b/palette/roles.conf
@@ -0,0 +1,14 @@
+# Scheme selection.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# This is the one line to change to retheme the desktop. It picks a pair of
+# files: palette/<scheme>.conf, which holds the colours under the scheme's own
+# names, and palette/roles-<scheme>.conf, which says what each colour is for.
+#
+# Run ./install.sh afterwards. Every themed config is generated from that pair,
+# so nothing else needs editing.
+#
+# Available: macchiato, tokyo-night, nord, dracula
+
+scheme = macchiato
diff --git a/palette/tokyo-night.conf b/palette/tokyo-night.conf
new file mode 100644
index 0000000..eb1b0b6
--- /dev/null
+++ b/palette/tokyo-night.conf
@@ -0,0 +1,43 @@
+# Tokyo Night.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# Source: https://github.com/folke/tokyonight.nvim (the "night" variant)
+#
+# Tokyo Night names colours by hue rather than by role, and carries three
+# background depths plus a distinct float/sidebar shade. The names below are
+# upstream's own.
+
+bg = #1a1b26
+bg_dark = #16161e
+bg_highlight = #292e42
+bg_float = #16161e
+bg_visual = #283457
+
+fg = #c0caf5
+fg_dark = #a9b1d6
+fg_gutter = #3b4261
+
+comment = #565f89
+terminal_black = #414868
+
+blue = #7aa2f7
+blue0 = #3d59a5
+blue1 = #2ac3de
+blue2 = #0db9d7
+blue5 = #89ddff
+blue6 = #b4f9f8
+blue7 = #394b70
+
+cyan = #7dcfff
+magenta = #bb9af7
+magenta2 = #ff007c
+purple = #9d7cd8
+orange = #ff9e64
+yellow = #e0af68
+green = #9ece6a
+green1 = #73daca
+green2 = #41a6b5
+teal = #1abc9c
+red = #f7768e
+red1 = #db4b4b
diff --git a/rofi/udt/palette.rasi b/rofi/udt/palette.rasi
deleted file mode 100644
index 4a95fea..0000000
--- a/rofi/udt/palette.rasi
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Catppuccin Macchiato palette for rofi.
- * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
- * Licensed under the GNU General Public License v2 only.
- *
- * Source: https://github.com/catppuccin/palette
- * Structural colors only. The accent lives in accent.rasi and is generated.
- */
-
-* {
- base: #24273aff;
- mantle: #1e2030ff;
- crust: #181926ff;
-
- text: #cad3f5ff;
- subtext1: #b8c0e0ff;
- subtext0: #a5adcbff;
-
- overlay2: #939ab7ff;
- overlay1: #8087a2ff;
- overlay0: #6e738dff;
-
- surface2: #5b6078ff;
- surface1: #494d64ff;
- surface0: #363a4fff;
-
- rosewater: #f4dbd6ff;
- flamingo: #f0c6c6ff;
- pink: #f5bde6ff;
- mauve: #c6a0f6ff;
- red: #ed8796ff;
- maroon: #ee99a0ff;
- peach: #f5a97fff;
- yellow: #eed49fff;
- green: #a6da95ff;
- teal: #8bd5caff;
- sky: #91d7e3ff;
- sapphire: #7dc4e4ff;
- blue: #8aadf4ff;
- lavender: #b7bdf8ff;
-}
diff --git a/templates/conky.conf.in b/templates/conky.conf.in
new file mode 100644
index 0000000..92ad989
--- /dev/null
+++ b/templates/conky.conf.in
@@ -0,0 +1,102 @@
+conky.config={
+ out_to_x = false,
+ out_to_wayland = true,
+ alignment='tr',
+ mpd_host = '172.16.34.5',
+ mpd_port = 6600,
+ background=true,
+ color1='@HEADING@',
+ color2='@LABEL@',
+ color3='@RULE@',
+ color4='@VALUE@',
+ color5='@HIGHLIGHT@',
+ color6='@OK@',
+ pad_percents=0,
+ cpu_avg_samples=4,
+ default_color='@BODY@',
+ default_outline_color='@BODY_OUTLINE@',
+ default_shade_color='@BODY_SHADE@',
+ default_bar_height=10,
+ default_bar_width=500,
+ double_buffer=true,
+ draw_borders=false,
+ draw_graph_borders=false,
+ draw_outline=false,
+ draw_shades=false,
+ font='Inconsolata Nerd Font:style=Regular:size=10',
+ use_spacer=none,
+ format_human_readable=true,
+ short_units=yes,
+ gap_x=1,
+ gap_y=0,
+ minimum_height=1030,
+ minimum_width=600,
+ net_avg_samples=2,
+ no_buffers=true,
+ override_utf8_locale=true,
+ own_window=true,
+ own_window_type='desktop',
+ own_window_argb_value=225,
+ own_window_class='Conky',
+ text_buffer_size=2048,
+ total_run_times=0,
+ update_interval=3,
+ uppercase=false,
+ use_xft=true,
+ xftalpha=0,
+}
+
+conky.text=[[
+${color1} 󰻠 ${goto 20}Queen Anne's Revenge ${goto 250}${color5}󰓅 ${cpugovernor cpu0}${alignr}${color}${cpu cpu0}%
+${alignr}${color4}TCTL: ${color}${execi 5 cat $(grep -l '^k10temp$' /sys/class/hwmon/hwmon*/name | head -1 | xargs dirname)/temp1_input | awk '{printf "%.0f", $1/1000}'}°c ${color3}| ${color4}TCCD1: ${color}${execi 5 sensors k10temp-pci-00c3 | grep 'Tccd1' | awk '{print $2}' | tr -d '+°C'}°c
+${alignr}${color4}NVME: ${color}${execi 5 cat $(grep -l '^nvme$' /sys/class/hwmon/hwmon*/name | head -1 | xargs dirname)/temp1_input | awk '{printf "%.0f", $1/1000}'}°c
+${goto 50}${cpugraph cpu0 74,500 5e7b7b d8deeb -t}
+${goto 30}${color}\#1 ${cpubar cpu1 5,100} ${goto 165}\#5 ${cpubar cpu5 5,100} ${goto 300}\#9 ${cpubar cpu9 5,100} ${goto 440}\#13 ${cpubar cpu13 5,100}
+${goto 30}\#2 ${cpubar cpu2 5,100} ${goto 165}\#6 ${cpubar cpu6 5,100} ${goto 300}\#10 ${cpubar cpu10 5,100} ${goto 440}\#14 ${cpubar cpu14 5,100}
+${goto 30}\#3 ${cpubar cpu3 5,100} ${goto 165}\#7 ${cpubar cpu7 5,100} ${goto 300}\#11 ${cpubar cpu11 5,100} ${goto 440}\#15 ${cpubar cpu15 5,100}
+${goto 30}\#4 ${cpubar cpu4 5,100} ${goto 165}\#8 ${cpubar cpu8 5,100} ${goto 300}\#12 ${cpubar cpu12 5,100} ${goto 440}\#16 ${cpubar cpu16 5,100}
+${color3}${hr}
+
+${color1}󰊲 ${goto 20}GPU ${color4}Arc B580${color}${alignr}${color4}pkg: ${color1}${execi 5 cat $(grep -l xe /sys/class/hwmon/hwmon*/name | head -1 | xargs dirname)/temp2_input | awk '{printf "%.0f", $1/1000}'}°c ${color4}vram: ${color}${execi 5 cat $(grep -l xe /sys/class/hwmon/hwmon*/name | head -1 | xargs dirname)/temp3_input | awk '{printf "%.0f", $1/1000}'}°c
+${alignr}${color4}fan: ${color}${execi 5 cat $(grep -l xe /sys/class/hwmon/hwmon*/name | head -1 | xargs dirname)/fan1_input} RPM
+${color1}󰘚 ${goto 20}MoBo ${color4}GigaByte X870 Eagle WiFi 7${color}${alignr}${color}${execi 5 cat $(grep -l '^gigabyte_wmi$' /sys/class/hwmon/hwmon*/name | head -1 | xargs dirname)/temp2_input | awk '{printf "%.0f", $1/1000}'}°c / ${execi 5 cat $(grep -l '^gigabyte_wmi$' /sys/class/hwmon/hwmon*/name | head -1 | xargs dirname)/temp3_input | awk '{printf "%.0f", $1/1000}'}°c
+${color3}${hr}
+
+${color1} ${goto 20}Disk
+${color4}${goto 50}FREE% | FREE / TOTAL ${color}
+${goto 18}${color5} ${color}${fs_free_perc /}% free ${color4}|${color} ${fs_free /} ${color4}/${color} ${fs_size /}${alignr}${fs_bar 10,250 /}
+${goto 18}${color6} ${color}${fs_free_perc /home}% free ${color4}|${color} ${fs_free /home} ${color4}/${color} ${fs_size /home}${alignr}${fs_bar 10,250 /home}
+${if_mounted /data}${goto 18}${color1}󰋊 ${color}${fs_free_perc /data}% free ${color4}|${color} ${fs_free /data} ${color4}/${color} ${fs_size /data}${alignr}${fs_bar 10,250 /data}${endif}
+${if_mounted /mnt/nfs/Slackware}${goto 18}${color1}󰛳 ${color}${fs_free_perc /mnt/nfs/Slackware}% free ${color4}|${color} ${fs_free /mnt/nfs/Slackware} ${color4}/${color} ${fs_size /mnt/nfs/Slackware}${alignr}${fs_bar 10,250 /mnt/nfs/Slackware}${endif}
+${diskiograph_read 50,245}${alignr}${diskiograph_write 50,245}
+${color1}${goto 120}${diskio_read}/s${goto 360}${diskio_write}/s
+${color4}${goto 119}read ${goto 360} write${color}
+${color1}󰃨 ${goto 20}Cache ${color5}${execpi 900 du -sh $HOME/.cache 2>/dev/null | cut -f1}${color}
+${color4}${goto 60}Folder Name${alignr 60}Size${color}
+${execpi 900 du -sh $HOME/.cache/* 2>/dev/null | sort -hr | head -4 | awk '{n=substr($2, index($2, ".cache/")+7); if (length(n)>26) n=substr(n,1,25) "…"; printf "${color6}${goto 60}%s${color1}${alignr 60}%s\n", n, $1}'}
+${color3}${hr}
+
+${color1}${font Inconsolata Nerd Font:style=Regular:pixelsize=13}󰒍 ${goto 20}Network
+󰌘 ${goto 20}${addr br0}${alignr}󰖟 ${color6}${execi 300 curl ipinfo.io/ip}${color}
+${downspeedgraph br0 50,245}${alignr}${upspeedgraph br0 50,245}
+${if_match "${addr br0}"!="No Address"}${goto 20}${color1}󰇚 ${downspeed br0}k/s${goto 350}󰕒 ${upspeed br0}k/s${endif}
+${color4}${goto 20} download ${goto 360} upload${color}
+${color3}${hr}
+
+${color4} ${color}${goto 20}${exec cat /etc/slackware-version}${alignr} 󰐥 ${uptime}
+󰌽 ${goto 20}${kernel}${alignr}󰏖 ${exec ls /var/log/packages|wc -l}
+󰍛 ${memperc}% / ${memmax}${alignr}${membar 10,250}${color}
+${color3}${hr}
+
+${color4}${goto 60}Process${alignr 60}PID %CPU %MEM${color}
+${color6}${goto 60}${top name 1}${color1}${alignr 60}${top pid 1} ${top cpu 1} ${top mem 1}
+${color6}${goto 60}${top name 2}${color1}${alignr 60}${top pid 2} ${top cpu 2} ${top mem 2}
+${color6}${goto 60}${top name 3}${color1}${alignr 60}${top pid 3} ${top cpu 3} ${top mem 3}
+${color6}${goto 60}${top name 4}${color1}${alignr 60}${top pid 4} ${top cpu 4} ${top mem 4}
+${color6}${goto 60}${top name 5}${color1}${alignr 60}${top pid 5} ${top cpu 5} ${top mem 5}
+${color3}${hr}
+
+${color1}󰝚 ${goto 20}${execi 2 playerctl status 2>/dev/null} · ${color4}${execi 2 playerctl metadata --format "{{artist}}"}${color}
+${goto 20}${execi 2 playerctl metadata --format "{{title}} ({{duration(position)}} / {{duration(mpris:length)}})" 2>/dev/null}
+${goto 50}${execibar 2 5,500 bash -c "pos=\$(playerctl position 2>/dev/null); len=\$(playerctl metadata mpris:length 2>/dev/null); echo \"scale=6; (\$pos / (\$len / 1000000)) * 100\" | bc | awk '{print int(\$1)}'"}
+]]
diff --git a/templates/dunstrc.in b/templates/dunstrc.in
new file mode 100644
index 0000000..f3a096e
--- /dev/null
+++ b/templates/dunstrc.in
@@ -0,0 +1,75 @@
+# See dunst(5) for all configuration options
+[global]
+ monitor = 1
+ follow = none
+ width = 300
+ height = (0, 300)
+ origin = top-right
+ offset = (10, 50)
+ scale = 0
+ notification_limit = 0
+ progress_bar = true
+ progress_bar_height = 10
+ progress_bar_frame_width = 1
+ progress_bar_min_width = 150
+ progress_bar_max_width = 300
+ indicate_hidden = yes
+ transparency = 15
+ separator_height = 2
+ padding = 8
+ horizontal_padding = 8
+ text_icon_padding = 0
+ frame_width = 3
+ frame_color = "@ACCENT@"
+ gap_size = 0
+ separator_color = frame
+ sort = yes
+ font = Noto Sans 11
+ line_height = 0
+ markup = full
+ format = "<b>%a</b>\n<i>%s</i>%p %n\n%b"
+ alignment = left
+ vertical_alignment = center
+ show_age_threshold = 60
+ ellipsize = middle
+ ignore_newline = no
+ stack_duplicates = true
+ hide_duplicate_count = false
+ show_indicators = yes
+ enable_recursive_icon_lookup = true
+ icon_theme = MB-Blueberry-Suru-GLOW
+ icon_position = left
+ icon_corner_radius = 5
+ min_icon_size = 32
+ max_icon_size = 128
+ icon_path = ~/.local/share/icons/MB-Blueberry-Suru-GLOW/status/48/:/usr/share/icons/MB-Blueberry-Suru-GLOW/devices/48/
+ sticky_history = yes
+ history_length = 20
+ dmenu = /usr/bin/rofi -theme ~/.config/rofi/udt/list.rasi -dmenu -p dunst
+ browser = /usr/bin/xdg-open
+ always_run_script = true
+ title = Dunst
+ class = Dunst
+ corner_radius = 10
+ ignore_dbusclose = false
+ mouse_left_click = context, close_current
+ mouse_middle_click = do_action, close_current
+ mouse_right_click = close_all
+# to have a consistent behaviour across releases.
+[experimental]
+ per_monitor_dpi = false
+[urgency_low]
+ background = "@BG_ALT@"
+ foreground = "@FG_DIM@"
+ frame_color = "@BORDER@"
+ timeout = 10
+[urgency_normal]
+ background = "@BG@"
+ foreground = "@FG@"
+ frame_color = "@ACCENT@"
+ timeout = 10
+[urgency_critical]
+ background = "@BG@"
+ foreground = "@FG@"
+ frame_color = "@CRITICAL@"
+ timeout = 0
diff --git a/templates/terminal/kitty-macchiato.conf b/templates/terminal/kitty-macchiato.conf
deleted file mode 100644
index 92fa1c0..0000000
--- a/templates/terminal/kitty-macchiato.conf
+++ /dev/null
@@ -1,84 +0,0 @@
-# vim:ft=kitty
-
-## name: Catppuccin Kitty Macchiato
-## author: Catppuccin Org
-## license: MIT
-## upstream: https://github.com/catppuccin/kitty/blob/main/themes/macchiato.conf
-## blurb: Soothing pastel theme for the high-spirited!
-
-
-
-# The basic colors
-foreground #cad3f5
-background #24273a
-selection_foreground #24273a
-selection_background #f4dbd6
-
-# Cursor colors
-cursor #f4dbd6
-cursor_text_color #24273a
-
-# Scrollbar colors
-scrollbar_handle_color #939ab7
-scrollbar_track_color #494d64
-
-# URL color when hovering with mouse
-url_color #f4dbd6
-
-# Kitty window border colors
-active_border_color #b7bdf8
-inactive_border_color #6e738d
-bell_border_color #eed49f
-
-# OS Window titlebar colors
-wayland_titlebar_color system
-macos_titlebar_color system
-
-# Tab bar colors
-active_tab_foreground #181926
-active_tab_background #c6a0f6
-inactive_tab_foreground #cad3f5
-inactive_tab_background #1e2030
-tab_bar_background #181926
-
-# Colors for marks (marked text in the terminal)
-mark1_foreground #24273a
-mark1_background #b7bdf8
-mark2_foreground #24273a
-mark2_background #c6a0f6
-mark3_foreground #24273a
-mark3_background #7dc4e4
-
-# The 16 terminal colors
-
-# black
-color0 #494d64
-color8 #5b6078
-
-# red
-color1 #ed8796
-color9 #ed8796
-
-# green
-color2 #a6da95
-color10 #a6da95
-
-# yellow
-color3 #eed49f
-color11 #eed49f
-
-# blue
-color4 #8aadf4
-color12 #8aadf4
-
-# magenta
-color5 #f5bde6
-color13 #f5bde6
-
-# cyan
-color6 #8bd5ca
-color14 #8bd5ca
-
-# white
-color7 #b8c0e0
-color15 #a5adcb
diff --git a/templates/waybar/catppuccin-macchiato.css b/templates/waybar/catppuccin-macchiato.css
deleted file mode 100644
index d98b1cc..0000000
--- a/templates/waybar/catppuccin-macchiato.css
+++ /dev/null
@@ -1,63 +0,0 @@
-/* catppuccin macchiato */
-
-@define-color rosewater #f4dbd6;
-@define-color flamingo #f0c6c6;
-@define-color pink #f5bde6;
-@define-color mauve #c6a0f6;
-@define-color red #ed8796;
-@define-color maroon #ee99a0;
-@define-color peach #f5a97f;
-@define-color yellow #eed49f;
-@define-color green #a6da95;
-@define-color teal #8bd5ca;
-@define-color sky #91d7e3;
-@define-color sapphire #7dc4e4;
-@define-color blue #8aadf4;
-@define-color lavender #b7bdf8;
-@define-color text #cad3f5;
-@define-color subtext1 #b8c0e0;
-@define-color subtext0 #a5adcb;
-@define-color overlay2 #939ab7;
-@define-color overlay1 #8087a2;
-@define-color overlay0 #6e738d;
-@define-color surface2 #5b6078;
-@define-color surface1 #494d64;
-@define-color surface0 #363a4f;
-@define-color base #24273a;
-@define-color mantle #1e2030;
-@define-color crust #181926;
-
-/*
- br - border
- bg - background
- fg - foreground
-*/
-
-/* main colors */
-
-@define-color accent @lavender;
-@define-color main-br @subtext0;
-@define-color main-bg @crust;
-@define-color main-fg @text;
-@define-color hover-bg @base;
-@define-color hover-fg alpha(@main-fg, 0.75);
-@define-color outline shade(@main-bg, 0.5);
-
-/* module colors */
-
-@define-color workspaces @mantle;
-@define-color temperature @mantle;
-@define-color memory @base;
-@define-color cpu @surface0;
-@define-color time @surface0;
-@define-color date @base;
-@define-color tray @mantle;
-@define-color volume @mantle;
-@define-color backlight @base;
-@define-color battery @surface0;
-
-/* state colors */
-
-@define-color warning @yellow;
-@define-color critical @red;
-@define-color charging @green;