Windows-Support, Build-Skripte und Installer fuer PulseGate Desktop
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:
2026-09-06 22:28:55 +00:00
parent dcafc1e7c1
commit 3f20bc392c
12 changed files with 392 additions and 63 deletions
+84 -37
View File
@@ -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()