illuscape/scripts/merge_prefs.py

115 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
merge_prefs.py — Merge Illuscape config patch into Inkscape preferences.xml
Usage:
python3 merge_prefs.py --prefs ~/.config/inkscape/preferences.xml \
--patch config/preferences-patch.xml \
--preset cc|cs6
"""
import argparse
import os
import shutil
import tempfile
from pathlib import Path
from xml.etree import ElementTree as ET
PRESET_OVERRIDES = {
"cc": {
("options", "keyboard"): {"file": "illustrator-cc"},
("options", "units"): {"doc": "px", "font": "px"},
},
"cs6": {
("options", "keyboard"): {"file": "illustrator-cs6"},
("options", "units"): {"doc": "pt", "font": "pt"},
},
}
def _atomic_write(tree: ET.ElementTree, path: Path) -> None:
"""Write XML tree atomically — prevents corruption on interrupt."""
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
tree.write(fh, encoding="unicode", xml_declaration=True)
os.replace(tmp, path)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
def _merge_nodes(live_parent: ET.Element, patch_parent: ET.Element) -> None:
"""Recursively upsert patch_parent's children into live_parent."""
for patch_child in patch_parent:
child_id = patch_child.get("id")
if child_id is None:
continue
live_child = next(
(c for c in live_parent if c.get("id") == child_id), None
)
if live_child is None:
live_child = ET.SubElement(live_parent, patch_child.tag)
live_child.set("id", child_id)
for attr, value in patch_child.attrib.items():
if attr != "id":
live_child.set(attr, value)
_merge_nodes(live_child, patch_child)
def _apply_preset(root: ET.Element, preset: str) -> None:
"""Apply preset-specific overrides (active keys file, document units)."""
if preset not in PRESET_OVERRIDES:
raise ValueError(f"Unknown preset {preset!r}. Valid: {list(PRESET_OVERRIDES)}")
for id_path, attrs in PRESET_OVERRIDES[preset].items():
node = root
for id_val in id_path:
child = next((c for c in node if c.get("id") == id_val), None)
if child is None:
child = ET.SubElement(node, "group")
child.set("id", id_val)
node = child
for attr, value in attrs.items():
node.set(attr, value)
def merge(prefs_path: Path, patch_path: Path, preset: str) -> None:
"""Merge patch into prefs, applying preset overrides. Lenient: creates prefs if missing."""
patch_tree = ET.parse(patch_path) # raises FileNotFoundError or ParseError
patch_root = patch_tree.getroot()
if not prefs_path.exists():
# Lenient: seed from patch, let Inkscape fill in the rest on first launch
prefs_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(patch_path, prefs_path)
live_tree = ET.parse(prefs_path)
_apply_preset(live_tree.getroot(), preset)
_atomic_write(live_tree, prefs_path)
return
live_tree = ET.parse(prefs_path) # raises ParseError on malformed XML
live_root = live_tree.getroot()
_merge_nodes(live_root, patch_root)
_apply_preset(live_root, preset)
live_tree.write(str(prefs_path), encoding="unicode", xml_declaration=True)
def main() -> None:
parser = argparse.ArgumentParser(
description="Merge Illuscape preferences patch into Inkscape preferences.xml"
)
parser.add_argument("--prefs", required=True, type=Path)
parser.add_argument("--patch", required=True, type=Path)
parser.add_argument("--preset", required=True, choices=["cc", "cs6"])
args = parser.parse_args()
merge(args.prefs, args.patch, args.preset)
print(f"Illuscape preferences merged ({args.preset} preset)")
if __name__ == "__main__":
main()