Compare commits
3 commits
main
...
feat/windo
| Author | SHA1 | Date | |
|---|---|---|---|
| 316b9bf606 | |||
| 7b2a2c498c | |||
| 8e245dfe61 |
10 changed files with 794 additions and 267 deletions
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
|
|
@ -7,10 +7,13 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-${{ matrix.ubuntu }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ubuntu: ["22.04", "24.04"]
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
- os: ubuntu-24.04
|
||||||
|
- os: windows-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -20,19 +23,25 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Install test deps
|
- name: Install test deps (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
pip install pytest
|
pip install pytest
|
||||||
sudo apt-get install -y bats shellcheck inkscape
|
sudo apt-get install -y shellcheck inkscape
|
||||||
|
|
||||||
- name: Python unit tests
|
- name: Install test deps (Windows)
|
||||||
run: pytest tests/test_merge_prefs.py -v
|
if: runner.os == 'Windows'
|
||||||
|
run: pip install pytest
|
||||||
|
|
||||||
- name: Shellcheck
|
- name: Shellcheck (Linux only)
|
||||||
|
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: Bats integration tests
|
- name: Python unit tests
|
||||||
run: bats tests/test_install.bats
|
run: pytest tests/test_merge_prefs.py -v
|
||||||
|
|
||||||
|
- name: Python integration tests
|
||||||
|
run: pytest tests/test_install.py -v
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,10 @@ 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).
|
||||||
|
|
|
||||||
BIN
assets/illuscape.ico
Normal file
BIN
assets/illuscape.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
306
install.py
Normal file
306
install.py
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
#!/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,185 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Illuscape installer
|
# Illuscape installer — thin wrapper around install.py
|
||||||
# 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)"
|
||||||
PRESET=""
|
exec python3 "$SCRIPT_DIR/install.py" "$@"
|
||||||
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
|
|
||||||
|
|
|
||||||
82
scripts/detect_platform.py
Normal file
82
scripts/detect_platform.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
INKSCAPE_CONFIG="$flatpak_config"
|
export INKSCAPE_CONFIG="$flatpak_config"
|
||||||
INKSCAPE_INSTALL_METHOD="flatpak"
|
export 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
|
||||||
INKSCAPE_CONFIG="$snap_config"
|
export INKSCAPE_CONFIG="$snap_config"
|
||||||
INKSCAPE_INSTALL_METHOD="snap"
|
export 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"
|
||||||
INKSCAPE_CONFIG="$xdg_config"
|
export INKSCAPE_CONFIG="$xdg_config"
|
||||||
INKSCAPE_INSTALL_METHOD="native"
|
export INKSCAPE_INSTALL_METHOD="native"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
222
tests/test_install.py
Normal file
222
tests/test_install.py
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
"""
|
||||||
|
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
Normal file
151
uninstall.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#!/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,74 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Illuscape uninstaller
|
# Illuscape uninstaller — thin wrapper around uninstall.py
|
||||||
|
# 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