aboutsummaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
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"),
]