diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..52f4ee5 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,54 @@ +name: Build Windows + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-build.txt + python scripts\make_icon.py + + - name: Build EXE (PyInstaller) + run: | + python -m PyInstaller --noconfirm --clean --onefile --windowed --name PulseGate --icon assets\pulsegate.ico desktop\pulsegate_desktop.py + Get-Item dist\PulseGate.exe | Select-Object FullName, Length + + - name: Install Inno Setup + shell: bash + run: | + choco install innosetup --no-progress --yes + + - name: Build installer + run: iscc installer\installer.iss + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: PulseGate-Windows + path: | + dist/PulseGate.exe + installer/output/*.exe + + - name: Release + if: startsWith(github.ref, 'refs/tags/') + uses: gitea/actions/release-action@v1 + with: + files: | + dist/PulseGate.exe + installer/output/*.exe \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3af272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.py[cod] +*.spec +build/ +dist/ +installer/output/ +.venv/ +venv/ +.env \ No newline at end of file diff --git a/README.md b/README.md index 7c9b447..e55e310 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # PulseGate Desktop -PulseGate Desktop ist eine grafische Desktop-Variante des PulseGate SSH Managers. Die App verwaltet SSH-Server, nutzt dieselbe YAML-Konfiguration wie die bestehende TUI und startet Verbindungen in einem lokalen Terminal-Emulator. +PulseGate Desktop ist eine grafische Desktop-App zur Verwaltung von SSH-Servern. Die App nutzt eine YAML-Konfiguration, listet und verwaltet Server, zeigt SSH-Befehle an und startet Verbindungen im lokalen Terminal-Emulator. + +Läuft nativ auf **Linux** und **Windows**. Für Windows liegt ein fertiger Installer-Build (PyInstaller + Inno Setup) bei. ## Features @@ -15,29 +17,35 @@ PulseGate Desktop ist eine grafische Desktop-Variante des PulseGate SSH Managers ## Voraussetzungen -- Linux Desktop-Umgebung +### Linux - Python 3 mit `tkinter` - Python-Paket `PyYAML` - `ssh` - ein unterstützter Terminal-Emulator: - - `kitty` - - `alacritty` - - `konsole` - - `gnome-terminal` - - `xfce4-terminal` - - `xterm` + - `kitty`, `alacritty`, `konsole`, `gnome-terminal`, `xfce4-terminal` oder `xterm` -Für die optionale Web-Variante wird zusätzlich Go benötigt. +### Windows +- Windows 10/11 (OpenSSH-Client ist standardmäßig integriert) +- Optionale Terminal-Emulatoren (werden in dieser Reihenfolge erkannt): + - `wt` (Windows Terminal) + - `cmd` (Fallback, immer verfügbar) + - `powershell` +- Für den Installer-Build zusätzlich: Python 3 + Inno Setup 6 ## Start Desktop-App starten: ```bash -cd /home/pascal/Projekte/PulseGate-GUI ./pulsegate-desktop ``` +Direkt mit Python: + +```bash +python3 desktop/pulsegate_desktop.py +``` + Optional kann ein eigener Config-Pfad übergeben werden: ```bash @@ -48,9 +56,8 @@ Optional kann ein eigener Config-Pfad übergeben werden: Standardmäßig liest und schreibt PulseGate Desktop diese Datei: -```text -~/.config/pulsegate/config.yaml -``` +- Linux: `~/.config/pulsegate/config.yaml` +- Windows: `%APPDATA%\pulsegate\config.yaml` Beispiel: @@ -77,31 +84,56 @@ quick_commands: command: df -h ``` +## Windows-Build und Installer + +Auf einem Windows-Rechner: + +1. Build der EXE (erzeugt `dist\PulseGate.exe`): + + ```bat + build_windows.bat + ``` + +2. Installer kompilieren – Inno Setup 6 öffnen mit: + + ```text + installer\installer.iss + ``` + + oder per Kommandozeile: + + ```bat + iscc installer\installer.iss + ``` + + Ergebnis: `installer\output\PulseGate-Setup-1.0.0.exe` + +Alternativ wird bei einem Tag `v*` (z. B. `git tag v1.0.0 && git push --tags`) automatisch per Gitea Actions ein Windows-Build inklusive Installer erstellt. + ## Web-Variante -Im Projekt liegt zusätzlich noch eine einfache lokale Web-Variante: +Im Projekt liegt zusätzlich eine optionale lokale Web-Variante: ```bash go run ./cmd/pulsegate-gui ``` -Danach öffnen: - -```text -http://127.0.0.1:8090 -``` +Danach öffnen: `http://127.0.0.1:8090` ## Projektstruktur ```text . -├── cmd/pulsegate-gui/ # optionaler Go-Webserver -├── desktop/ # Python/Tk Desktop-App -├── internal/config/ # Go-Konfigurationslogik -├── internal/models/ # Go-Datenmodelle +├── cmd/pulsegate-gui/ # optionale Go-Webvariante +├── desktop/ # Python/Tk Desktop-App (Linux + Windows) +├── installer/ # Inno Setup Installer-Skript +├── assets/ # App-Icon +├── scripts/ # Icon-Generierung ├── web/ # optionale Web-Oberfläche -├── go.mod -├── pulsegate-desktop # Launcher für die Desktop-App +├── build_windows.bat # Windows-EXE-Build (PyInstaller) +├── requirements.txt # Laufzeit-Abhängigkeiten +├── requirements-build.txt # Build-Abhängigkeiten +├── pulsegate-desktop # Linux-Launcher └── README.md ``` @@ -121,4 +153,4 @@ go test ./... ## Hinweis -Die Desktop-App startet SSH in einem separaten lokalen Terminal-Fenster. Passwortabfragen und interaktive SSH-Sessions laufen dort direkt im Terminal. +Die Desktop-App startet SSH in einem separaten lokalen Terminal-Fenster. Passwortabfragen und interaktive SSH-Sessions laufen dort direkt im Terminal. \ No newline at end of file diff --git a/assets/pulsegate.ico b/assets/pulsegate.ico new file mode 100644 index 0000000..27350e2 Binary files /dev/null and b/assets/pulsegate.ico differ diff --git a/assets/pulsegate.png b/assets/pulsegate.png new file mode 100644 index 0000000..c45be96 Binary files /dev/null and b/assets/pulsegate.png differ diff --git a/build_windows.bat b/build_windows.bat new file mode 100644 index 0000000..1be6e44 --- /dev/null +++ b/build_windows.bat @@ -0,0 +1,40 @@ +@echo off +setlocal +REM ============================================================ +REM Build PulseGate Desktop als Windows-EXE (PyInstaller) +REM Inno Setup-Build danach: installer\installer.iss oeffnen +REM bzw. als Script-Kompilierung ausfuehren. +REM ============================================================ + +set "PY=%PYTHON%" +if "%PY%"=="" set "PY=py -3" +if "%PY%"=="" set "PY=python" + +echo [1/3] Installiere Build-Abhaengigkeiten ... +%PY% -m pip install --upgrade pip +%PY% -m pip install -r requirements-build.txt +if errorlevel 1 goto :error + +echo [2/3] Generiere Icon-Assets ... +%PY% scripts\make_icon.py +if errorlevel 1 goto :error + +echo [3/3] Baue EXE ... +%PY% -m PyInstaller ^ + --noconfirm --clean ^ + --onefile ^ + --windowed ^ + --name PulseGate ^ + --icon assets\pulsegate.ico ^ + desktop\pulsegate_desktop.py +if errorlevel 1 goto :error + +echo. +echo Fertig! EXE liegt unter dist\PulseGate.exe +echo Installer: installer\installer.iss mit Inno Setup kompilieren. +exit /b 0 + +:error +echo. +echo Build fehlgeschlagen! +exit /b 1 \ No newline at end of file diff --git a/desktop/__pycache__/pulsegate_desktop.cpython-314.pyc b/desktop/__pycache__/pulsegate_desktop.cpython-314.pyc deleted file mode 100644 index f0f54bd..0000000 Binary files a/desktop/__pycache__/pulsegate_desktop.cpython-314.pyc and /dev/null differ diff --git a/desktop/pulsegate_desktop.py b/desktop/pulsegate_desktop.py index 5c1d6d9..166893d 100644 --- a/desktop/pulsegate_desktop.py +++ b/desktop/pulsegate_desktop.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- from __future__ import annotations import os @@ -14,6 +15,8 @@ import yaml APP_TITLE = "PulseGate Desktop" +APP_VERSION = "1.0.0" +IS_WINDOWS = sys.platform == "win32" COLORS = { "bg": "#07110d", @@ -30,16 +33,25 @@ COLORS = { "danger": "#ff6666", } -FONT = ("Inter", 11) -FONT_SMALL = ("Inter", 9) -FONT_TITLE = ("Inter", 24, "bold") -FONT_CARD = ("Inter", 13, "bold") +UI_FONT = ("Segoe UI", 11) if IS_WINDOWS else ("Inter", 11) +UI_FONT_SMALL = ("Segoe UI", 9) if IS_WINDOWS else ("Inter", 9) +UI_FONT_MONO = ("Consolas", 10) if IS_WINDOWS else ("JetBrains Mono", 10) +FONT = UI_FONT +FONT_SMALL = UI_FONT_SMALL +FONT_TITLE = (UI_FONT[0], 24, "bold") +FONT_CARD = (UI_FONT[0], 13, "bold") def config_path() -> Path: if len(sys.argv) > 1: return Path(sys.argv[1]).expanduser() + if IS_WINDOWS: + appdata = os.environ.get("APPDATA") + if appdata: + return Path(appdata) / "pulsegate" / "config.yaml" + return Path.home() / "AppData" / "Roaming" / "pulsegate" / "config.yaml" + config_home = os.environ.get("XDG_CONFIG_HOME") if config_home: return Path(config_home) / "pulsegate" / "config.yaml" @@ -115,11 +127,20 @@ def save_config(path: Path, config: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8") as handle: yaml.safe_dump(normalize_config(config), handle, sort_keys=False, allow_unicode=True) - path.chmod(0o600) + try: + path.chmod(0o600) + except OSError: + pass def expand_home(value: str) -> str: - return str(Path(value).expanduser()) if value.startswith("~/") else value + if not (value.startswith("~/") or value.startswith("~\\")): + return value + if IS_WINDOWS: + profile = os.environ.get("USERPROFILE") + if profile: + return os.path.join(profile, value[2:].replace("/", "\\")) + return str(Path(value).expanduser()) def ssh_args(server: dict) -> list[str]: @@ -130,7 +151,21 @@ def ssh_args(server: dict) -> list[str]: return args +def display_command(server: dict) -> str: + args = ssh_args(server) + if IS_WINDOWS: + return subprocess.list2cmdline(args) + return " ".join(shlex.quote(arg) for arg in args) + + def detect_terminal() -> str | None: + if IS_WINDOWS: + for name in ("wt", "cmd", "powershell"): + path = shutil.which(name) + if path: + return path + return None + for name in ("kitty", "alacritty", "konsole", "gnome-terminal", "xfce4-terminal", "xterm"): path = shutil.which(name) if path: @@ -140,24 +175,35 @@ def detect_terminal() -> str | None: def terminal_command(terminal: str, server: dict) -> list[str]: title = f"PulseGate - {server.get('name') or server.get('host')}" - command = " ".join(shlex.quote(arg) for arg in ssh_args(server)) - hold_command = f"{command}; printf '\\nSSH beendet. Enter zum Schliessen...'; read _" + + if IS_WINDOWS: + params = subprocess.list2cmdline(ssh_args(server)) + stem = Path(terminal).stem.lower() + if stem in ("powershell", "pwsh"): + hold = f"& {params}; Write-Host ''; Write-Host 'SSH beendet.'; Read-Host 'Enter zum Schliessen...'" + return [terminal, "-NoExit", "-Command", hold] + hold = f"{params} & echo. & echo SSH beendet. Bitte Fenster schliessen... & pause" + if stem == "wt": + return [terminal, "new-tab", "--title", title, "cmd.exe", "/k", hold] + return [terminal, "/k", hold] + + hold = " ".join(shlex.quote(arg) for arg in ssh_args(server)) + "; printf '\\nSSH beendet. Enter zum Schließen...'; read _" + name = Path(terminal).name - if name == "kitty": - return [terminal, "--title", title, "sh", "-lc", hold_command] + return [terminal, "--title", title, "sh", "-lc", hold] if name == "alacritty": - return [terminal, "--title", title, "-e", "sh", "-lc", hold_command] + return [terminal, "--title", title, "-e", "sh", "-lc", hold] if name == "konsole": - return [terminal, "--new-tab", "-p", f"tabtitle={title}", "-e", "sh", "-lc", hold_command] + return [terminal, "--new-tab", "-p", f"tabtitle={title}", "-e", "sh", "-lc", hold] if name == "gnome-terminal": - return [terminal, "--title", title, "--", "sh", "-lc", hold_command] + return [terminal, "--title", title, "--", "sh", "-lc", hold] if name == "xfce4-terminal": - return [terminal, "--title", title, "--command", f"sh -lc {shlex.quote(hold_command)}"] + return [terminal, "--title", title, "--command", f"sh -lc {shlex.quote(hold)}"] if name == "xterm": - return [terminal, "-T", title, "-e", "sh", "-lc", hold_command] + return [terminal, "-T", title, "-e", "sh", "-lc", hold] - return [terminal, "-e", "sh", "-lc", hold_command] + return [terminal, "-e", "sh", "-lc", hold] class PulseGateDesktop(tk.Tk): @@ -204,7 +250,7 @@ class PulseGateDesktop(tk.Tk): text="PG", bg=COLORS["accent"], fg="#001a10", - font=("Inter", 14, "bold"), + font=(UI_FONT[0], 14, "bold"), width=4, height=2, ) @@ -212,7 +258,7 @@ class PulseGateDesktop(tk.Tk): brand_text = tk.Frame(brand, bg=COLORS["sidebar"]) brand_text.pack(side="left", padx=12) - tk.Label(brand_text, text="PulseGate", bg=COLORS["sidebar"], fg=COLORS["text"], font=("Inter", 18, "bold")).pack(anchor="w") + tk.Label(brand_text, text="PulseGate", bg=COLORS["sidebar"], fg=COLORS["text"], font=(UI_FONT[0], 18, "bold")).pack(anchor="w") tk.Label(brand_text, text="Desktop SSH", bg=COLORS["sidebar"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w") self._label(sidebar, "Suche", bg=COLORS["sidebar"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", padx=22) @@ -249,7 +295,7 @@ class PulseGateDesktop(tk.Tk): self.title_label.pack(anchor="w") self._button(topbar, "Neu laden", self._reload, variant="ghost").pack(side="right", padx=(8, 0)) - self._button(topbar, "Server hinzufuegen", self._add_server).pack(side="right") + self._button(topbar, "Server hinzufügen", self._add_server).pack(side="right") self.page_host = tk.Frame(content, bg=COLORS["bg"]) self.page_host.pack(fill="both", expand=True) @@ -282,7 +328,7 @@ class PulseGateDesktop(tk.Tk): header = tk.Frame(editor_outer, bg=COLORS["surface"]) header.pack(fill="x", pady=(0, 16)) - self.form_title = self._label(header, "Server bearbeiten", bg=COLORS["surface"], fg=COLORS["text"], font=("Inter", 18, "bold")) + self.form_title = self._label(header, "Server bearbeiten", bg=COLORS["surface"], fg=COLORS["text"], font=(UI_FONT[0], 18, "bold")) self.form_title.pack(side="left") self.auth_chip = self._chip(header, "key", COLORS["accent_2"]) self.auth_chip.pack(side="right") @@ -331,7 +377,7 @@ class PulseGateDesktop(tk.Tk): self.vars["kitty_fix"] = tk.BooleanVar(value=True) tk.Checkbutton( editor_outer, - text="Kitty Fix fuer diesen Server nutzen", + text="Kitty Fix für diesen Server nutzen", variable=self.vars["kitty_fix"], bg=COLORS["surface"], fg=COLORS["text"], @@ -345,7 +391,7 @@ class PulseGateDesktop(tk.Tk): actions.pack(fill="x", pady=(16, 14)) self._button(actions, "Verbinden", self._connect).pack(side="left") self._button(actions, "SSH Befehl", self._show_command, variant="ghost").pack(side="left", padx=(8, 0)) - self._button(actions, "Loeschen", self._delete_current_server, variant="danger").pack(side="right") + self._button(actions, "Löschen", self._delete_current_server, variant="danger").pack(side="right") self._button(actions, "Speichern", self._save_current_server).pack(side="right", padx=(0, 8)) self.command_text = tk.Text( @@ -356,7 +402,7 @@ class PulseGateDesktop(tk.Tk): insertbackground=COLORS["text"], relief="flat", wrap="word", - font=("JetBrains Mono", 10), + font=UI_FONT_MONO, padx=12, pady=10, ) @@ -370,7 +416,7 @@ class PulseGateDesktop(tk.Tk): panel = self._card(page, bg=COLORS["surface"], padx=22, pady=22) panel.pack(anchor="nw", fill="x") - self._label(panel, "Terminal", bg=COLORS["surface"], fg=COLORS["text"], font=("Inter", 18, "bold")).pack(anchor="w") + self._label(panel, "Terminal", bg=COLORS["surface"], fg=COLORS["text"], font=(UI_FONT[0], 18, "bold")).pack(anchor="w") self._label(panel, "Diese Werte werden beim Starten einer SSH-Verbindung genutzt.", bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", pady=(4, 18)) self.settings_vars["theme"] = tk.StringVar() @@ -461,7 +507,7 @@ class PulseGateDesktop(tk.Tk): card = self._card(self.commands_host, bg=COLORS["surface"], padx=16, pady=14) card.pack(fill="x", pady=(0, 10)) self._label(card, command.get("name") or "Command", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(anchor="w") - self._label(card, command.get("command") or "", bg=COLORS["surface"], fg=COLORS["accent"], font=("JetBrains Mono", 10)).pack(anchor="w", pady=(7, 0)) + self._label(card, command.get("command") or "", bg=COLORS["surface"], fg=COLORS["accent"], font=UI_FONT_MONO).pack(anchor="w", pady=(7, 0)) def _refresh_settings(self) -> None: settings = self.config_data["settings"] @@ -567,12 +613,12 @@ class PulseGateDesktop(tk.Tk): if not self.config_data["servers"]: return server = self.config_data["servers"][self.selected_index] - if not messagebox.askyesno(APP_TITLE, f"{server.get('name')} wirklich loeschen?"): + if not messagebox.askyesno(APP_TITLE, f"{server.get('name')} wirklich löschen?"): return del self.config_data["servers"][self.selected_index] self.selected_index = max(0, self.selected_index - 1) save_config(self.path, self.config_data) - self.status_var.set("Server geloescht") + self.status_var.set("Server gelöscht") self._refresh_all() def _save_settings(self) -> None: @@ -594,7 +640,7 @@ class PulseGateDesktop(tk.Tk): def _current_server(self) -> dict | None: if not self.config_data["servers"]: - messagebox.showerror(APP_TITLE, "Kein Server ausgewaehlt.") + messagebox.showerror(APP_TITLE, "Kein Server ausgewählt.") return None return self.config_data["servers"][self.selected_index] @@ -603,21 +649,22 @@ class PulseGateDesktop(tk.Tk): if not server: return self.command_text.delete("1.0", "end") - self.command_text.insert("1.0", " ".join(shlex.quote(arg) for arg in ssh_args(server))) + self.command_text.insert("1.0", display_command(server)) def _connect(self) -> None: server = self._current_server() if not server: return if not self.terminal: - messagebox.showerror(APP_TITLE, "Kein unterstuetzter Terminal-Emulator gefunden.") + messagebox.showerror(APP_TITLE, "Kein unterstützter Terminal-Emulator gefunden.") return env = os.environ.copy() - settings = self.config_data["settings"] - terminal_settings = settings.get("terminal", {}) - if terminal_settings.get("enable_kitty_fix", True) and server.get("kitty_fix", True): - env["TERM"] = terminal_settings.get("term") or "xterm-256color" + if not IS_WINDOWS: + settings = self.config_data["settings"] + terminal_settings = settings.get("terminal", {}) + if terminal_settings.get("enable_kitty_fix", True) and server.get("kitty_fix", True): + env["TERM"] = terminal_settings.get("term") or "xterm-256color" try: subprocess.Popen(terminal_command(self.terminal, server), env=env) @@ -674,7 +721,7 @@ class PulseGateDesktop(tk.Tk): bd=0, padx=14, pady=9, - font=("Inter", 10, "bold"), + font=(UI_FONT[0], 10, "bold"), cursor="hand2", ) @@ -692,7 +739,7 @@ class PulseGateDesktop(tk.Tk): padx=12, pady=10, anchor="w", - font=("Inter", 10, "bold"), + font=(UI_FONT[0], 10, "bold"), cursor="hand2", highlightthickness=1, highlightbackground=COLORS["sidebar"], @@ -722,4 +769,4 @@ class PulseGateDesktop(tk.Tk): if __name__ == "__main__": app = PulseGateDesktop(config_path()) - app.mainloop() + app.mainloop() \ No newline at end of file diff --git a/installer/installer.iss b/installer/installer.iss new file mode 100644 index 0000000..37a87a4 --- /dev/null +++ b/installer/installer.iss @@ -0,0 +1,50 @@ +; Inno Setup Script fuer PulseGate Desktop +; Kompilieren: iscc installer\installer.iss (Inno Setup 6) + +#define MyAppName "PulseGate Desktop" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "Pepe44DEV" +#define MyAppURL "https://git.pepe44.dev/Pepe44DEV/PulseGate-GUI" +#define MyAppExeName "PulseGate.exe" +#define MyAppAssocName MyAppName + " Config" + +[Setup] +AppId={{8AF1A0C9-4B5D-4E11-9C0A-3B2E7F4D1A55} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\PulseGate +DefaultGroupName=PulseGate +DisableProgramGroupPage=yes +OutputDir=output +OutputBaseFilename=PulseGate-Setup-{#MyAppVersion} +SetupIconFile=..\assets\pulsegate.ico +UninstallDisplayIcon={app}\{#MyAppExeName} +Compression=lzma2 +SolidCompression=yes +WizardStyle=modern +ArchitecturesInstallIn64BitMode=x64compatible +PrivilegesRequiredOverridesAllowed=dialog +AllowNoIcons=yes + +[Languages] +Name: "german"; MessagesFile: "compiler:Languages\German.isl" +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "..\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{#MyAppName} Konfiguration oeffnen"; Filename: "%APPDATA%\pulsegate\config.yaml"; Flags: runminimized; Comment: "PulseGate-Konfigurationsdatei" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent \ No newline at end of file diff --git a/requirements-build.txt b/requirements-build.txt new file mode 100644 index 0000000..a1799b4 --- /dev/null +++ b/requirements-build.txt @@ -0,0 +1,2 @@ +PyYAML>=6.0 +pyinstaller>=6.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..043876c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyYAML>=6.0 \ No newline at end of file diff --git a/scripts/make_icon.py b/scripts/make_icon.py new file mode 100644 index 0000000..1594363 --- /dev/null +++ b/scripts/make_icon.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Generiert das PulseGate-Icon als .ico (und .png) ohne externe Abhängigkeiten.""" +from __future__ import annotations + +import struct +import sys +import zlib +from pathlib import Path + +ACCENT = (0x00, 0xFF, 0x99) +SURFACE = (0x0D, 0x1B, 0x15) +BG = (0x07, 0x11, 0x0D) + +ROOT = Path(__file__).resolve().parent.parent +OUT_DIR = ROOT / "assets" +SIZES = [16, 32, 48, 64, 128, 256] + + +def pixel(size: int, x: int, y: int) -> tuple[int, int, int, int]: + nx = (x + 0.5) / size + ny = (y + 0.5) / size + + if nx < 0.08 or nx > 0.92 or ny < 0.08 or ny > 0.92: + return (*ACCENT, 255) + + if 0.34 < nx < 0.66 and 0.34 < ny < 0.66: + return (*ACCENT, 255) + + if 0.24 < nx < 0.76 and 0.24 < ny < 0.76: + return (*SURFACE, 255) + + return (*BG, 255) + + +def bmp_data(size: int) -> bytes: + header = struct.pack(" None: + blocks = [bmp_data(size) for size in SIZES] + count = len(SIZES) + total_size = 6 + 16 * count + sum(len(b) for b in blocks) + + out = bytearray(struct.pack(" None: + raw = bytearray() + for y in range(size): + raw += b"\x00" + for x in range(size): + raw += bytes(pixel(size, x, y)) + ihdr = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0) + chunks = [] + for chunk_type, data in ((b"IHDR", ihdr), (b"IDAT", zlib.compress(bytes(raw), 9)), (b"IEND", b"")): + crc = zlib.crc32(chunk_type + data) & 0xFFFFFFFF + chunks.append(struct.pack(">I", len(data)) + chunk_type + data + struct.pack(">I", crc)) + path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"".join(chunks)) + + +def main() -> int: + OUT_DIR.mkdir(parents=True, exist_ok=True) + write_ico(OUT_DIR / "pulsegate.ico") + write_png(OUT_DIR / "pulsegate.png", 256) + print(f"Assets erzeugt in {OUT_DIR}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file