commit 66a5179c83b42901904dc90abccfb0643dca909a Author: Void Date: Sat Jan 3 15:47:49 2026 +0800 initialize project diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ee9cc4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# MacOS Setup Wizard + +A simple setup wizard helping me setup my laptop correctly and fast. \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a070056 --- /dev/null +++ b/main.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +""" +macOS Setup Wizard - Automatisierte Installation und Konfiguration +Erstellt Verzeichnisse und installiert Apps aus einer JSON-Repository-Liste +""" + +import os +import sys +import json +import subprocess +import urllib.request +from pathlib import Path +from typing import List, Dict + +class Colors: + """ANSI Farbcodes für Terminal-Ausgabe""" + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + END = '\033[0m' + BOLD = '\033[1m' + +class MacSetupWizard: + def __init__(self): + self.directories: List[str] = [ + '~/Development', + '~/Development/Projects', + '~/Development/Tools', + '~/Documents/Work' + ] + self.apps: List[Dict[str, str]] = [] + self.selected_apps: List[str] = [] + + def clear_screen(self): + """Terminal-Bildschirm leeren""" + os.system('clear' if os.name != 'nt' else 'cls') + + def print_header(self, text: str): + """Formatierte Überschrift ausgeben""" + print(f"\n{Colors.BOLD}{Colors.CYAN}{'='*60}{Colors.END}") + print(f"{Colors.BOLD}{Colors.CYAN}{text.center(60)}{Colors.END}") + print(f"{Colors.BOLD}{Colors.CYAN}{'='*60}{Colors.END}\n") + + def print_step(self, current: int, total: int, title: str): + """Aktuellen Schritt anzeigen""" + print(f"{Colors.BLUE}[Schritt {current}/{total}]{Colors.END} {Colors.BOLD}{title}{Colors.END}\n") + + def step_directories(self): + """Schritt 1: Verzeichnisse verwalten und erstellen""" + self.clear_screen() + self.print_header("macOS Setup Wizard") + self.print_step(1, 4, "Verzeichnisse erstellen") + + while True: + print(f"{Colors.YELLOW}Aktuelle Verzeichnisse:{Colors.END}") + for idx, directory in enumerate(self.directories, 1): + expanded_path = os.path.expanduser(directory) + exists = "✓" if os.path.exists(expanded_path) else "○" + print(f" {idx}. {exists} {directory}") + + print(f"\n{Colors.CYAN}Optionen:{Colors.END}") + print(" [a] Verzeichnis hinzufügen") + print(" [r] Verzeichnis entfernen") + print(" [c] Verzeichnisse erstellen und weiter") + print(" [q] Beenden") + + choice = input(f"\n{Colors.GREEN}Auswahl:{Colors.END} ").strip().lower() + + if choice == 'a': + new_dir = input(f"{Colors.GREEN}Neuer Pfad (z.B. ~/Projects):{Colors.END} ").strip() + if new_dir: + self.directories.append(new_dir) + print(f"{Colors.GREEN}✓ Verzeichnis hinzugefügt{Colors.END}") + + elif choice == 'r': + try: + idx = int(input(f"{Colors.YELLOW}Nummer zum Entfernen:{Colors.END} ")) - 1 + if 0 <= idx < len(self.directories): + removed = self.directories.pop(idx) + print(f"{Colors.GREEN}✓ '{removed}' entfernt{Colors.END}") + else: + print(f"{Colors.RED}✗ Ungültige Nummer{Colors.END}") + except ValueError: + print(f"{Colors.RED}✗ Bitte eine Zahl eingeben{Colors.END}") + + elif choice == 'c': + self.create_directories() + input(f"\n{Colors.GREEN}Enter drücken um fortzufahren...{Colors.END}") + break + + elif choice == 'q': + print(f"\n{Colors.YELLOW}Setup abgebrochen.{Colors.END}") + sys.exit(0) + + self.clear_screen() + self.print_header("macOS Setup Wizard") + self.print_step(1, 4, "Verzeichnisse erstellen") + + def create_directories(self): + """Verzeichnisse tatsächlich erstellen""" + print(f"\n{Colors.YELLOW}Erstelle Verzeichnisse...{Colors.END}") + for directory in self.directories: + expanded_path = os.path.expanduser(directory) + try: + Path(expanded_path).mkdir(parents=True, exist_ok=True) + print(f"{Colors.GREEN}✓{Colors.END} {directory}") + except Exception as e: + print(f"{Colors.RED}✗{Colors.END} {directory} - Fehler: {e}") + + def step_load_apps(self): + """Schritt 2: Apps-Liste aus Repository laden""" + self.clear_screen() + self.print_header("macOS Setup Wizard") + self.print_step(2, 4, "Apps-Liste laden") + + default_url = "https://raw.githubusercontent.com/username/repo/main/apps.json" + + print(f"{Colors.YELLOW}Beispiel JSON-Format:{Colors.END}") + print(json.dumps({ + "apps": [ + { + "id": "vscode", + "name": "Visual Studio Code", + "url": "https://code.visualstudio.com/download", + "type": "dmg", + "install_command": "brew install --cask visual-studio-code" + } + ] + }, indent=2)) + + print(f"\n{Colors.CYAN}Repository-URL:{Colors.END}") + repo_url = input(f"[{default_url}]: ").strip() or default_url + + try: + print(f"\n{Colors.YELLOW}Lade Apps-Liste...{Colors.END}") + with urllib.request.urlopen(repo_url) as response: + data = json.loads(response.read().decode()) + self.apps = data.get('apps', []) + print(f"{Colors.GREEN}✓ {len(self.apps)} Apps geladen{Colors.END}") + except Exception as e: + print(f"{Colors.RED}✗ Fehler beim Laden: {e}{Colors.END}") + print(f"{Colors.YELLOW}Verwende Demo-Daten...{Colors.END}") + self.apps = self.get_demo_apps() + + input(f"\n{Colors.GREEN}Enter drücken um fortzufahren...{Colors.END}") + + def get_demo_apps(self) -> List[Dict[str, str]]: + """Demo-Apps für Testzwecke""" + return [ + { + "id": "homebrew", + "name": "Homebrew", + "url": "https://brew.sh", + "type": "script", + "install_command": '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + }, + { + "id": "vscode", + "name": "Visual Studio Code", + "url": "https://code.visualstudio.com", + "type": "brew", + "install_command": "brew install --cask visual-studio-code" + }, + { + "id": "git", + "name": "Git", + "url": "https://git-scm.com", + "type": "brew", + "install_command": "brew install git" + }, + { + "id": "docker", + "name": "Docker Desktop", + "url": "https://www.docker.com/products/docker-desktop", + "type": "brew", + "install_command": "brew install --cask docker" + }, + { + "id": "iterm2", + "name": "iTerm2", + "url": "https://iterm2.com", + "type": "brew", + "install_command": "brew install --cask iterm2" + } + ] + + def step_select_apps(self): + """Schritt 3: Apps zur Installation auswählen""" + self.clear_screen() + self.print_header("macOS Setup Wizard") + self.print_step(3, 4, "Apps auswählen") + + while True: + print(f"{Colors.YELLOW}Verfügbare Apps:{Colors.END}") + for idx, app in enumerate(self.apps, 1): + selected = "✓" if app['id'] in self.selected_apps else "○" + print(f" {idx}. [{selected}] {app['name']} ({app['type']})") + + print(f"\n{Colors.CYAN}Optionen:{Colors.END}") + print(" [Nummer] App auswählen/abwählen") + print(" [a] Alle auswählen") + print(" [n] Keine auswählen") + print(" [i] Info zu einer App") + print(" [c] Weiter zur Installation") + print(" [q] Beenden") + + choice = input(f"\n{Colors.GREEN}Auswahl:{Colors.END} ").strip().lower() + + if choice == 'a': + self.selected_apps = [app['id'] for app in self.apps] + print(f"{Colors.GREEN}✓ Alle Apps ausgewählt{Colors.END}") + + elif choice == 'n': + self.selected_apps = [] + print(f"{Colors.YELLOW}○ Alle Apps abgewählt{Colors.END}") + + elif choice == 'i': + try: + idx = int(input(f"{Colors.YELLOW}App-Nummer für Info:{Colors.END} ")) - 1 + if 0 <= idx < len(self.apps): + app = self.apps[idx] + print(f"\n{Colors.CYAN}{'─'*50}{Colors.END}") + print(f"{Colors.BOLD}Name:{Colors.END} {app['name']}") + print(f"{Colors.BOLD}URL:{Colors.END} {app['url']}") + print(f"{Colors.BOLD}Typ:{Colors.END} {app['type']}") + print(f"{Colors.BOLD}Befehl:{Colors.END} {app.get('install_command', 'N/A')}") + print(f"{Colors.CYAN}{'─'*50}{Colors.END}") + except ValueError: + print(f"{Colors.RED}✗ Ungültige Eingabe{Colors.END}") + + elif choice == 'c': + if not self.selected_apps: + print(f"{Colors.RED}✗ Keine Apps ausgewählt!{Colors.END}") + else: + break + + elif choice == 'q': + print(f"\n{Colors.YELLOW}Setup abgebrochen.{Colors.END}") + sys.exit(0) + + else: + try: + idx = int(choice) - 1 + if 0 <= idx < len(self.apps): + app_id = self.apps[idx]['id'] + if app_id in self.selected_apps: + self.selected_apps.remove(app_id) + print(f"{Colors.YELLOW}○ App abgewählt{Colors.END}") + else: + self.selected_apps.append(app_id) + print(f"{Colors.GREEN}✓ App ausgewählt{Colors.END}") + except ValueError: + print(f"{Colors.RED}✗ Ungültige Eingabe{Colors.END}") + + input(f"\n{Colors.GREEN}Enter drücken um fortzufahren...{Colors.END}") + self.clear_screen() + self.print_header("macOS Setup Wizard") + self.print_step(3, 4, "Apps auswählen") + + def step_install_apps(self): + """Schritt 4: Ausgewählte Apps installieren""" + self.clear_screen() + self.print_header("macOS Setup Wizard") + self.print_step(4, 4, "Apps installieren") + + selected_app_objects = [app for app in self.apps if app['id'] in self.selected_apps] + + print(f"{Colors.YELLOW}Installation von {len(selected_app_objects)} Apps...{Colors.END}\n") + + for idx, app in enumerate(selected_app_objects, 1): + print(f"\n{Colors.CYAN}[{idx}/{len(selected_app_objects)}]{Colors.END} {Colors.BOLD}{app['name']}{Colors.END}") + + install_cmd = app.get('install_command') + if not install_cmd: + print(f"{Colors.YELLOW} ⚠ Kein Installationsbefehl definiert{Colors.END}") + print(f"{Colors.BLUE} → Öffne URL: {app['url']}{Colors.END}") + continue + + print(f"{Colors.BLUE} → Führe aus: {install_cmd}{Colors.END}") + + try: + # Installationsbefehl ausführen + result = subprocess.run( + install_cmd, + shell=True, + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode == 0: + print(f"{Colors.GREEN} ✓ Erfolgreich installiert{Colors.END}") + else: + print(f"{Colors.RED} ✗ Installation fehlgeschlagen{Colors.END}") + if result.stderr: + print(f"{Colors.RED} Fehler: {result.stderr[:200]}{Colors.END}") + + except subprocess.TimeoutExpired: + print(f"{Colors.RED} ✗ Timeout (>5 Minuten){Colors.END}") + except Exception as e: + print(f"{Colors.RED} ✗ Fehler: {e}{Colors.END}") + + print(f"\n{Colors.GREEN}{Colors.BOLD}Installation abgeschlossen!{Colors.END}") + + # Zusammenfassung + print(f"\n{Colors.CYAN}{'─'*60}{Colors.END}") + print(f"{Colors.BOLD}Zusammenfassung:{Colors.END}") + print(f" • {len(self.directories)} Verzeichnisse erstellt") + print(f" • {len(selected_app_objects)} Apps installiert") + print(f"{Colors.CYAN}{'─'*60}{Colors.END}\n") + + def run(self): + """Hauptablauf des Setup-Wizards""" + try: + self.step_directories() + self.step_load_apps() + self.step_select_apps() + self.step_install_apps() + + print(f"\n{Colors.GREEN}{Colors.BOLD}✓ Setup erfolgreich abgeschlossen!{Colors.END}\n") + + except KeyboardInterrupt: + print(f"\n\n{Colors.YELLOW}Setup durch Benutzer abgebrochen.{Colors.END}") + sys.exit(1) + except Exception as e: + print(f"\n{Colors.RED}Fehler: {e}{Colors.END}") + sys.exit(1) + +def main(): + """Hauptfunktion""" + # Prüfen ob macOS + if sys.platform != 'darwin': + print(f"{Colors.YELLOW}Warnung: Dieses Script ist für macOS optimiert.{Colors.END}") + response = input("Trotzdem fortfahren? (j/n): ") + if response.lower() != 'j': + sys.exit(0) + + wizard = MacSetupWizard() + wizard.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..86c7ecd --- /dev/null +++ b/poetry.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.5" +description = "Python graph (network) package" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597"}, + {file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"}, +] + +[[package]] +name = "macholib" +version = "1.16.4" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea"}, + {file = "macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, + {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, +] + +[[package]] +name = "pyinstaller" +version = "6.17.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.15,>=3.8" +files = [ + {file = "pyinstaller-6.17.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4e446b8030c6e5a2f712e3f82011ecf6c7ead86008357b0d23a0ec4bcde31dac"}, + {file = "pyinstaller-6.17.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa9fd87aaa28239c6f0d0210114029bd03f8cac316a90bab071a5092d7c85ad7"}, + {file = "pyinstaller-6.17.0-py3-none-manylinux2014_i686.whl", hash = "sha256:060b122e43e7c0b23e759a4153be34bd70914135ab955bb18a67181e0dca85a2"}, + {file = "pyinstaller-6.17.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cd213d1a545c97dfe4a3c40e8213ff7c5127fc115c49229f27a3fa541503444b"}, + {file = "pyinstaller-6.17.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:89c0d18ba8b62c6607abd8cf2299ae5ffa5c36d8c47f39608ce8c3f357f6099f"}, + {file = "pyinstaller-6.17.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2a147b83cdebb07855bd5a663600891550062373a2ca375c58eacead33741a27"}, + {file = "pyinstaller-6.17.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:f8cfbbfa6708e54fb936df6dd6eafaf133e84efb0d2fe25b91cfeefa793c4ca4"}, + {file = "pyinstaller-6.17.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:97f4c1942f7b4cd73f9e38b49cc8f5f8a6fbb44922cb60dd3073a189b77ee1ae"}, + {file = "pyinstaller-6.17.0-py3-none-win32.whl", hash = "sha256:ce0be227a037fd4be672226db709088565484f597d6b230bceec19850fdd4c85"}, + {file = "pyinstaller-6.17.0-py3-none-win_amd64.whl", hash = "sha256:b019940dbf7a01489d6b26f9fb97db74b504e0a757010f7ad078675befc85a82"}, + {file = "pyinstaller-6.17.0-py3-none-win_arm64.whl", hash = "sha256:3c92a335e338170df7e615f75279cfeea97ade89e6dd7694943c8c185460f7b7"}, + {file = "pyinstaller-6.17.0.tar.gz", hash = "sha256:be372bd911392b88277e510940ac32a5c2a6ce4b8d00a311c78fa443f4f27313"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2025.9" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +completion = ["argcomplete"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2025.11" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyinstaller_hooks_contrib-2025.11-py3-none-any.whl", hash = "sha256:777e163e2942474aa41a8e6d31ac1635292d63422c3646c176d584d04d971c34"}, + {file = "pyinstaller_hooks_contrib-2025.11.tar.gz", hash = "sha256:dfe18632e06655fa88d218e0d768fd753e1886465c12a6d4bce04f1aaeec917d"}, +] + +[package.dependencies] +packaging = ">=22.0" +setuptools = ">=42.0.0" + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.13" +content-hash = "8b9049a2e56b8d2d81d5dd0fa6f03a029c468e7b3dfac6eaf5ddfa1fcaceaedb" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e783ac2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "mac-setup" +version = "0.1.0" +description = "My setup wizard for MacOS" +authors = ["Void "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.13" +pyinstaller = { version = "*", python = "<3.15" } + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"