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
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.spec
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
installer/output/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# PulseGate Desktop
|
# 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
|
## Features
|
||||||
|
|
||||||
@@ -15,29 +17,35 @@ PulseGate Desktop ist eine grafische Desktop-Variante des PulseGate SSH Managers
|
|||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Linux Desktop-Umgebung
|
### Linux
|
||||||
- Python 3 mit `tkinter`
|
- Python 3 mit `tkinter`
|
||||||
- Python-Paket `PyYAML`
|
- Python-Paket `PyYAML`
|
||||||
- `ssh`
|
- `ssh`
|
||||||
- ein unterstützter Terminal-Emulator:
|
- ein unterstützter Terminal-Emulator:
|
||||||
- `kitty`
|
- `kitty`, `alacritty`, `konsole`, `gnome-terminal`, `xfce4-terminal` oder `xterm`
|
||||||
- `alacritty`
|
|
||||||
- `konsole`
|
|
||||||
- `gnome-terminal`
|
|
||||||
- `xfce4-terminal`
|
|
||||||
- `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
|
## Start
|
||||||
|
|
||||||
Desktop-App starten:
|
Desktop-App starten:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/pascal/Projekte/PulseGate-GUI
|
|
||||||
./pulsegate-desktop
|
./pulsegate-desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Direkt mit Python:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 desktop/pulsegate_desktop.py
|
||||||
|
```
|
||||||
|
|
||||||
Optional kann ein eigener Config-Pfad übergeben werden:
|
Optional kann ein eigener Config-Pfad übergeben werden:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -48,9 +56,8 @@ Optional kann ein eigener Config-Pfad übergeben werden:
|
|||||||
|
|
||||||
Standardmäßig liest und schreibt PulseGate Desktop diese Datei:
|
Standardmäßig liest und schreibt PulseGate Desktop diese Datei:
|
||||||
|
|
||||||
```text
|
- Linux: `~/.config/pulsegate/config.yaml`
|
||||||
~/.config/pulsegate/config.yaml
|
- Windows: `%APPDATA%\pulsegate\config.yaml`
|
||||||
```
|
|
||||||
|
|
||||||
Beispiel:
|
Beispiel:
|
||||||
|
|
||||||
@@ -77,31 +84,56 @@ quick_commands:
|
|||||||
command: df -h
|
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
|
## Web-Variante
|
||||||
|
|
||||||
Im Projekt liegt zusätzlich noch eine einfache lokale Web-Variante:
|
Im Projekt liegt zusätzlich eine optionale lokale Web-Variante:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run ./cmd/pulsegate-gui
|
go run ./cmd/pulsegate-gui
|
||||||
```
|
```
|
||||||
|
|
||||||
Danach öffnen:
|
Danach öffnen: `http://127.0.0.1:8090`
|
||||||
|
|
||||||
```text
|
|
||||||
http://127.0.0.1:8090
|
|
||||||
```
|
|
||||||
|
|
||||||
## Projektstruktur
|
## Projektstruktur
|
||||||
|
|
||||||
```text
|
```text
|
||||||
.
|
.
|
||||||
├── cmd/pulsegate-gui/ # optionaler Go-Webserver
|
├── cmd/pulsegate-gui/ # optionale Go-Webvariante
|
||||||
├── desktop/ # Python/Tk Desktop-App
|
├── desktop/ # Python/Tk Desktop-App (Linux + Windows)
|
||||||
├── internal/config/ # Go-Konfigurationslogik
|
├── installer/ # Inno Setup Installer-Skript
|
||||||
├── internal/models/ # Go-Datenmodelle
|
├── assets/ # App-Icon
|
||||||
|
├── scripts/ # Icon-Generierung
|
||||||
├── web/ # optionale Web-Oberfläche
|
├── web/ # optionale Web-Oberfläche
|
||||||
├── go.mod
|
├── build_windows.bat # Windows-EXE-Build (PyInstaller)
|
||||||
├── pulsegate-desktop # Launcher für die Desktop-App
|
├── requirements.txt # Laufzeit-Abhängigkeiten
|
||||||
|
├── requirements-build.txt # Build-Abhängigkeiten
|
||||||
|
├── pulsegate-desktop # Linux-Launcher
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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
|
||||||
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -14,6 +15,8 @@ import yaml
|
|||||||
|
|
||||||
|
|
||||||
APP_TITLE = "PulseGate Desktop"
|
APP_TITLE = "PulseGate Desktop"
|
||||||
|
APP_VERSION = "1.0.0"
|
||||||
|
IS_WINDOWS = sys.platform == "win32"
|
||||||
|
|
||||||
COLORS = {
|
COLORS = {
|
||||||
"bg": "#07110d",
|
"bg": "#07110d",
|
||||||
@@ -30,16 +33,25 @@ COLORS = {
|
|||||||
"danger": "#ff6666",
|
"danger": "#ff6666",
|
||||||
}
|
}
|
||||||
|
|
||||||
FONT = ("Inter", 11)
|
UI_FONT = ("Segoe UI", 11) if IS_WINDOWS else ("Inter", 11)
|
||||||
FONT_SMALL = ("Inter", 9)
|
UI_FONT_SMALL = ("Segoe UI", 9) if IS_WINDOWS else ("Inter", 9)
|
||||||
FONT_TITLE = ("Inter", 24, "bold")
|
UI_FONT_MONO = ("Consolas", 10) if IS_WINDOWS else ("JetBrains Mono", 10)
|
||||||
FONT_CARD = ("Inter", 13, "bold")
|
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:
|
def config_path() -> Path:
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
return Path(sys.argv[1]).expanduser()
|
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")
|
config_home = os.environ.get("XDG_CONFIG_HOME")
|
||||||
if config_home:
|
if config_home:
|
||||||
return Path(config_home) / "pulsegate" / "config.yaml"
|
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)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with path.open("w", encoding="utf-8") as handle:
|
with path.open("w", encoding="utf-8") as handle:
|
||||||
yaml.safe_dump(normalize_config(config), handle, sort_keys=False, allow_unicode=True)
|
yaml.safe_dump(normalize_config(config), handle, sort_keys=False, allow_unicode=True)
|
||||||
|
try:
|
||||||
path.chmod(0o600)
|
path.chmod(0o600)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def expand_home(value: str) -> str:
|
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]:
|
def ssh_args(server: dict) -> list[str]:
|
||||||
@@ -130,7 +151,21 @@ def ssh_args(server: dict) -> list[str]:
|
|||||||
return args
|
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:
|
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"):
|
for name in ("kitty", "alacritty", "konsole", "gnome-terminal", "xfce4-terminal", "xterm"):
|
||||||
path = shutil.which(name)
|
path = shutil.which(name)
|
||||||
if path:
|
if path:
|
||||||
@@ -140,24 +175,35 @@ def detect_terminal() -> str | None:
|
|||||||
|
|
||||||
def terminal_command(terminal: str, server: dict) -> list[str]:
|
def terminal_command(terminal: str, server: dict) -> list[str]:
|
||||||
title = f"PulseGate - {server.get('name') or server.get('host')}"
|
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
|
name = Path(terminal).name
|
||||||
|
|
||||||
if name == "kitty":
|
if name == "kitty":
|
||||||
return [terminal, "--title", title, "sh", "-lc", hold_command]
|
return [terminal, "--title", title, "sh", "-lc", hold]
|
||||||
if name == "alacritty":
|
if name == "alacritty":
|
||||||
return [terminal, "--title", title, "-e", "sh", "-lc", hold_command]
|
return [terminal, "--title", title, "-e", "sh", "-lc", hold]
|
||||||
if name == "konsole":
|
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":
|
if name == "gnome-terminal":
|
||||||
return [terminal, "--title", title, "--", "sh", "-lc", hold_command]
|
return [terminal, "--title", title, "--", "sh", "-lc", hold]
|
||||||
if name == "xfce4-terminal":
|
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":
|
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):
|
class PulseGateDesktop(tk.Tk):
|
||||||
@@ -204,7 +250,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
text="PG",
|
text="PG",
|
||||||
bg=COLORS["accent"],
|
bg=COLORS["accent"],
|
||||||
fg="#001a10",
|
fg="#001a10",
|
||||||
font=("Inter", 14, "bold"),
|
font=(UI_FONT[0], 14, "bold"),
|
||||||
width=4,
|
width=4,
|
||||||
height=2,
|
height=2,
|
||||||
)
|
)
|
||||||
@@ -212,7 +258,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
|
|
||||||
brand_text = tk.Frame(brand, bg=COLORS["sidebar"])
|
brand_text = tk.Frame(brand, bg=COLORS["sidebar"])
|
||||||
brand_text.pack(side="left", padx=12)
|
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")
|
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)
|
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.title_label.pack(anchor="w")
|
||||||
|
|
||||||
self._button(topbar, "Neu laden", self._reload, variant="ghost").pack(side="right", padx=(8, 0))
|
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 = tk.Frame(content, bg=COLORS["bg"])
|
||||||
self.page_host.pack(fill="both", expand=True)
|
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 = tk.Frame(editor_outer, bg=COLORS["surface"])
|
||||||
header.pack(fill="x", pady=(0, 16))
|
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.form_title.pack(side="left")
|
||||||
self.auth_chip = self._chip(header, "key", COLORS["accent_2"])
|
self.auth_chip = self._chip(header, "key", COLORS["accent_2"])
|
||||||
self.auth_chip.pack(side="right")
|
self.auth_chip.pack(side="right")
|
||||||
@@ -331,7 +377,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
self.vars["kitty_fix"] = tk.BooleanVar(value=True)
|
self.vars["kitty_fix"] = tk.BooleanVar(value=True)
|
||||||
tk.Checkbutton(
|
tk.Checkbutton(
|
||||||
editor_outer,
|
editor_outer,
|
||||||
text="Kitty Fix fuer diesen Server nutzen",
|
text="Kitty Fix für diesen Server nutzen",
|
||||||
variable=self.vars["kitty_fix"],
|
variable=self.vars["kitty_fix"],
|
||||||
bg=COLORS["surface"],
|
bg=COLORS["surface"],
|
||||||
fg=COLORS["text"],
|
fg=COLORS["text"],
|
||||||
@@ -345,7 +391,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
actions.pack(fill="x", pady=(16, 14))
|
actions.pack(fill="x", pady=(16, 14))
|
||||||
self._button(actions, "Verbinden", self._connect).pack(side="left")
|
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, "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._button(actions, "Speichern", self._save_current_server).pack(side="right", padx=(0, 8))
|
||||||
|
|
||||||
self.command_text = tk.Text(
|
self.command_text = tk.Text(
|
||||||
@@ -356,7 +402,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
insertbackground=COLORS["text"],
|
insertbackground=COLORS["text"],
|
||||||
relief="flat",
|
relief="flat",
|
||||||
wrap="word",
|
wrap="word",
|
||||||
font=("JetBrains Mono", 10),
|
font=UI_FONT_MONO,
|
||||||
padx=12,
|
padx=12,
|
||||||
pady=10,
|
pady=10,
|
||||||
)
|
)
|
||||||
@@ -370,7 +416,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
panel = self._card(page, bg=COLORS["surface"], padx=22, pady=22)
|
panel = self._card(page, bg=COLORS["surface"], padx=22, pady=22)
|
||||||
panel.pack(anchor="nw", fill="x")
|
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._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()
|
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 = self._card(self.commands_host, bg=COLORS["surface"], padx=16, pady=14)
|
||||||
card.pack(fill="x", pady=(0, 10))
|
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("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:
|
def _refresh_settings(self) -> None:
|
||||||
settings = self.config_data["settings"]
|
settings = self.config_data["settings"]
|
||||||
@@ -567,12 +613,12 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
if not self.config_data["servers"]:
|
if not self.config_data["servers"]:
|
||||||
return
|
return
|
||||||
server = self.config_data["servers"][self.selected_index]
|
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
|
return
|
||||||
del self.config_data["servers"][self.selected_index]
|
del self.config_data["servers"][self.selected_index]
|
||||||
self.selected_index = max(0, self.selected_index - 1)
|
self.selected_index = max(0, self.selected_index - 1)
|
||||||
save_config(self.path, self.config_data)
|
save_config(self.path, self.config_data)
|
||||||
self.status_var.set("Server geloescht")
|
self.status_var.set("Server gelöscht")
|
||||||
self._refresh_all()
|
self._refresh_all()
|
||||||
|
|
||||||
def _save_settings(self) -> None:
|
def _save_settings(self) -> None:
|
||||||
@@ -594,7 +640,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
|
|
||||||
def _current_server(self) -> dict | None:
|
def _current_server(self) -> dict | None:
|
||||||
if not self.config_data["servers"]:
|
if not self.config_data["servers"]:
|
||||||
messagebox.showerror(APP_TITLE, "Kein Server ausgewaehlt.")
|
messagebox.showerror(APP_TITLE, "Kein Server ausgewählt.")
|
||||||
return None
|
return None
|
||||||
return self.config_data["servers"][self.selected_index]
|
return self.config_data["servers"][self.selected_index]
|
||||||
|
|
||||||
@@ -603,17 +649,18 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
if not server:
|
if not server:
|
||||||
return
|
return
|
||||||
self.command_text.delete("1.0", "end")
|
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:
|
def _connect(self) -> None:
|
||||||
server = self._current_server()
|
server = self._current_server()
|
||||||
if not server:
|
if not server:
|
||||||
return
|
return
|
||||||
if not self.terminal:
|
if not self.terminal:
|
||||||
messagebox.showerror(APP_TITLE, "Kein unterstuetzter Terminal-Emulator gefunden.")
|
messagebox.showerror(APP_TITLE, "Kein unterstützter Terminal-Emulator gefunden.")
|
||||||
return
|
return
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
if not IS_WINDOWS:
|
||||||
settings = self.config_data["settings"]
|
settings = self.config_data["settings"]
|
||||||
terminal_settings = settings.get("terminal", {})
|
terminal_settings = settings.get("terminal", {})
|
||||||
if terminal_settings.get("enable_kitty_fix", True) and server.get("kitty_fix", True):
|
if terminal_settings.get("enable_kitty_fix", True) and server.get("kitty_fix", True):
|
||||||
@@ -674,7 +721,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
bd=0,
|
bd=0,
|
||||||
padx=14,
|
padx=14,
|
||||||
pady=9,
|
pady=9,
|
||||||
font=("Inter", 10, "bold"),
|
font=(UI_FONT[0], 10, "bold"),
|
||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -692,7 +739,7 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
padx=12,
|
padx=12,
|
||||||
pady=10,
|
pady=10,
|
||||||
anchor="w",
|
anchor="w",
|
||||||
font=("Inter", 10, "bold"),
|
font=(UI_FONT[0], 10, "bold"),
|
||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
highlightthickness=1,
|
highlightthickness=1,
|
||||||
highlightbackground=COLORS["sidebar"],
|
highlightbackground=COLORS["sidebar"],
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
PyYAML>=6.0
|
||||||
|
pyinstaller>=6.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PyYAML>=6.0
|
||||||
@@ -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("<IiiHHIIiiII", 40, size, size * 2, 1, 32, 0, size * size * 4, 0, 0, 0, 0)
|
||||||
|
xor = bytearray()
|
||||||
|
for y in range(size - 1, -1, -1):
|
||||||
|
for x in range(size):
|
||||||
|
r, g, b, a = pixel(size, x, y)
|
||||||
|
xor += bytes((b, g, r, a))
|
||||||
|
return header + bytes(xor)
|
||||||
|
|
||||||
|
|
||||||
|
def write_ico(path: Path) -> 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("<HHH", 0, 1, count))
|
||||||
|
offset = 6 + 16 * count
|
||||||
|
for size, block in zip(SIZES, blocks):
|
||||||
|
entry = struct.pack(
|
||||||
|
"<BBBBHHII",
|
||||||
|
size % 256,
|
||||||
|
size % 256,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
32,
|
||||||
|
len(block),
|
||||||
|
offset,
|
||||||
|
)
|
||||||
|
out += entry
|
||||||
|
offset += len(block)
|
||||||
|
for block in blocks:
|
||||||
|
out += block
|
||||||
|
path.write_bytes(bytes(out))
|
||||||
|
|
||||||
|
|
||||||
|
def write_png(path: Path, size: int) -> 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())
|
||||||
Reference in New Issue
Block a user