aboutsummaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-11 18:57:59 +0200
committerDanilo M. <danix@danix.xyz>2026-09-11 18:57:59 +0200
commit7b11746f0b6a280169150fc8e18146963a65c55c (patch)
tree75ad1834c11187d8f88cae2bf8b7c036675609da /bin
parentfd0a0e63d78138b485c2440781f6c210dd3df8a2 (diff)
downloadunified-desktop-theme-7b11746f0b6a280169150fc8e18146963a65c55c.tar.gz
unified-desktop-theme-7b11746f0b6a280169150fc8e18146963a65c55c.zip
feat(palette): add Obsidian and Typora as consumers
Both expose a first-class, update-safe theming hook, which is what separates them from the Electron apps that cannot be consumers: Signal, Discord and Spotify ship closed bundles whose only theming route is patching a signed asar that every update reverts. Obsidian gets a CSS snippet rather than a theme, so it layers over whatever theme a vault already uses instead of replacing the user's choice. Enabling it means appending to enabledCssSnippets in each vault's appearance.json, which bin/udt-appthemes does by reading the list and adding to it: rewriting the file would have switched off the layout snippet one vault was already using. Typora's theme is derived from the Dracula theme installed here, whose 350 rules all resolve through :root variables, so only that block is templated and none of the layout is touched. Verified across six vaults, nested ones included: the vault with an existing snippet kept it, a vault with no appearance.json got a valid one, and three consecutive installs leave exactly one udt entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gbjA2bmswN8jyPDKzrvqe
Diffstat (limited to 'bin')
-rwxr-xr-xbin/udt-appthemes88
-rwxr-xr-xbin/udt-palette85
2 files changed, 173 insertions, 0 deletions
diff --git a/bin/udt-appthemes b/bin/udt-appthemes
new file mode 100755
index 0000000..eedc144
--- /dev/null
+++ b/bin/udt-appthemes
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+# udt-appthemes: install the generated app themes that need more than a copy.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+"""Install the Obsidian snippet into every vault, and the Typora theme.
+
+Separate from install.sh because enabling an Obsidian snippet means editing
+JSON that the user also owns: appearance.json lists every enabled snippet, so
+the entry has to be appended rather than the file rewritten, or a vault loses
+whatever layout snippets it had.
+"""
+
+import json
+import shutil
+import sys
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parent.parent
+SNIPPET = "udt"
+VAULT_ROOT = Path.home() / "Documents" / "Obsidian"
+TYPORA_THEMES = Path.home() / ".config" / "Typora" / "themes"
+
+
+def vaults():
+ """Every Obsidian vault under the vault root.
+
+ A vault is any directory holding a .obsidian config dir. Nested vaults are
+ real: a vault inside another vault's folder is still its own vault.
+ """
+ if not VAULT_ROOT.is_dir():
+ return []
+ return sorted(p for p in VAULT_ROOT.rglob(".obsidian") if p.is_dir())
+
+
+def install_obsidian(source):
+ """Copy the snippet into each vault and enable it, preserving the rest."""
+ done = []
+ for config in vaults():
+ snippets = config / "snippets"
+ snippets.mkdir(exist_ok=True)
+ shutil.copyfile(source, snippets / f"{SNIPPET}.css")
+
+ appearance = config / "appearance.json"
+ try:
+ data = json.loads(appearance.read_text())
+ except (OSError, json.JSONDecodeError):
+ data = {}
+
+ enabled = data.get("enabledCssSnippets", [])
+ if SNIPPET not in enabled:
+ # Append: a vault may already have snippets on, and replacing the
+ # list would silently switch them off.
+ enabled.append(SNIPPET)
+ data["enabledCssSnippets"] = enabled
+ appearance.write_text(json.dumps(data, indent=2) + "\n")
+
+ done.append(config.parent.name or str(config.parent))
+ return done
+
+
+def install_typora(source):
+ """Drop the theme in. Typora picks it from Themes once the file exists."""
+ if not TYPORA_THEMES.is_dir():
+ return False
+ shutil.copyfile(source, TYPORA_THEMES / "udt.css")
+ return True
+
+
+def main():
+ obsidian = REPO / "templates" / "obsidian" / "udt.css"
+ typora = REPO / "templates" / "typora" / "udt.css"
+
+ if not obsidian.exists() or not typora.exists():
+ print("udt-appthemes: run udt-palette first", file=sys.stderr)
+ return 1
+
+ names = install_obsidian(obsidian)
+ if names:
+ print(f"obsidian: {len(names)} vaults ({', '.join(names)})")
+
+ if install_typora(typora):
+ print("typora: udt.css installed (select it in Themes)")
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/bin/udt-palette b/bin/udt-palette
index edde7f3..f2807af 100755
--- a/bin/udt-palette
+++ b/bin/udt-palette
@@ -223,6 +223,89 @@ def gen_kitty(res, scheme, palette):
return "\n".join(lines) + "\n"
+def gen_obsidian(res, scheme, palette):
+ """Obsidian: a CSS snippet overriding the theme variables.
+
+ A snippet rather than a theme, because a theme replaces the user's choice
+ outright while a snippet layers over whatever they have enabled. Obsidian
+ loads snippets after the theme, so these win without the theme having to go.
+
+ The variables are Obsidian's documented public API for this. Only colour is
+ set: spacing and typography belong to whatever theme the vault uses.
+ """
+ def c(role):
+ return hex6(res[role])
+
+ return f"""/* {BANNER.format(scheme=scheme)}
+ *
+ * Enabled per vault in Appearance > CSS snippets. install.sh writes this into
+ * every vault it finds and enables it without disturbing the snippets already
+ * on, so a vault keeps its layout snippets and gains these colours.
+ */
+
+.theme-dark {{
+ --background-primary: {c('bg')};
+ --background-primary-alt: {c('bg_alt')};
+ --background-secondary: {c('bg_alt')};
+ --background-secondary-alt: {c('bg_deep')};
+ --background-modifier-border: {c('border')};
+ --background-modifier-hover: {c('surface')};
+ --background-modifier-error: {c('critical')};
+ --background-modifier-success:{c('success')};
+
+ --text-normal: {c('fg')};
+ --text-muted: {c('fg_dim')};
+ --text-faint: {c('fg_faint')};
+ --text-error: {c('critical')};
+ --text-success: {c('success')};
+ --text-accent: {c('accent')};
+ --text-accent-hover:{c('accent_bright')};
+ --text-on-accent: {c('bg')};
+ --text-selection: {hex8((*res['accent'][:3], 30))};
+ --text-highlight-bg:{hex8((*res['warning'][:3], 40))};
+
+ --interactive-normal: {c('surface')};
+ --interactive-hover: {c('surface_alt')};
+ --interactive-accent: {c('accent')};
+ --interactive-accent-hover: {c('accent_bright')};
+
+ --h1-color: {c('accent')};
+ --h2-color: {c('accent')};
+ --h3-color: {c('info')};
+ --h4-color: {c('info')};
+ --h5-color: {c('fg_dim')};
+ --h6-color: {c('fg_dim')};
+
+ --code-normal: {c('warning')};
+ --code-background: {c('bg_alt')};
+ --blockquote-border-color: {c('accent')};
+ --hr-color: {c('border')};
+ --checkbox-color: {c('accent')};
+ --tag-color: {c('info')};
+ --tag-background: {hex8((*res['info'][:3], 20))};
+}}
+"""
+
+
+def gen_typora(res, scheme, palette, template):
+ """Typora: substitute the palette into the theme's :root block.
+
+ Typora themes are one self-contained stylesheet, but this one routes all
+ 350 of its rules through variables in :root, so only that block is a
+ template. Derived from the dracula theme already installed here.
+ """
+ out = template
+ for role in TYPORA_ROLES:
+ out = out.replace(f"@{role.upper()}@", hex6(res[role]))
+ return out
+
+
+# Exactly the placeholders templates/typora/udt.css.in carries.
+TYPORA_ROLES = ["bg", "bg_deep", "surface_alt", "fg", "fg_dim", "fg_faint",
+ "accent", "accent_bright", "critical", "warning", "success",
+ "info", "highlight"]
+
+
KVANTUM_ROLES = ["bg", "bg_alt", "surface", "surface_alt", "surface_high",
"fg", "fg_dim", "fg_faint", "mid_high", "accent",
"accent_dim", "accent_bright", "highlight", "critical"]
@@ -436,6 +519,8 @@ TARGETS = [
("templates/conky.conf", gen_conky, "templates/conky.conf.in"),
("bin/udt_colors.py", gen_accent_py, None),
("templates/homepage/custom.css", gen_homepage, None),
+ ("templates/obsidian/udt.css", gen_obsidian, None),
+ ("templates/typora/udt.css", gen_typora, "templates/typora/udt.css.in"),
("templates/kvantum/theme.kvconfig", gen_kvantum, "templates/kvantum/theme.kvconfig.in"),
("templates/kvantum/theme.svg", gen_kvantum, "templates/kvantum/theme.svg.in"),
]