aboutsummaryrefslogtreecommitdiffstats
path: root/bin/udt-palette
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 12:29:38 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 12:29:38 +0200
commit0ca89376ea0a04ad88ae80b9b602033f3beb8eb6 (patch)
tree1c20a959647fb31689e97b343f1cabd11084c3ce /bin/udt-palette
parent5b133dbd86aaf54b0c728128f304c8dd8ebea9d1 (diff)
downloadunified-desktop-theme-0ca89376ea0a04ad88ae80b9b602033f3beb8eb6.tar.gz
unified-desktop-theme-0ca89376ea0a04ad88ae80b9b602033f3beb8eb6.zip
feat(palette): theme btop, Joplin and Feishin
btop gets a generated udt.theme and only the color_theme line of btop.conf is rewritten. Joplin gets userchrome.css overriding the --joplin-* custom properties most of its UI is built on, with !important since Joplin's own :root block is injected later; a userchrome.css udt did not write is left alone. Feishin 1.17 watches a Themes folder for JSON themes, so UDT.json extends its built-in Catppuccin Mocha with the scheme's roles and reaches a running Feishin live once selected. All three use existing roles, so no scheme file changes. udt-appthemes installs them next to Obsidian and Typora; the recap hint names btop and Joplin among the apps that need a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'bin/udt-palette')
-rwxr-xr-xbin/udt-palette151
1 files changed, 151 insertions, 0 deletions
diff --git a/bin/udt-palette b/bin/udt-palette
index 00e3092..cdeb571 100755
--- a/bin/udt-palette
+++ b/bin/udt-palette
@@ -13,6 +13,7 @@ what each is for, and everything else here is generated. Editing a generated
file is pointless: the next install.sh overwrites it.
"""
+import json
import re
import sys
from pathlib import Path
@@ -795,6 +796,153 @@ def load(selector_path, scheme=None):
# What gets written where. Templates are read from the repo, everything else is
# generated whole.
+def gen_btop(res, scheme, palette):
+ """btop: a .theme file, installed as ~/.config/btop/themes/udt.theme.
+
+ btop has no include or variables, so every key is written. The graphs are
+ three-stop gradients; they run through the state colours so a meter reads
+ the same way a waybar pill does, calm to critical.
+ """
+ def c(role):
+ return hex6(res[role])
+
+ def grad(name, start, mid, end):
+ return (f'theme[{name}_start]="{c(start)}"\n'
+ f'theme[{name}_mid]="{c(mid)}"\n'
+ f'theme[{name}_end]="{c(end)}"\n')
+
+ return (f"# {BANNER.format(scheme=scheme)}\n\n"
+ f'theme[main_bg]="{c("bg")}"\n'
+ f'theme[main_fg]="{c("fg")}"\n'
+ f'theme[title]="{c("fg")}"\n'
+ f'theme[hi_fg]="{c("accent")}"\n'
+ f'theme[selected_bg]="{c("surface_alt")}"\n'
+ f'theme[selected_fg]="{c("fg")}"\n'
+ f'theme[inactive_fg]="{c("fg_faint")}"\n'
+ f'theme[graph_text]="{c("fg_dim")}"\n'
+ f'theme[meter_bg]="{c("surface")}"\n'
+ f'theme[proc_misc]="{c("info")}"\n'
+ f'theme[cpu_box]="{c("accent")}"\n'
+ f'theme[mem_box]="{c("success")}"\n'
+ f'theme[net_box]="{c("peach")}"\n'
+ f'theme[proc_box]="{c("info")}"\n'
+ f'theme[div_line]="{c("border")}"\n'
+ + grad("temp", "success", "warning", "critical")
+ + grad("cpu", "success", "warning", "critical")
+ + grad("free", "surface_alt", "success", "success")
+ + grad("cached", "surface_alt", "info", "info")
+ + grad("available", "surface_alt", "peach", "peach")
+ + grad("used", "surface_alt", "critical", "critical")
+ + grad("download", "surface_alt", "info", "accent")
+ + grad("upload", "surface_alt", "peach", "warning")
+ + grad("process", "info", "accent", "critical"))
+
+
+def gen_joplin(res, scheme, palette):
+ """Joplin: userchrome.css, overriding the --joplin-* theme variables.
+
+ Joplin turns its theme object into :root custom properties and most of the
+ desktop UI reads those, so overriding them recolours it without restyling
+ a single component. !important because Joplin's own :root block is
+ injected after this file. Parts still styled from the JS theme object keep
+ the base theme's colours, so the base theme should stay a dark one.
+ """
+ def c(role):
+ return hex6(res[role])
+
+ pairs = [
+ ("background-color", c("bg")),
+ ("background-color-transparent", hex8((*res["bg"][:3], 90))),
+ ("odd-background-color", c("bg_alt")),
+ ("color", c("fg")),
+ ("color-error", c("critical")),
+ ("color-correct", c("success")),
+ ("color-warn", c("warning")),
+ ("color-warn-url", c("warning")),
+ ("color-faded", c("fg_faint")),
+ ("divider-color", c("border")),
+ ("selected-color", c("surface_alt")),
+ ("url-color", c("info")),
+ ("color-error-selected", c("critical")),
+ ("background-color2", c("bg_deep")),
+ ("color2", c("fg")),
+ ("selected-color2", c("surface_alt")),
+ ("color-error2", c("critical")),
+ ("color-warn2", c("warning")),
+ ("color-warn3", c("warning")),
+ ("background-color3", c("bg_alt")),
+ ("background-color-hover3", c("surface")),
+ ("background-color-active3", c("surface_alt")),
+ ("color3", c("fg")),
+ ("background-color4", c("bg")),
+ ("color4", c("accent")),
+ ("background-color5", c("surface")),
+ ("color5", c("fg")),
+ ("background-color-hover2", c("surface")),
+ ("color-hover2", c("fg")),
+ ("color-active2", c("fg")),
+ ("color-faded2", c("fg_dim")),
+ ("raised-background-color", c("surface")),
+ ("raised-color", c("fg")),
+ ("warning-background-color", c("surface_alt")),
+ ("destructive-color", c("critical")),
+ ("table-background-color", c("bg_alt")),
+ ("code-background-color", c("bg_deep")),
+ ("code-border-color", c("border")),
+ ("code-color", c("fg")),
+ ("header-background-color", c("bg_alt")),
+ ("text-selection-color", c("accent")),
+ ("search-marker-background-color", c("warning")),
+ ("search-marker-color", c("bg")),
+ ("focus-outline-color", c("accent")),
+ ("focus-outline-color-dimmed", hex8((*res["accent"][:3], 50))),
+ ("border-color4", c("border")),
+ ("scrollbar-thumb-color", c("fg_faint")),
+ ("scrollbar-thumb-color-hover", c("fg_dim")),
+ ("scrollbar-thumb-color2", c("fg_faint")),
+ ("scrollbar-thumb-color-hover2", c("fg_dim")),
+ ]
+ body = "\n".join(f" --joplin-{k}: {v} !important;" for k, v in pairs)
+ return f"/* {BANNER.format(scheme=scheme)} */\n\n:root {{\n{body}\n}}\n"
+
+
+def gen_feishin(res, scheme, palette):
+ """Feishin: a custom theme JSON for its watched Themes folder.
+
+ It extends the built-in Catppuccin Mocha, so everything not restated here
+ (spacing, radii, the Mantine mapping) comes from a dark theme Feishin
+ already ships. Feishin validates every colour and drops what it cannot
+ parse, so plain hex only.
+ """
+ def c(role):
+ return hex6(res[role])
+
+ r, g, b, _ = res["bg"]
+ theme = {
+ "extends": "catppuccinMocha",
+ "mode": "dark",
+ "app": {
+ "overlay-header": f"linear-gradient(transparent 0%, rgb({r} {g} {b} / 85%) 100%), var(--theme-background-noise)",
+ "overlay-subheader": f"linear-gradient(180deg, rgb({r} {g} {b} / 5%) 0%, var(--theme-colors-background) 100%), var(--theme-background-noise)",
+ },
+ "colors": {
+ "background": c("bg"),
+ "background-alternate": c("bg_deep"),
+ "foreground": c("fg"),
+ "foreground-muted": c("fg_dim"),
+ "primary": c("accent"),
+ "state-error": c("critical"),
+ "state-info": c("info"),
+ "state-success": c("success"),
+ "state-warning": c("warning"),
+ "surface": c("bg_alt"),
+ "surface-foreground": c("fg"),
+ },
+ }
+ # JSON has no comments, so the banner goes in a key Feishin ignores.
+ return json.dumps({"_comment": BANNER.format(scheme=scheme), **theme}, indent=4) + "\n"
+
+
TARGETS = [
("rofi/udt/palette.rasi", gen_rofi, None),
("templates/waybar/theme.css", gen_waybar, None),
@@ -817,6 +965,9 @@ TARGETS = [
("templates/gtk/gtk3.css", gen_gtk, "templates/gtk/gtk3.css.in"),
("templates/gtk/gtk4.css", gen_gtk, "templates/gtk/gtk4.css.in"),
("templates/gtk/index.theme", gen_gtk, "templates/gtk/index.theme.in"),
+ ("templates/btop/udt.theme", gen_btop, None),
+ ("templates/joplin/userchrome.css", gen_joplin, None),
+ ("templates/feishin/UDT.json", gen_feishin, None),
]
# The GTK widget assets, one target each. Discovered rather than listed: there