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:
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()
|
||||
Reference in New Issue
Block a user