From 3f20bc392c2ff33e2cd0d97ffd395f0c65640d31 Mon Sep 17 00:00:00 2001 From: Pepe44DEV Date: Sun, 6 Sep 2026 22:28:55 +0000 Subject: [PATCH] Windows-Support, Build-Skripte und Installer fuer PulseGate Desktop - Python-App cross-platform: %APPDATA%-Konfiguration, Terminal-Erkennung (wt/cmd/powershell), Windows-kompatibler SSH-Start, Segoe-UI-Fonts - App-Icon (ICO/PNG) mit Skript ohne externe Abhaengigkeiten - PyInstaller-Build (build_windows.bat) und Inno-Setup-Installer - Gitea-Actions-Workflow fuer automatischen Windows-Build - requirements.txt, .gitignore, README aktualisiert; __pycache__ entfernt --- .gitea/workflows/build.yml | 54 ++++++++ .gitignore | 9 ++ README.md | 84 ++++++++---- assets/pulsegate.ico | Bin 0 -> 358742 bytes assets/pulsegate.png | Bin 0 -> 1136 bytes build_windows.bat | 40 ++++++ .../pulsegate_desktop.cpython-314.pyc | Bin 53914 -> 0 bytes desktop/pulsegate_desktop.py | 121 ++++++++++++------ installer/installer.iss | 50 ++++++++ requirements-build.txt | 2 + requirements.txt | 1 + scripts/make_icon.py | 94 ++++++++++++++ 12 files changed, 392 insertions(+), 63 deletions(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitignore create mode 100644 assets/pulsegate.ico create mode 100644 assets/pulsegate.png create mode 100644 build_windows.bat delete mode 100644 desktop/__pycache__/pulsegate_desktop.cpython-314.pyc create mode 100644 installer/installer.iss create mode 100644 requirements-build.txt create mode 100644 requirements.txt create mode 100644 scripts/make_icon.py 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 0000000000000000000000000000000000000000..27350e2028e5a847c512b96ec94a3523626b6001 GIT binary patch literal 358742 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 F2>_q}T)Y4P literal 0 HcmV?d00001 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 f0f54bd62b1c672e6c6dc4b47d90051f94c892af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53914 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 UEbQ$*jXrIDz>9rYq`-jz0PVXzkpKVy 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