#!/usr/bin/env python3 """ Illuscape installer — cross-platform (Linux, Windows, macOS). Usage: python3 install.py [--preset=cc|cs6] [--yes] ./install.sh [--preset=cc|cs6] [--yes] (Linux / macOS) """ from __future__ import annotations import argparse import datetime import os import platform import re import shutil import subprocess import sys from pathlib import Path # Ensure scripts/ is importable regardless of cwd SCRIPT_DIR = Path(__file__).parent sys.path.insert(0, str(SCRIPT_DIR / "scripts")) from detect_platform import detect_config # noqa: E402 ICON_SIZES = (16, 32, 48, 64, 128, 256, 512) # ── Dependency check ────────────────────────────────────────────────────────── def check_deps(auto_yes: bool) -> None: """Verify Inkscape is present and meets the minimum version.""" if not shutil.which("inkscape"): sys.exit("✗ Inkscape is not installed. Install Inkscape first, then run Illuscape.") try: raw = subprocess.run( ["inkscape", "--version"], capture_output=True, text=True, timeout=10, ).stdout except (FileNotFoundError, subprocess.TimeoutExpired): return # cannot check version — proceed anyway match = re.search(r"(\d+)\.(\d+)", raw) if match: major, minor = int(match.group(1)), int(match.group(2)) if major < 1 or (major == 1 and minor < 2): version = f"{major}.{minor}" print(f"⚠ Inkscape {version} detected. Illuscape targets Inkscape 1.2+.") print(" Some settings may not apply correctly. Continue anyway? [y/N]") if not auto_yes and input().strip().lower() != "y": sys.exit(0) # ── Backup ──────────────────────────────────────────────────────────────────── def backup_config(config_path: Path) -> None: """Copy the config directory to a timestamped backup; idempotent on repeat runs.""" timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") backup_dir = Path(str(config_path) + f".bak-illuscape-{timestamp}") existing = sorted(config_path.parent.glob(config_path.name + ".bak-illuscape-*")) if existing: print("→ Backup already exists — skipping (idempotent)") return if config_path.is_dir(): shutil.copytree(config_path, backup_dir) print(f"→ Backed up to: {backup_dir}") else: print("→ No existing config to back up (fresh install)") # ── Preset selection ────────────────────────────────────────────────────────── def choose_preset(preset: str, auto_yes: bool) -> str: """Prompt the user for CC / CS6 unless preset is already provided.""" if preset: return preset print() print("Which Illustrator era are you coming from?") print(" [1] CC — current Creative Cloud (default)") print(" [2] CS6 — last perpetual license") print("Choice [1]: ", end="", flush=True) if auto_yes: print("1 (auto)") return "cc" choice = input().strip() return "cs6" if choice == "2" else "cc" # ── Preferences merge ───────────────────────────────────────────────────────── def merge_prefs(config_path: Path, preset: str) -> None: """Invoke merge_prefs.py to upsert the preferences patch into Inkscape's prefs.""" patch_path = SCRIPT_DIR / "config/preferences-patch.xml" if not patch_path.exists(): sys.exit( f"✗ Installation file missing: {patch_path}\n" " The repo may be incomplete — try re-cloning." ) prefs_path = config_path / "preferences.xml" if not prefs_path.exists(): print("→ No Inkscape preferences found — seeding from Illuscape defaults") print(" (Inkscape will fill in the rest on first launch)") try: subprocess.run( [ sys.executable, str(SCRIPT_DIR / "scripts/merge_prefs.py"), "--prefs", str(prefs_path), "--patch", str(patch_path), "--preset", preset, ], check=True, ) except subprocess.CalledProcessError: sys.exit( "✗ Could not merge Inkscape preferences.\n" " Launch Inkscape once to initialise its config, then re-run this installer." ) # ── Config file copy ────────────────────────────────────────────────────────── def copy_files(config_path: Path, preset: str) -> None: """Copy keys, palettes, templates, and symbols into the Inkscape config dir.""" for subdir in ("keys", "palettes", "templates", "symbols", "splashscreens"): (config_path / subdir).mkdir(parents=True, exist_ok=True) shutil.copy2( SCRIPT_DIR / f"config/keys/illustrator-{preset}.xml", config_path / "keys/", ) for palette in (SCRIPT_DIR / "config/palettes").glob("*.gpl"): shutil.copy2(palette, config_path / "palettes/") for template in (SCRIPT_DIR / "config/templates").glob("*.svg"): shutil.copy2(template, config_path / "templates/") for symbol in (SCRIPT_DIR / "config/symbols").glob("*.svg"): shutil.copy2(symbol, config_path / "symbols/") print("→ Config files copied") # ── Desktop integration ─────────────────────────────────────────────────────── def install_desktop(config_path: Path, install_method: str) -> None: """Install a desktop launcher / Start Menu shortcut for the current platform.""" system = platform.system() if system == "Linux" and install_method == "native": _install_linux_desktop(config_path) elif system == "Windows": _install_windows_desktop(config_path) # macOS: Inkscape.app handles its own dock icon; no extra step needed. def _install_linux_desktop(config_path: Path) -> None: app_dir = Path.home() / ".local/share/applications" icon_src = SCRIPT_DIR / "assets/illuscape.svg" splash_src = SCRIPT_DIR / "assets/splash.svg" app_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(SCRIPT_DIR / "assets/org.inkscape.Inkscape.desktop", app_dir) for size in ICON_SIZES: icon_dir = Path.home() / f".local/share/icons/hicolor/{size}x{size}/apps" icon_dir.mkdir(parents=True, exist_ok=True) icon_out = icon_dir / "illuscape.png" if _rsvg_convert(icon_src, icon_out, size, size): continue if _inkscape_export(icon_src, icon_out, size, size): continue print(f"⚠ Could not rasterize icon at {size}px (install rsvg-convert for icons)") splash_out = config_path / "splashscreens/illuscape-splash.png" _rsvg_convert(splash_src, splash_out, 600, 400) subprocess.run( ["update-desktop-database", str(app_dir)], capture_output=True, ) print("→ Desktop launcher installed") def _install_windows_desktop(config_path: Path) -> None: """ Create a Start Menu .lnk shortcut via PowerShell WScript.Shell COM object. The ICO is copied to the config dir so it stays with the config. """ ico_src = SCRIPT_DIR / "assets/illuscape.ico" ico_dst = config_path / "illuscape.ico" if ico_src.exists(): shutil.copy2(ico_src, ico_dst) start_menu = ( Path(os.environ.get("APPDATA", "")) / "Microsoft/Windows/Start Menu/Programs" ) lnk_path = start_menu / "Illuscape.lnk" inkscape_exe = shutil.which("inkscape") or "inkscape.exe" # Build a PowerShell one-liner using WScript.Shell ps_lines = [ "$ws = New-Object -ComObject WScript.Shell", f"$s = $ws.CreateShortcut('{lnk_path}')", f"$s.TargetPath = '{inkscape_exe}'", "$s.Description = 'Inkscape with Illustrator-style config'", ] if ico_dst.exists(): ps_lines.append(f"$s.IconLocation = '{ico_dst}'") ps_lines.append("$s.Save()") try: subprocess.run( ["powershell", "-NoProfile", "-Command", "; ".join(ps_lines)], check=True, capture_output=True, text=True, ) print("→ Start Menu shortcut installed") except (FileNotFoundError, subprocess.CalledProcessError) as exc: print(f"⚠ Could not create Start Menu shortcut: {exc}") print(" You can create one manually pointing to inkscape.exe.") # ── Icon rasterization helpers ──────────────────────────────────────────────── def _rsvg_convert(src: Path, dst: Path, w: int, h: int) -> bool: """Rasterize *src* SVG to *dst* PNG using rsvg-convert. Returns True on success.""" if not shutil.which("rsvg-convert"): return False result = subprocess.run( ["rsvg-convert", "-w", str(w), "-h", str(h), str(src), "-o", str(dst)], capture_output=True, ) return result.returncode == 0 def _inkscape_export(src: Path, dst: Path, w: int, h: int) -> bool: """ Rasterize *src* SVG using Inkscape 1.x export flags. (--export-png was removed in Inkscape 1.0) """ result = subprocess.run( [ "inkscape", f"--export-filename={dst}", "--export-type=png", f"--export-width={w}", f"--export-height={h}", str(src), ], capture_output=True, ) return result.returncode == 0 # ── CLI ─────────────────────────────────────────────────────────────────────── def _parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description="Illuscape — Inkscape with Illustrator feel") p.add_argument( "--preset", choices=["cc", "cs6"], default="", help="Illustrator shortcut era: cc (default) or cs6", ) p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompts") return p.parse_args() def main() -> None: args = _parse_args() print("╔══════════════════════════════╗") print("║ Illuscape Installer ║") print("╚══════════════════════════════╝") print() check_deps(args.yes) config_path, install_method = detect_config() print(f"→ Inkscape config: {config_path} ({install_method})") backup_config(config_path) preset = choose_preset(args.preset, args.yes) print(f"→ Using preset: {preset}") merge_prefs(config_path, preset) copy_files(config_path, preset) install_desktop(config_path, install_method) print() print(f"✓ Illuscape installed (preset: {preset})") print() print(" Open Inkscape to start using your Illustrator-style workspace.") print(" To uninstall: python3 uninstall.py (or ./uninstall.sh on Linux/macOS)") print() if __name__ == "__main__": main()