Windows-Support, Build-Skripte und Installer fuer PulseGate Desktop
Build Windows / build (push) Has been cancelled
Build Windows / build (push) Has been cancelled
- 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 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.
|
||||
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
|
||||
# -*- 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()
|
||||
@@ -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