Compare commits
No commits in common. "feat/windows-port" and "main" have entirely different histories.
feat/windo
...
main
10 changed files with 267 additions and 794 deletions
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
|
|
@ -7,13 +7,10 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-${{ matrix.ubuntu }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
ubuntu: ["22.04", "24.04"]
|
||||||
- os: ubuntu-22.04
|
|
||||||
- os: ubuntu-24.04
|
|
||||||
- os: windows-latest
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -23,25 +20,19 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Install test deps (Linux)
|
- name: Install test deps
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
run: |
|
||||||
pip install pytest
|
pip install pytest
|
||||||
sudo apt-get install -y shellcheck inkscape
|
sudo apt-get install -y bats shellcheck inkscape
|
||||||
|
|
||||||
- name: Install test deps (Windows)
|
- name: Python unit tests
|
||||||
if: runner.os == 'Windows'
|
run: pytest tests/test_merge_prefs.py -v
|
||||||
run: pip install pytest
|
|
||||||
|
|
||||||
- name: Shellcheck (Linux only)
|
- name: Shellcheck
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
run: |
|
||||||
shellcheck install.sh
|
shellcheck install.sh
|
||||||
shellcheck uninstall.sh
|
shellcheck uninstall.sh
|
||||||
shellcheck scripts/detect_platform.sh
|
shellcheck scripts/detect_platform.sh
|
||||||
|
|
||||||
- name: Python unit tests
|
- name: Bats integration tests
|
||||||
run: pytest tests/test_merge_prefs.py -v
|
run: bats tests/test_install.bats
|
||||||
|
|
||||||
- name: Python integration tests
|
|
||||||
run: pytest tests/test_install.py -v
|
|
||||||
|
|
|
||||||
|
|
@ -124,10 +124,6 @@ Your original Inkscape config is restored from the timestamped backup created at
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs welcome.
|
See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs welcome.
|
||||||
|
|
||||||
## Development tooling
|
|
||||||
|
|
||||||
All code in this repo is reviewed, tested, and owned by human contributors. LLM (large language model) coding tools are part of our development workflow.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-3.0 — see [LICENSE](LICENSE).
|
GPL-3.0 — see [LICENSE](LICENSE).
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
306
install.py
306
install.py
|
|
@ -1,306 +0,0 @@
|
||||||
#!/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()
|
|
||||||
183
install.sh
183
install.sh
|
|
@ -1,6 +1,185 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Illuscape installer — thin wrapper around install.py
|
# Illuscape installer
|
||||||
# Usage: ./install.sh [--preset=cc|cs6] [--yes]
|
# Usage: ./install.sh [--preset=cc|cs6] [--yes]
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
exec python3 "$SCRIPT_DIR/install.py" "$@"
|
PRESET=""
|
||||||
|
AUTO_YES=0
|
||||||
|
|
||||||
|
# ── Parse args ──────────────────────────────────────────────────────────────
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--preset=cc|--preset=cs6) PRESET="${arg#--preset=}" ;;
|
||||||
|
--yes|-y) AUTO_YES=1 ;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: ./install.sh [--preset=cc|cs6] [--yes]"
|
||||||
|
echo " --preset=cc Illustrator CC shortcuts (default)"
|
||||||
|
echo " --preset=cs6 Illustrator CS6 shortcuts"
|
||||||
|
echo " --yes Skip confirmation prompts"
|
||||||
|
exit 0 ;;
|
||||||
|
*) echo "Unknown argument: $arg"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Check dependencies ───────────────────────────────────────────────────────
|
||||||
|
check_deps() {
|
||||||
|
if ! command -v inkscape &>/dev/null; then
|
||||||
|
echo "✗ Inkscape is not installed. Install Inkscape first, then run Illuscape."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local version
|
||||||
|
version=$(inkscape --version 2>/dev/null | grep -oP '\d+\.\d+' | head -1)
|
||||||
|
local major minor
|
||||||
|
IFS='.' read -r major minor <<< "$version"
|
||||||
|
# Skip version check if regex couldn't match (non-standard version string)
|
||||||
|
if [[ -n "$major" ]] && { (( major < 1 )) || (( major == 1 && minor < 2 )); }; then
|
||||||
|
echo "⚠ Inkscape $version detected. Illuscape targets Inkscape 1.2+."
|
||||||
|
echo " Some settings may not apply correctly. Continue anyway? [y/N]"
|
||||||
|
[[ $AUTO_YES == 1 ]] || { read -r ans; [[ "$ans" =~ ^[Yy]$ ]] || exit 0; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v python3 &>/dev/null; then
|
||||||
|
echo "✗ python3 is required. Install it and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Detect platform ──────────────────────────────────────────────────────────
|
||||||
|
detect_config() {
|
||||||
|
source "$SCRIPT_DIR/scripts/detect_platform.sh"
|
||||||
|
echo "→ Inkscape config: $INKSCAPE_CONFIG ($INKSCAPE_INSTALL_METHOD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Backup ───────────────────────────────────────────────────────────────────
|
||||||
|
backup_config() {
|
||||||
|
local backup_dir="${INKSCAPE_CONFIG}.bak-illuscape-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
|
# If any illuscape backup already exists, skip (idempotent)
|
||||||
|
if ls "${INKSCAPE_CONFIG}".bak-illuscape-* &>/dev/null; then
|
||||||
|
echo "→ Backup already exists — skipping (idempotent)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$INKSCAPE_CONFIG" ]]; then
|
||||||
|
cp -r "$INKSCAPE_CONFIG" "$backup_dir"
|
||||||
|
echo "→ Backed up to: $backup_dir"
|
||||||
|
else
|
||||||
|
echo "→ No existing config to back up (fresh install)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Prompt for preset ────────────────────────────────────────────────────────
|
||||||
|
choose_preset() {
|
||||||
|
[[ -n "$PRESET" ]] && return 0
|
||||||
|
echo ""
|
||||||
|
echo "Which Illustrator era are you coming from?"
|
||||||
|
echo " [1] CC — current Creative Cloud (default)"
|
||||||
|
echo " [2] CS6 — last perpetual license"
|
||||||
|
printf "Choice [1]: "
|
||||||
|
if [[ $AUTO_YES == 1 ]]; then
|
||||||
|
echo "1 (auto)"
|
||||||
|
PRESET="cc"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
read -r choice
|
||||||
|
case "${choice:-1}" in
|
||||||
|
2) PRESET="cs6" ;;
|
||||||
|
*) PRESET="cc" ;;
|
||||||
|
esac
|
||||||
|
echo "→ Using preset: $PRESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Merge preferences ────────────────────────────────────────────────────────
|
||||||
|
merge_prefs() {
|
||||||
|
local prefs_file="$INKSCAPE_CONFIG/preferences.xml"
|
||||||
|
python3 "$SCRIPT_DIR/scripts/merge_prefs.py" \
|
||||||
|
--prefs "$prefs_file" \
|
||||||
|
--patch "$SCRIPT_DIR/config/preferences-patch.xml" \
|
||||||
|
--preset "$PRESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Copy config files ────────────────────────────────────────────────────────
|
||||||
|
copy_files() {
|
||||||
|
mkdir -p \
|
||||||
|
"$INKSCAPE_CONFIG/keys" \
|
||||||
|
"$INKSCAPE_CONFIG/palettes" \
|
||||||
|
"$INKSCAPE_CONFIG/templates" \
|
||||||
|
"$INKSCAPE_CONFIG/symbols" \
|
||||||
|
"$INKSCAPE_CONFIG/splashscreens"
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/config/keys/illustrator-${PRESET}.xml" \
|
||||||
|
"$INKSCAPE_CONFIG/keys/"
|
||||||
|
|
||||||
|
cp "$SCRIPT_DIR/config/palettes/"*.gpl "$INKSCAPE_CONFIG/palettes/"
|
||||||
|
cp "$SCRIPT_DIR/config/templates/"*.svg "$INKSCAPE_CONFIG/templates/"
|
||||||
|
cp "$SCRIPT_DIR/config/symbols/"*.svg "$INKSCAPE_CONFIG/symbols/"
|
||||||
|
|
||||||
|
echo "→ Config files copied"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Install desktop launcher + icons (Linux native only) ─────────────────────
|
||||||
|
install_desktop() {
|
||||||
|
[[ "$INKSCAPE_INSTALL_METHOD" != "native" ]] && return 0
|
||||||
|
|
||||||
|
local app_dir="$HOME/.local/share/applications"
|
||||||
|
local icon_src="$SCRIPT_DIR/assets/illuscape.svg"
|
||||||
|
local splash_src="$SCRIPT_DIR/assets/splash.svg"
|
||||||
|
local icon_sizes=(16 32 48 64 128 256 512)
|
||||||
|
|
||||||
|
mkdir -p "$app_dir"
|
||||||
|
cp "$SCRIPT_DIR/assets/org.inkscape.Inkscape.desktop" "$app_dir/"
|
||||||
|
|
||||||
|
# Rasterize icon at each size (prefer rsvg-convert, fall back to Inkscape)
|
||||||
|
for size in "${icon_sizes[@]}"; do
|
||||||
|
local icon_dir="$HOME/.local/share/icons/hicolor/${size}x${size}/apps"
|
||||||
|
mkdir -p "$icon_dir"
|
||||||
|
if command -v rsvg-convert &>/dev/null; then
|
||||||
|
rsvg-convert -w "$size" -h "$size" "$icon_src" \
|
||||||
|
-o "$icon_dir/illuscape.png" 2>/dev/null && continue
|
||||||
|
fi
|
||||||
|
if command -v inkscape &>/dev/null; then
|
||||||
|
# Inkscape 1.x uses --export-filename + --export-type (--export-png removed in 1.0)
|
||||||
|
inkscape --export-filename="$icon_dir/illuscape.png" \
|
||||||
|
--export-type=png \
|
||||||
|
--export-width="$size" --export-height="$size" \
|
||||||
|
"$icon_src" 2>/dev/null && continue
|
||||||
|
fi
|
||||||
|
echo "⚠ Could not rasterize icon at ${size}px (install rsvg-convert for icons)"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Splash screen (600x400)
|
||||||
|
if command -v rsvg-convert &>/dev/null; then
|
||||||
|
rsvg-convert -w 600 -h 400 "$splash_src" \
|
||||||
|
-o "$INKSCAPE_CONFIG/splashscreens/illuscape-splash.png" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
update-desktop-database "$app_dir" 2>/dev/null || true
|
||||||
|
echo "→ Desktop launcher installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
main() {
|
||||||
|
echo "╔══════════════════════════════╗"
|
||||||
|
echo "║ Illuscape Installer ║"
|
||||||
|
echo "╚══════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_deps
|
||||||
|
detect_config
|
||||||
|
backup_config
|
||||||
|
choose_preset
|
||||||
|
merge_prefs
|
||||||
|
copy_files
|
||||||
|
install_desktop
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Illuscape installed (preset: $PRESET)"
|
||||||
|
echo ""
|
||||||
|
echo " Open Inkscape to start using your Illustrator-style workspace."
|
||||||
|
echo " To uninstall: ./uninstall.sh"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
"""
|
|
||||||
detect_platform.py — Inkscape config-path detection, cross-platform.
|
|
||||||
|
|
||||||
Shared by install.py and uninstall.py.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def _cmd_contains(cmd: list[str], pattern: str) -> bool:
|
|
||||||
"""Run *cmd*; return True if stdout contains *pattern*."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd, capture_output=True, text=True, timeout=5
|
|
||||||
)
|
|
||||||
return pattern in result.stdout
|
|
||||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def detect_config() -> tuple[Path, str]:
|
|
||||||
"""
|
|
||||||
Return (inkscape_config_path, install_method).
|
|
||||||
|
|
||||||
install_method is one of: "flatpak", "snap", "native", "store"
|
|
||||||
"""
|
|
||||||
system = platform.system()
|
|
||||||
if system == "Windows":
|
|
||||||
return _detect_windows()
|
|
||||||
if system == "Darwin":
|
|
||||||
return _detect_macos()
|
|
||||||
return _detect_linux()
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_linux() -> tuple[Path, str]:
|
|
||||||
# Flatpak takes priority — most common modern Linux install
|
|
||||||
flatpak_config = Path.home() / ".var/app/org.inkscape.Inkscape/config/inkscape"
|
|
||||||
if flatpak_config.is_dir() or _cmd_contains(
|
|
||||||
["flatpak", "list"], "org.inkscape.Inkscape"
|
|
||||||
):
|
|
||||||
return flatpak_config, "flatpak"
|
|
||||||
|
|
||||||
# Snap
|
|
||||||
snap_config = Path.home() / "snap/inkscape/current/.config/inkscape"
|
|
||||||
if snap_config.is_dir() or _cmd_contains(["snap", "list"], "inkscape"):
|
|
||||||
return snap_config, "snap"
|
|
||||||
|
|
||||||
# Native / XDG
|
|
||||||
xdg = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "inkscape"
|
|
||||||
return xdg, "native"
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_macos() -> tuple[Path, str]:
|
|
||||||
# App Bundle (DMG install) stores config under ~/Library/Application Support
|
|
||||||
app_support = (
|
|
||||||
Path.home()
|
|
||||||
/ "Library/Application Support/org.inkscape.Inkscape/config/inkscape"
|
|
||||||
)
|
|
||||||
if app_support.is_dir():
|
|
||||||
return app_support, "native"
|
|
||||||
# Homebrew / XDG fallback
|
|
||||||
xdg = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "inkscape"
|
|
||||||
return xdg, "native"
|
|
||||||
|
|
||||||
|
|
||||||
def _detect_windows() -> tuple[Path, str]:
|
|
||||||
# MS Store — glob for the Packages directory
|
|
||||||
local_appdata = os.environ.get("LOCALAPPDATA", "")
|
|
||||||
if local_appdata:
|
|
||||||
packages = Path(local_appdata) / "Packages"
|
|
||||||
if packages.is_dir():
|
|
||||||
matches = list(packages.glob("org.inkscape.Inkscape*"))
|
|
||||||
if matches:
|
|
||||||
return matches[0] / "LocalCache/Roaming/inkscape", "store"
|
|
||||||
|
|
||||||
# Native (winget / Scoop / chocolatey / official installer)
|
|
||||||
appdata = Path(os.environ.get("APPDATA", Path.home() / "AppData/Roaming"))
|
|
||||||
return appdata / "inkscape", "native"
|
|
||||||
|
|
@ -9,23 +9,23 @@ _detect_inkscape_config() {
|
||||||
# Flatpak takes priority — most common modern Linux install
|
# Flatpak takes priority — most common modern Linux install
|
||||||
local flatpak_config="$HOME/.var/app/org.inkscape.Inkscape/config/inkscape"
|
local flatpak_config="$HOME/.var/app/org.inkscape.Inkscape/config/inkscape"
|
||||||
if [[ -d "$flatpak_config" ]] || flatpak list 2>/dev/null | grep -q "org.inkscape.Inkscape"; then
|
if [[ -d "$flatpak_config" ]] || flatpak list 2>/dev/null | grep -q "org.inkscape.Inkscape"; then
|
||||||
export INKSCAPE_CONFIG="$flatpak_config"
|
INKSCAPE_CONFIG="$flatpak_config"
|
||||||
export INKSCAPE_INSTALL_METHOD="flatpak"
|
INKSCAPE_INSTALL_METHOD="flatpak"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Snap
|
# Snap
|
||||||
local snap_config="$HOME/snap/inkscape/current/.config/inkscape"
|
local snap_config="$HOME/snap/inkscape/current/.config/inkscape"
|
||||||
if [[ -d "$snap_config" ]] || snap list 2>/dev/null | grep -q "^inkscape "; then
|
if [[ -d "$snap_config" ]] || snap list 2>/dev/null | grep -q "^inkscape "; then
|
||||||
export INKSCAPE_CONFIG="$snap_config"
|
INKSCAPE_CONFIG="$snap_config"
|
||||||
export INKSCAPE_INSTALL_METHOD="snap"
|
INKSCAPE_INSTALL_METHOD="snap"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Native / XDG
|
# Native / XDG
|
||||||
local xdg_config="${XDG_CONFIG_HOME:-$HOME/.config}/inkscape"
|
local xdg_config="${XDG_CONFIG_HOME:-$HOME/.config}/inkscape"
|
||||||
export INKSCAPE_CONFIG="$xdg_config"
|
INKSCAPE_CONFIG="$xdg_config"
|
||||||
export INKSCAPE_INSTALL_METHOD="native"
|
INKSCAPE_INSTALL_METHOD="native"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,222 +0,0 @@
|
||||||
"""
|
|
||||||
Integration tests for install.py and uninstall.py — cross-platform.
|
|
||||||
Replaces tests/test_install.bats.
|
|
||||||
|
|
||||||
Each test runs inside a fully isolated tmp_path with a fake inkscape binary
|
|
||||||
injected via PATH and an overridden config home pointing to tmp_path.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
REPO = Path(__file__).parent.parent
|
|
||||||
INSTALL_PY = REPO / "install.py"
|
|
||||||
UNINSTALL_PY = REPO / "uninstall.py"
|
|
||||||
|
|
||||||
_FAKE_INKSCAPE_SH = """\
|
|
||||||
#!/usr/bin/env sh
|
|
||||||
case "$1" in
|
|
||||||
--version) echo 'Inkscape 1.3.2 (1:1.3.2+202311252150+091e20ef0f)' ;;
|
|
||||||
esac
|
|
||||||
"""
|
|
||||||
|
|
||||||
_FAKE_INKSCAPE_BAT = """\
|
|
||||||
@echo off
|
|
||||||
if "%1"=="--version" echo Inkscape 1.3.2 (1:1.3.2+202311252150+091e20ef0f)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def _make_fake_inkscape(bin_dir: Path) -> None:
|
|
||||||
"""Create a platform-appropriate fake inkscape binary in *bin_dir*."""
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
bat = bin_dir / "inkscape.bat"
|
|
||||||
bat.write_text(_FAKE_INKSCAPE_BAT)
|
|
||||||
else:
|
|
||||||
script = bin_dir / "inkscape"
|
|
||||||
script.write_text(_FAKE_INKSCAPE_SH)
|
|
||||||
script.chmod(0o755)
|
|
||||||
|
|
||||||
|
|
||||||
def _config_dir(tmp_path: Path) -> Path:
|
|
||||||
"""
|
|
||||||
Return the expected config path matching detect_config() for this platform
|
|
||||||
when the env fixture overrides the config home to *tmp_path*.
|
|
||||||
"""
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
# install.py on Windows: APPDATA/inkscape
|
|
||||||
return tmp_path / "config" / "inkscape"
|
|
||||||
else:
|
|
||||||
# install.py on Linux/macOS: XDG_CONFIG_HOME/inkscape
|
|
||||||
return tmp_path / "config" / "inkscape"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def env(tmp_path: Path):
|
|
||||||
"""
|
|
||||||
Isolated subprocess environment with a fake inkscape and a temp config dir.
|
|
||||||
On Linux/macOS: overrides HOME and XDG_CONFIG_HOME.
|
|
||||||
On Windows: overrides APPDATA and LOCALAPPDATA.
|
|
||||||
"""
|
|
||||||
bin_dir = tmp_path / "bin"
|
|
||||||
bin_dir.mkdir()
|
|
||||||
_make_fake_inkscape(bin_dir)
|
|
||||||
|
|
||||||
cfg_dir = _config_dir(tmp_path)
|
|
||||||
cfg_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
base_env = os.environ.copy()
|
|
||||||
base_env["PATH"] = str(bin_dir) + os.pathsep + base_env.get("PATH", "")
|
|
||||||
# Block flatpak / snap detection inside subprocesses
|
|
||||||
base_env.pop("FLATPAK_ID", None)
|
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
base_env["APPDATA"] = str(tmp_path / "config")
|
|
||||||
base_env["LOCALAPPDATA"] = str(tmp_path / "local")
|
|
||||||
else:
|
|
||||||
base_env["XDG_CONFIG_HOME"] = str(tmp_path / "config")
|
|
||||||
base_env["HOME"] = str(tmp_path)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"tmp_path": tmp_path,
|
|
||||||
"config_dir": cfg_dir,
|
|
||||||
"env": base_env,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _run_install(env_fixture: dict, preset: str = "cc") -> subprocess.CompletedProcess[str]:
|
|
||||||
return subprocess.run(
|
|
||||||
[sys.executable, str(INSTALL_PY), "--yes", f"--preset={preset}"],
|
|
||||||
env=env_fixture["env"],
|
|
||||||
cwd=str(REPO),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_uninstall(env_fixture: dict) -> subprocess.CompletedProcess[str]:
|
|
||||||
return subprocess.run(
|
|
||||||
[sys.executable, str(UNINSTALL_PY)],
|
|
||||||
env=env_fixture["env"],
|
|
||||||
cwd=str(REPO),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ── install tests ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_install_creates_backup(env):
|
|
||||||
result = _run_install(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
backups = list(env["tmp_path"].glob("config/inkscape.bak-illuscape-*"))
|
|
||||||
assert backups, "No backup directory created"
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_backup_is_idempotent(env):
|
|
||||||
_run_install(env)
|
|
||||||
first_backups = sorted(env["tmp_path"].glob("config/inkscape.bak-illuscape-*"))
|
|
||||||
_run_install(env)
|
|
||||||
second_backups = sorted(env["tmp_path"].glob("config/inkscape.bak-illuscape-*"))
|
|
||||||
assert first_backups == second_backups, "Second install created an extra backup"
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_cc_key_file(env):
|
|
||||||
result = _run_install(env, preset="cc")
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
assert (env["config_dir"] / "keys/illustrator-cc.xml").exists()
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_cs6_key_file(env):
|
|
||||||
result = _run_install(env, preset="cs6")
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
assert (env["config_dir"] / "keys/illustrator-cs6.xml").exists()
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_palettes(env):
|
|
||||||
result = _run_install(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
for name in (
|
|
||||||
"Illustrator-Defaults.gpl",
|
|
||||||
"Illustrator-Grays.gpl",
|
|
||||||
"Illustrator-Earth.gpl",
|
|
||||||
):
|
|
||||||
assert (env["config_dir"] / "palettes" / name).exists(), f"Missing palette: {name}"
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_templates(env):
|
|
||||||
result = _run_install(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
for name in (
|
|
||||||
"Letter.svg",
|
|
||||||
"A4.svg",
|
|
||||||
"Web-1920x1080.svg",
|
|
||||||
"Web-1280x720.svg",
|
|
||||||
"Print-CMYK-Letter.svg",
|
|
||||||
"Print-CMYK-A4.svg",
|
|
||||||
):
|
|
||||||
assert (env["config_dir"] / "templates" / name).exists(), f"Missing template: {name}"
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_creates_prefs(env):
|
|
||||||
result = _run_install(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
assert (env["config_dir"] / "preferences.xml").exists()
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_prefs_contains_units(env):
|
|
||||||
result = _run_install(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
content = (env["config_dir"] / "preferences.xml").read_text()
|
|
||||||
assert "px" in content or "pt" in content, "No unit setting found in preferences.xml"
|
|
||||||
|
|
||||||
|
|
||||||
def test_install_noninteractive(env):
|
|
||||||
"""--yes + --preset should exit 0 without any stdin."""
|
|
||||||
result = _run_install(env)
|
|
||||||
assert result.returncode == 0, f"Non-interactive install failed:\n{result.stderr}"
|
|
||||||
|
|
||||||
|
|
||||||
# ── uninstall tests ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_uninstall_restores_prefs(env):
|
|
||||||
"""Original prefs.xml is restored after install + uninstall."""
|
|
||||||
original_xml = '<inkscape version="1.0" />'
|
|
||||||
(env["config_dir"] / "preferences.xml").write_text(original_xml)
|
|
||||||
|
|
||||||
_run_install(env)
|
|
||||||
# prefs should now contain the patched version
|
|
||||||
result = _run_uninstall(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
|
|
||||||
restored = (env["config_dir"] / "preferences.xml").read_text()
|
|
||||||
assert restored == original_xml, "prefs.xml was not restored to the original content"
|
|
||||||
|
|
||||||
|
|
||||||
def test_uninstall_removes_palettes(env):
|
|
||||||
_run_install(env)
|
|
||||||
result = _run_uninstall(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
for name in (
|
|
||||||
"Illustrator-Defaults.gpl",
|
|
||||||
"Illustrator-Grays.gpl",
|
|
||||||
"Illustrator-Earth.gpl",
|
|
||||||
):
|
|
||||||
assert not (
|
|
||||||
env["config_dir"] / "palettes" / name
|
|
||||||
).exists(), f"Palette was not removed: {name}"
|
|
||||||
|
|
||||||
|
|
||||||
def test_uninstall_removes_templates(env):
|
|
||||||
_run_install(env)
|
|
||||||
result = _run_uninstall(env)
|
|
||||||
assert result.returncode == 0, result.stderr
|
|
||||||
for name in ("Letter.svg", "A4.svg"):
|
|
||||||
assert not (
|
|
||||||
env["config_dir"] / "templates" / name
|
|
||||||
).exists(), f"Template was not removed: {name}"
|
|
||||||
151
uninstall.py
151
uninstall.py
|
|
@ -1,151 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Illuscape uninstaller — cross-platform (Linux, Windows, macOS).
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python3 uninstall.py
|
|
||||||
./uninstall.sh (Linux / macOS)
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# All files installed by install.py, keyed by subdirectory
|
|
||||||
_INSTALLED_FILES: dict[str, list[str]] = {
|
|
||||||
"keys": [
|
|
||||||
"illustrator-cc.xml",
|
|
||||||
"illustrator-cs6.xml",
|
|
||||||
],
|
|
||||||
"palettes": [
|
|
||||||
"Illustrator-Defaults.gpl",
|
|
||||||
"Illustrator-Grays.gpl",
|
|
||||||
"Illustrator-Earth.gpl",
|
|
||||||
],
|
|
||||||
"templates": [
|
|
||||||
"Letter.svg",
|
|
||||||
"A4.svg",
|
|
||||||
"Web-1920x1080.svg",
|
|
||||||
"Web-1280x720.svg",
|
|
||||||
"Print-CMYK-Letter.svg",
|
|
||||||
"Print-CMYK-A4.svg",
|
|
||||||
],
|
|
||||||
"symbols": [
|
|
||||||
"illuscape-common.svg",
|
|
||||||
],
|
|
||||||
"splashscreens": [
|
|
||||||
"illuscape-splash.png",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ── Backup discovery ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def find_backup(config_path: Path) -> Path | None:
|
|
||||||
"""Return the most recent illuscape backup directory, or None."""
|
|
||||||
backups = sorted(config_path.parent.glob(config_path.name + ".bak-illuscape-*"))
|
|
||||||
return backups[-1] if backups else None
|
|
||||||
|
|
||||||
|
|
||||||
# ── Restore ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def restore_prefs(backup: Path, config_path: Path) -> None:
|
|
||||||
"""Copy preferences.xml back from the backup directory."""
|
|
||||||
src = backup / "preferences.xml"
|
|
||||||
dst = config_path / "preferences.xml"
|
|
||||||
if src.exists():
|
|
||||||
shutil.copy2(src, dst)
|
|
||||||
print(f"→ preferences.xml restored from: {backup}")
|
|
||||||
else:
|
|
||||||
print("⚠ No preferences.xml in backup — skipping restore")
|
|
||||||
|
|
||||||
|
|
||||||
# ── File removal ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def remove_files(config_path: Path) -> None:
|
|
||||||
"""Delete all files that install.py placed in the Inkscape config dir."""
|
|
||||||
for subdir, names in _INSTALLED_FILES.items():
|
|
||||||
for name in names:
|
|
||||||
(config_path / subdir / name).unlink(missing_ok=True)
|
|
||||||
(config_path / "illuscape.ico").unlink(missing_ok=True)
|
|
||||||
print("→ Illuscape config files removed")
|
|
||||||
|
|
||||||
|
|
||||||
# ── Desktop cleanup ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def remove_desktop(install_method: str) -> None:
|
|
||||||
"""Remove the desktop launcher / Start Menu shortcut for the current platform."""
|
|
||||||
system = platform.system()
|
|
||||||
if system == "Linux" and install_method == "native":
|
|
||||||
_remove_linux_desktop()
|
|
||||||
elif system == "Windows":
|
|
||||||
_remove_windows_desktop()
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_linux_desktop() -> None:
|
|
||||||
lnk = Path.home() / ".local/share/applications/org.inkscape.Inkscape.desktop"
|
|
||||||
lnk.unlink(missing_ok=True)
|
|
||||||
for size in ICON_SIZES:
|
|
||||||
icon = (
|
|
||||||
Path.home()
|
|
||||||
/ f".local/share/icons/hicolor/{size}x{size}/apps/illuscape.png"
|
|
||||||
)
|
|
||||||
icon.unlink(missing_ok=True)
|
|
||||||
app_dir = Path.home() / ".local/share/applications"
|
|
||||||
subprocess.run(["update-desktop-database", str(app_dir)], capture_output=True)
|
|
||||||
print("→ Desktop launcher removed")
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_windows_desktop() -> None:
|
|
||||||
lnk = (
|
|
||||||
Path(os.environ.get("APPDATA", ""))
|
|
||||||
/ "Microsoft/Windows/Start Menu/Programs/Illuscape.lnk"
|
|
||||||
)
|
|
||||||
lnk.unlink(missing_ok=True)
|
|
||||||
print("→ Start Menu shortcut removed")
|
|
||||||
|
|
||||||
|
|
||||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
print("╔══════════════════════════════╗")
|
|
||||||
print("║ Illuscape Uninstaller ║")
|
|
||||||
print("╚══════════════════════════════╝")
|
|
||||||
print()
|
|
||||||
|
|
||||||
config_path, install_method = detect_config()
|
|
||||||
print(f"→ Inkscape config: {config_path} ({install_method})")
|
|
||||||
|
|
||||||
backup = find_backup(config_path)
|
|
||||||
if backup is None:
|
|
||||||
print(f"⚠ No Illuscape backup found at {config_path}.bak-illuscape-*")
|
|
||||||
print(" Cannot restore original preferences.xml automatically.")
|
|
||||||
print(" Removing only Illuscape-installed files.")
|
|
||||||
else:
|
|
||||||
restore_prefs(backup, config_path)
|
|
||||||
|
|
||||||
remove_files(config_path)
|
|
||||||
remove_desktop(install_method)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("✓ Illuscape uninstalled.")
|
|
||||||
print()
|
|
||||||
print(" Note: Your Inkscape preferences may have changed since Illuscape was")
|
|
||||||
print(" installed. If anything looks wrong, review:")
|
|
||||||
print(f" {config_path}/preferences.xml")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
74
uninstall.sh
74
uninstall.sh
|
|
@ -1,6 +1,74 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Illuscape uninstaller — thin wrapper around uninstall.py
|
# Illuscape uninstaller
|
||||||
# Usage: ./uninstall.sh
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
exec python3 "$SCRIPT_DIR/uninstall.py"
|
|
||||||
|
source "$SCRIPT_DIR/scripts/detect_platform.sh"
|
||||||
|
|
||||||
|
echo "╔══════════════════════════════╗"
|
||||||
|
echo "║ Illuscape Uninstaller ║"
|
||||||
|
echo "╚══════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
echo "→ Inkscape config: $INKSCAPE_CONFIG"
|
||||||
|
|
||||||
|
# ── Find backup ───────────────────────────────────────────────────────────────
|
||||||
|
BACKUP=$(ls -1d "${INKSCAPE_CONFIG}".bak-illuscape-* 2>/dev/null | sort | tail -1 || true)
|
||||||
|
|
||||||
|
if [[ -z "$BACKUP" ]]; then
|
||||||
|
echo "⚠ No Illuscape backup found at ${INKSCAPE_CONFIG}.bak-illuscape-*"
|
||||||
|
echo " Cannot restore original preferences.xml automatically."
|
||||||
|
echo " Removing only Illuscape-installed files."
|
||||||
|
else
|
||||||
|
echo "→ Restoring preferences.xml from: $BACKUP"
|
||||||
|
cp "$BACKUP/preferences.xml" "$INKSCAPE_CONFIG/preferences.xml" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Remove Illuscape-installed files ─────────────────────────────────────────
|
||||||
|
remove_files() {
|
||||||
|
# Keys
|
||||||
|
rm -f "$INKSCAPE_CONFIG/keys/illustrator-cc.xml"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/keys/illustrator-cs6.xml"
|
||||||
|
|
||||||
|
# Palettes
|
||||||
|
rm -f "$INKSCAPE_CONFIG/palettes/Illustrator-Defaults.gpl"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/palettes/Illustrator-Grays.gpl"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/palettes/Illustrator-Earth.gpl"
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
rm -f "$INKSCAPE_CONFIG/templates/Letter.svg"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/templates/A4.svg"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/templates/Web-1920x1080.svg"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/templates/Web-1280x720.svg"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/templates/Print-CMYK-Letter.svg"
|
||||||
|
rm -f "$INKSCAPE_CONFIG/templates/Print-CMYK-A4.svg"
|
||||||
|
|
||||||
|
# Symbols
|
||||||
|
rm -f "$INKSCAPE_CONFIG/symbols/illuscape-common.svg"
|
||||||
|
|
||||||
|
# Splash
|
||||||
|
rm -f "$INKSCAPE_CONFIG/splashscreens/illuscape-splash.png"
|
||||||
|
|
||||||
|
echo "→ Illuscape config files removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_desktop() {
|
||||||
|
[[ "$INKSCAPE_INSTALL_METHOD" != "native" ]] && return 0
|
||||||
|
rm -f "$HOME/.local/share/applications/org.inkscape.Inkscape.desktop"
|
||||||
|
for size in 16 32 48 64 128 256 512; do
|
||||||
|
rm -f "$HOME/.local/share/icons/hicolor/${size}x${size}/apps/illuscape.png"
|
||||||
|
done
|
||||||
|
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true
|
||||||
|
echo "→ Desktop launcher removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_files
|
||||||
|
remove_desktop
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Illuscape uninstalled."
|
||||||
|
echo ""
|
||||||
|
echo " Note: Your Inkscape preferences may have changed since Illuscape was"
|
||||||
|
echo " installed. If anything looks wrong, review:"
|
||||||
|
echo " $INKSCAPE_CONFIG/preferences.xml"
|
||||||
|
echo ""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue