diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-11 18:57:59 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-11 18:57:59 +0200 |
| commit | 7b11746f0b6a280169150fc8e18146963a65c55c (patch) | |
| tree | 75ad1834c11187d8f88cae2bf8b7c036675609da /bin/udt-appthemes | |
| parent | fd0a0e63d78138b485c2440781f6c210dd3df8a2 (diff) | |
| download | unified-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/udt-appthemes')
| -rwxr-xr-x | bin/udt-appthemes | 88 |
1 files changed, 88 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()) |
