115 lines
3.9 KiB
Python
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()
|