3 Commits
Author SHA1 Message Date
Pepe44DEV 71546327ae GUI-Politur: Hover-Effekte, edlere Karten, verfeinerte Farbpaletten, dünnere Scrollbar, aufgeräumte Sidebar-Stats
Build Windows / build (push) Has been cancelled
- Buttons/Nav/Filter-Chips mit Hover-Farbwechsel und aktiver Akzent-Kante
- Server-Karten mit Status rechts, Verbinden-Hinweis und Akzent-Rahmen bei Hover
- Shell-Karten mit '»'-Badge
- Verfeinerte Farbpaletten (kontrastreicher) + eigener Hover-Akzent pro Theme
- Sidebar-Stats mit Version, farbigem Terminal-Status und Server-Zähler
- Empty-States mit Symbol, Scrollbar schlanker (6px, Akzent-Scrollbalken)
- Version 1.2.0
2026-09-06 23:14:59 +00:00
Pepe44DEV 743b8d7a5e Features: Quick Commands ausfuehrbar, Gruppenfilter, Duplizieren, Erreichbarkeits-Check, Themes, Kontextmenue
Build Windows / build (push) Has been cancelled
- Quick Commands lokal oder via SSH auf dem ausgewaehlten Server starten
- Server duplizieren, SSH-Befehl in die Zwischenablage kopieren
- Erreichbarkeits-Check pro Server (TCP, Thread-basiert) mit Statuspunkt
- Gruppenfilter-Chips in der Sidebar
- Themes: neon-green, ocean-blue, sunset-orange, cyber-purple
- Doppelklick verbindet, Rechtsklick-Kontextmenue, Shortcuts F5/Ctrl+N/Ctrl+S
- Quick-Commands-Seite mit Buttons, About-Dialog, Config-Ordner oeffnen
- Version 1.1.0
2026-09-06 22:57:39 +00:00
Pepe44DEV aef3138cdd CI: Windows-Build und Installer ueber Wine-Docker-Image (kein Windows-Runner noetig) 2026-09-06 22:45:43 +00:00
4 changed files with 518 additions and 133 deletions
+11 -23
View File
@@ -8,34 +8,22 @@ on:
jobs: jobs:
build: build:
runs-on: windows-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Python - name: Build EXE + Installer (Wine)
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: | run: |
python -m pip install --upgrade pip docker pull cdrx/pyinstaller-windows:python3
python -m pip install -r requirements-build.txt docker run --rm -v "$(pwd):/src" -e SRCDIR=/src cdrx/pyinstaller-windows:python3 \
python scripts\make_icon.py "pyinstaller --clean -y --noconfirm --onefile --windowed --name PulseGate --icon assets/pulsegate.ico --distpath dist/windows --workpath /tmp desktop/pulsegate_desktop.py; \
cp dist/windows/PulseGate.exe dist/PulseGate.exe; \
- name: Build EXE (PyInstaller) apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq xvfb >/dev/null 2>&1; \
run: | curl -sL -o /tmp/is.exe https://github.com/jrsoftware/issrc/releases/download/is-7_1_0/innosetup-7.1.0-x64.exe; \
python -m PyInstaller --noconfirm --clean --onefile --windowed --name PulseGate --icon assets\pulsegate.ico desktop\pulsegate_desktop.py xvfb-run -a wine /tmp/is.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART; \
Get-Item dist\PulseGate.exe | Select-Object FullName, Length cd /src/installer && xvfb-run -a wine '/wine/drive_c/Program Files/Inno Setup 7/ISCC.exe' 'Z:\src\installer\installer.iss'; \
chown -R $(id -u):$(id -g) /src/dist /src/installer/output"
- name: Install Inno Setup
shell: bash
run: |
choco install innosetup --no-progress --yes
- name: Build installer
run: iscc installer\installer.iss
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+10 -5
View File
@@ -7,12 +7,17 @@ Läuft nativ auf **Linux** und **Windows**. Für Windows liegt ein fertiger Inst
## Features ## Features
- Desktop-UI ohne Browser - Desktop-UI ohne Browser
- Aufpolierte Oberfläche mit Hover-Effekten, Akzent-Design und edlen Karten
- Serverliste mit Suche - Serverliste mit Suche
- Server hinzufügen, bearbeiten und löschen - Server hinzufügen, bearbeiten, duplizieren und löschen
- SSH-Verbindung per Button starten - Gruppenfilter in der Sidebar
- SSH-Befehl pro Server anzeigen - SSH-Verbindung per Button, per Doppelklick oder über das Kontextmenü starten
- Terminal-Settings verwalten - Erreichbarkeits-Check pro Server (TCP-Ping)
- Quick Commands anzeigen - SSH-Befehl pro Server anzeigen und in die Zwischenablage kopieren
- **Quick Commands ausführbar** – lokal oder via SSH auf dem ausgewählten Server
- Konfigurierbare Themes (neon-green, ocean-blue, sunset-orange, cyber-purple)
- Tastenkürzel: F5 neu laden, Ctrl+N neuer Server, Ctrl+S speichern
- Terminal-Settings verwalten, Config-Ordner direkt öffnen
- Gemeinsame Konfiguration mit PulseGate TUI - Gemeinsame Konfiguration mit PulseGate TUI
## Voraussetzungen ## Voraussetzungen
+496 -104
View File
@@ -5,8 +5,10 @@ from __future__ import annotations
import os import os
import shlex import shlex
import shutil import shutil
import socket
import subprocess import subprocess
import sys import sys
import threading
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import messagebox from tkinter import messagebox
@@ -15,24 +17,77 @@ import yaml
APP_TITLE = "PulseGate Desktop" APP_TITLE = "PulseGate Desktop"
APP_VERSION = "1.0.0" APP_VERSION = "1.2.0"
APP_URL = "https://git.pepe44.dev/Pepe44DEV/PulseGate-GUI"
IS_WINDOWS = sys.platform == "win32" IS_WINDOWS = sys.platform == "win32"
CONNECT_TIMEOUT = 2.5
STATUS_OK = "#00e07a"
COLORS = { THEMES = {
"bg": "#07110d", "neon-green": {
"sidebar": "#050c09", "bg": "#050d09",
"surface": "#0d1b15", "sidebar": "#040b08",
"surface_2": "#13251e", "surface": "#0c1a13",
"surface_3": "#172c24", "surface_2": "#12241b",
"field": "#09150f", "surface_3": "#193127",
"line": "#1f4b38", "field": "#08130e",
"text": "#d7ffe9", "line": "#1d4a37",
"muted": "#8aa99b", "text": "#d9ffe9",
"accent": "#00ff99", "muted": "#86a998",
"accent_2": "#33ccff", "accent": "#00ff99",
"danger": "#ff6666", "accent_hi": "#5cffb9",
"accent_2": "#3ecfff",
"danger": "#ff7070",
},
"ocean-blue": {
"bg": "#061019",
"sidebar": "#040b13",
"surface": "#0d1d29",
"surface_2": "#13293a",
"surface_3": "#1a364c",
"field": "#081420",
"line": "#1c4a6e",
"text": "#d8edff",
"muted": "#86a7c2",
"accent": "#3ddcff",
"accent_hi": "#8fecff",
"accent_2": "#6f9bff",
"danger": "#ff7a7a",
},
"sunset-orange": {
"bg": "#130d06",
"sidebar": "#0e0904",
"surface": "#241911",
"surface_2": "#312419",
"surface_3": "#3f2f20",
"field": "#181009",
"line": "#6c4a1d",
"text": "#ffead6",
"muted": "#c3ab8b",
"accent": "#ff9d3d",
"accent_hi": "#ffbd7c",
"accent_2": "#ff7a5c",
"danger": "#ff7a7a",
},
"cyber-purple": {
"bg": "#0f0712",
"sidebar": "#0a050c",
"surface": "#1e0f24",
"surface_2": "#2b1534",
"surface_3": "#3a1d46",
"field": "#150a19",
"line": "#5c2172",
"text": "#f2d9ff",
"muted": "#b590c2",
"accent": "#c95aff",
"accent_hi": "#e0a0ff",
"accent_2": "#6affcc",
"danger": "#ff7a7a",
},
} }
COLORS = dict(THEMES["neon-green"])
UI_FONT = ("Segoe UI", 11) if IS_WINDOWS else ("Inter", 11) 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_SMALL = ("Segoe UI", 9) if IS_WINDOWS else ("Inter", 9)
UI_FONT_MONO = ("Consolas", 10) if IS_WINDOWS else ("JetBrains Mono", 10) UI_FONT_MONO = ("Consolas", 10) if IS_WINDOWS else ("JetBrains Mono", 10)
@@ -84,17 +139,19 @@ def default_config() -> dict:
"quick_commands": [ "quick_commands": [
{"name": "Disk Usage", "command": "df -h"}, {"name": "Disk Usage", "command": "df -h"},
{"name": "RAM Usage", "command": "free -h"}, {"name": "RAM Usage", "command": "free -h"},
{"name": "Uptime", "command": "uptime"}, {"name": "Container Status", "command": "docker ps"},
], ],
} }
def normalize_config(config: dict) -> dict: def normalize_config(config: dict) -> dict:
config.setdefault("settings", {}) config.setdefault("settings", {})
config["settings"].setdefault("theme", "neon-green") settings = config["settings"]
config["settings"].setdefault("terminal", {}) settings.setdefault("theme", "neon-green")
config["settings"]["terminal"].setdefault("term", "xterm-256color") settings.setdefault("quick_commands_enabled", True)
config["settings"]["terminal"].setdefault("enable_kitty_fix", True) settings.setdefault("terminal", {})
settings["terminal"].setdefault("term", "xterm-256color")
settings["terminal"].setdefault("enable_kitty_fix", True)
config.setdefault("servers", []) config.setdefault("servers", [])
config.setdefault("quick_commands", []) config.setdefault("quick_commands", [])
@@ -139,7 +196,7 @@ def expand_home(value: str) -> str:
if IS_WINDOWS: if IS_WINDOWS:
profile = os.environ.get("USERPROFILE") profile = os.environ.get("USERPROFILE")
if profile: if profile:
return os.path.join(profile, value[2:].replace("/", "\\")) return profile + "\\" + value[2:].replace("/", "\\")
return str(Path(value).expanduser()) return str(Path(value).expanduser())
@@ -158,6 +215,15 @@ def display_command(server: dict) -> str:
return " ".join(shlex.quote(arg) for arg in args) return " ".join(shlex.quote(arg) for arg in args)
def remote_ssh_command(server: dict, command: str) -> str:
"""Führt einen Befehl auf dem Server via SSH aus (im Terminal)."""
if IS_WINDOWS:
base = subprocess.list2cmdline(ssh_args(server))
return base + " " + subprocess.list2cmdline([command])
base = " ".join(shlex.quote(arg) for arg in ssh_args(server))
return base + " " + shlex.quote(command)
def detect_terminal() -> str | None: def detect_terminal() -> str | None:
if IS_WINDOWS: if IS_WINDOWS:
for name in ("wt", "cmd", "powershell"): for name in ("wt", "cmd", "powershell"):
@@ -173,37 +239,44 @@ def detect_terminal() -> str | None:
return None return None
def terminal_command(terminal: str, server: dict) -> list[str]: def open_in_terminal(terminal: str, title: str, command: str, hold: bool = True, done_message: str = "Fertig.") -> list[str]:
title = f"PulseGate - {server.get('name') or server.get('host')}"
if IS_WINDOWS: if IS_WINDOWS:
params = subprocess.list2cmdline(ssh_args(server))
stem = Path(terminal).stem.lower() stem = Path(terminal).stem.lower()
if stem in ("powershell", "pwsh"): if stem in ("powershell", "pwsh"):
hold = f"& {params}; Write-Host ''; Write-Host 'SSH beendet.'; Read-Host 'Enter zum Schliessen...'" suffix = f"; Write-Host ''; Write-Host '{done_message}'; Read-Host 'Enter zum Schliessen...'" if hold else ""
return [terminal, "-NoExit", "-Command", hold] return [terminal, "-NoExit", "-Command", f"& {command}{suffix}"]
hold = f"{params} & echo. & echo SSH beendet. Bitte Fenster schliessen... & pause" suffix = f" & echo. & echo {done_message} & pause" if hold else ""
inner = command + suffix
if stem == "wt": if stem == "wt":
return [terminal, "new-tab", "--title", title, "cmd.exe", "/k", hold] return [terminal, "new-tab", "--title", title, "cmd.exe", "/k", inner]
return [terminal, "/k", hold] return [terminal, "/k", inner]
hold = " ".join(shlex.quote(arg) for arg in ssh_args(server)) + "; printf '\\nSSH beendet. Enter zum Schließen...'; read _"
suffix = f"; printf '\\n{done_message} Enter zum Schließen...'; read _" if hold else ""
inner = command + suffix
name = Path(terminal).name name = Path(terminal).name
if name == "kitty": if name == "kitty":
return [terminal, "--title", title, "sh", "-lc", hold] return [terminal, "--title", title, "sh", "-lc", inner]
if name == "alacritty": if name == "alacritty":
return [terminal, "--title", title, "-e", "sh", "-lc", hold] return [terminal, "--title", title, "-e", "sh", "-lc", inner]
if name == "konsole": if name == "konsole":
return [terminal, "--new-tab", "-p", f"tabtitle={title}", "-e", "sh", "-lc", hold] return [terminal, "--new-tab", "-p", f"tabtitle={title}", "-e", "sh", "-lc", inner]
if name == "gnome-terminal": if name == "gnome-terminal":
return [terminal, "--title", title, "--", "sh", "-lc", hold] return [terminal, "--title", title, "--", "sh", "-lc", inner]
if name == "xfce4-terminal": if name == "xfce4-terminal":
return [terminal, "--title", title, "--command", f"sh -lc {shlex.quote(hold)}"] return [terminal, "--title", title, "--command", f"sh -lc {shlex.quote(inner)}"]
if name == "xterm": if name == "xterm":
return [terminal, "-T", title, "-e", "sh", "-lc", hold] return [terminal, "-T", title, "-e", "sh", "-lc", inner]
return [terminal, "-e", "sh", "-lc", hold] return [terminal, "-e", "sh", "-lc", inner]
def terminal_command(terminal: str, server: dict) -> list[str]:
title = f"PulseGate - {server.get('name') or server.get('host')}"
if IS_WINDOWS:
command = subprocess.list2cmdline(ssh_args(server))
return open_in_terminal(terminal, title, command, done_message="SSH beendet.")
command = " ".join(shlex.quote(arg) for arg in ssh_args(server))
return open_in_terminal(terminal, title, command, done_message="SSH beendet.")
class PulseGateDesktop(tk.Tk): class PulseGateDesktop(tk.Tk):
@@ -213,7 +286,13 @@ class PulseGateDesktop(tk.Tk):
self.config_data = load_config(path) self.config_data = load_config(path)
self.selected_index = 0 self.selected_index = 0
self.current_view = "servers" self.current_view = "servers"
self.group_filter = "Alle"
self.terminal = detect_terminal() self.terminal = detect_terminal()
self.reachability: dict[int, bool] = {}
self.checked_hosts: dict[tuple[str, int], bool] = {}
self.reach_labels: dict[int, tk.Label] = {}
COLORS.update(THEMES.get(self.config_data["settings"].get("theme", "neon-green"), THEMES["neon-green"]))
self.vars: dict[str, tk.Variable] = {} self.vars: dict[str, tk.Variable] = {}
self.settings_vars: dict[str, tk.Variable] = {} self.settings_vars: dict[str, tk.Variable] = {}
@@ -223,13 +302,17 @@ class PulseGateDesktop(tk.Tk):
self.status_var = tk.StringVar(value="Bereit") self.status_var = tk.StringVar(value="Bereit")
self.title(APP_TITLE) self.title(APP_TITLE)
self.geometry("1180x760") self.geometry("1240x800")
self.minsize(980, 640) self.minsize(1000, 640)
self.configure(bg=COLORS["bg"]) self.configure(bg=COLORS["bg"])
self._build_layout() self._build_layout()
self._refresh_all() self._refresh_all()
self.bind("<F5>", lambda _event: self._reload())
self.bind("<Control-n>", lambda _event: self._add_server())
self.bind("<Control-s>", lambda _event: self._save_current_server())
def _build_layout(self) -> None: def _build_layout(self) -> None:
root = tk.Frame(self, bg=COLORS["bg"]) root = tk.Frame(self, bg=COLORS["bg"])
root.pack(fill="both", expand=True) root.pack(fill="both", expand=True)
@@ -238,12 +321,12 @@ class PulseGateDesktop(tk.Tk):
self._build_content(root) self._build_content(root)
def _build_sidebar(self, parent: tk.Frame) -> None: def _build_sidebar(self, parent: tk.Frame) -> None:
sidebar = tk.Frame(parent, bg=COLORS["sidebar"], width=282) sidebar = tk.Frame(parent, bg=COLORS["sidebar"], width=290)
sidebar.pack(side="left", fill="y") sidebar.pack(side="left", fill="y")
sidebar.pack_propagate(False) sidebar.pack_propagate(False)
brand = tk.Frame(sidebar, bg=COLORS["sidebar"]) brand = tk.Frame(sidebar, bg=COLORS["sidebar"])
brand.pack(fill="x", padx=22, pady=(24, 22)) brand.pack(fill="x", padx=22, pady=(24, 20))
mark = tk.Label( mark = tk.Label(
brand, brand,
@@ -263,9 +346,13 @@ class PulseGateDesktop(tk.Tk):
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)
search = self._entry(sidebar, self.search_var) search = self._entry(sidebar, self.search_var)
search.pack(fill="x", padx=22, pady=(7, 22)) search.pack(fill="x", padx=22, pady=(7, 14))
self.search_var.trace_add("write", lambda *_: self._refresh_server_cards()) self.search_var.trace_add("write", lambda *_: self._refresh_server_cards())
self._label(sidebar, "Gruppenfilter", bg=COLORS["sidebar"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", padx=22)
self.filter_row = tk.Frame(sidebar, bg=COLORS["sidebar"])
self.filter_row.pack(fill="x", padx=22, pady=(7, 20))
nav = tk.Frame(sidebar, bg=COLORS["sidebar"]) nav = tk.Frame(sidebar, bg=COLORS["sidebar"])
nav.pack(fill="x", padx=22) nav.pack(fill="x", padx=22)
for view, label in (("servers", "Server"), ("commands", "Commands"), ("settings", "Settings")): for view, label in (("servers", "Server"), ("commands", "Commands"), ("settings", "Settings")):
@@ -274,12 +361,19 @@ class PulseGateDesktop(tk.Tk):
self.nav_buttons[view] = button self.nav_buttons[view] = button
stats = self._card(sidebar, bg=COLORS["surface"], padx=14, pady=12) stats = self._card(sidebar, bg=COLORS["surface"], padx=14, pady=12)
stats.pack(fill="x", padx=22, pady=(28, 0)) stats.pack(fill="x", padx=22, pady=(28, 10))
self.server_count_label = self._label(stats, "", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD) head = tk.Frame(stats, bg=COLORS["surface"])
self.server_count_label.pack(anchor="w") head.pack(fill="x")
self._label(head, "PulseGate", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(side="left")
self._label(head, "v" + APP_VERSION, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(side="right", anchor="e", pady=(3, 0))
terminal_text = "Terminal bereit" if self.terminal else "Kein Terminal gefunden" terminal_text = "Terminal bereit" if self.terminal else "Kein Terminal gefunden"
self._label(stats, terminal_text, bg=COLORS["surface"], fg=COLORS["accent_2"], font=FONT_SMALL).pack(anchor="w", pady=(7, 0)) terminal_fg = STATUS_OK if self.terminal else COLORS["danger"]
self._label(stats, str(self.path), bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL, wraplength=205).pack(anchor="w", pady=(8, 0)) self._label(stats, f"● {terminal_text}", bg=COLORS["surface"], fg=terminal_fg, font=FONT_SMALL).pack(anchor="w", pady=(8, 0))
self.server_count_label = self._label(stats, "0 Server", bg=COLORS["surface"], fg=COLORS["accent"], font=(UI_FONT[0], 10, "bold"))
self.server_count_label.pack(anchor="w", pady=(7, 0))
self._label(stats, str(self.path), bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL, wraplength=210).pack(anchor="w", pady=(8, 0))
self._button(sidebar, "Über PulseGate", self._about, variant="ghost").pack(fill="x", padx=22, side="bottom", pady=(0, 16))
def _build_content(self, parent: tk.Frame) -> None: def _build_content(self, parent: tk.Frame) -> None:
content = tk.Frame(parent, bg=COLORS["bg"]) content = tk.Frame(parent, bg=COLORS["bg"])
@@ -309,19 +403,28 @@ class PulseGateDesktop(tk.Tk):
self._build_settings_page(self.pages["settings"]) self._build_settings_page(self.pages["settings"])
def _build_servers_page(self, page: tk.Frame) -> None: def _build_servers_page(self, page: tk.Frame) -> None:
left = tk.Frame(page, bg=COLORS["bg"], width=390) left = tk.Frame(page, bg=COLORS["bg"], width=400)
left.pack(side="left", fill="both", padx=(0, 18)) left.pack(side="left", fill="both", padx=(0, 18))
left.pack_propagate(False) left.pack_propagate(False)
self.server_canvas = tk.Canvas(left, bg=COLORS["bg"], highlightthickness=0, bd=0) self.server_canvas = tk.Canvas(left, bg=COLORS["bg"], highlightthickness=0, bd=0)
scrollbar = tk.Scrollbar(left, orient="vertical", command=self.server_canvas.yview, bg=COLORS["bg"], troughcolor=COLORS["bg"]) scrollbar = tk.Scrollbar(
left,
orient="vertical",
command=self.server_canvas.yview,
bg=COLORS["surface_3"],
activebackground=COLORS["accent"],
troughcolor=COLORS["bg"],
relief="flat",
bd=0,
width=6,
)
self.server_cards = tk.Frame(self.server_canvas, bg=COLORS["bg"]) self.server_cards = tk.Frame(self.server_canvas, bg=COLORS["bg"])
self.server_cards.bind("<Configure>", lambda _event: self.server_canvas.configure(scrollregion=self.server_canvas.bbox("all"))) self.server_cards.bind("<Configure>", lambda _event: self.server_canvas.configure(scrollregion=self.server_canvas.bbox("all")))
self.server_canvas.create_window((0, 0), window=self.server_cards, anchor="nw", width=372) self.server_canvas.create_window((0, 0), window=self.server_cards, anchor="nw", width=382)
self.server_canvas.configure(yscrollcommand=scrollbar.set) self.server_canvas.configure(yscrollcommand=scrollbar.set)
self.server_canvas.pack(side="left", fill="both", expand=True) self.server_canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y") scrollbar.pack(side="right", fill="y")
self.server_canvas.bind_all("<MouseWheel>", self._on_mousewheel)
editor_outer = self._card(page, bg=COLORS["surface"], padx=18, pady=18) editor_outer = self._card(page, bg=COLORS["surface"], padx=18, pady=18)
editor_outer.pack(side="left", fill="both", expand=True) editor_outer.pack(side="left", fill="both", expand=True)
@@ -355,18 +458,7 @@ class PulseGateDesktop(tk.Tk):
var = tk.StringVar() var = tk.StringVar()
self.vars[key] = var self.vars[key] = var
if key == "auth": if key == "auth":
widget = tk.OptionMenu(cell, var, "key", "password") widget = self._option_menu(cell, var, ("key", "password"))
widget.configure(
bg=COLORS["field"],
fg=COLORS["text"],
activebackground=COLORS["surface_2"],
activeforeground=COLORS["text"],
highlightthickness=1,
highlightbackground=COLORS["line"],
relief="flat",
font=FONT,
)
widget["menu"].configure(bg=COLORS["surface_2"], fg=COLORS["text"], activebackground=COLORS["accent"], activeforeground="#001a10")
else: else:
widget = self._entry(cell, var) widget = self._entry(cell, var)
widget.pack(fill="x", pady=(6, 0), ipady=3) widget.pack(fill="x", pady=(6, 0), ipady=3)
@@ -385,18 +477,31 @@ class PulseGateDesktop(tk.Tk):
activeforeground=COLORS["text"], activeforeground=COLORS["text"],
selectcolor=COLORS["field"], selectcolor=COLORS["field"],
font=FONT, font=FONT,
).pack(anchor="w", pady=(12, 4)) ).pack(anchor="w", pady=(12, 0))
hint = tk.Frame(editor_outer, bg=COLORS["surface"])
hint.pack(fill="x", pady=(10, 14))
self._label(
hint,
"Tipp: Doppelklick auf einen Server verbindet direkt · Rechtsklick öffnet das Kontextmenü · Ctrl+S speichert.",
bg=COLORS["surface"],
fg=COLORS["muted"],
font=FONT_SMALL,
wraplength=520,
).pack(anchor="w")
actions = tk.Frame(editor_outer, bg=COLORS["surface"]) actions = tk.Frame(editor_outer, bg=COLORS["surface"])
actions.pack(fill="x", pady=(16, 14)) actions.pack(fill="x")
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, "Kopieren", self._copy_command, variant="ghost").pack(side="left", padx=(8, 0))
self._button(actions, "Löschen", 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, "Duplizieren", self._duplicate_current_server, variant="ghost").pack(side="right", padx=(0, 8))
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(
editor_outer, editor_outer,
height=6, height=5,
bg=COLORS["field"], bg=COLORS["field"],
fg=COLORS["accent"], fg=COLORS["accent"],
insertbackground=COLORS["text"], insertbackground=COLORS["text"],
@@ -406,27 +511,44 @@ class PulseGateDesktop(tk.Tk):
padx=12, padx=12,
pady=10, pady=10,
) )
self.command_text.pack(fill="both", expand=True) self.command_text.pack(fill="both", expand=True, pady=(14, 0))
def _build_commands_page(self, page: tk.Frame) -> None: def _build_commands_page(self, page: tk.Frame) -> None:
self.commands_host = tk.Frame(page, bg=COLORS["bg"]) self.commands_host = tk.Frame(page, bg=COLORS["bg"])
self.commands_host.pack(fill="both", expand=True) self.commands_host.pack(fill="both", expand=True)
intro = self._card(self.commands_host, bg=COLORS["surface"], padx=16, pady=14)
intro.pack(fill="x", pady=(0, 12))
self._label(intro, "Quick Commands", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(anchor="w")
self._label(
intro,
"Befehle können lokal auf diesem Rechner oder auf dem aktuell ausgewählten Server (via SSH) ausgeführt werden.",
bg=COLORS["surface"],
fg=COLORS["muted"],
font=FONT_SMALL,
wraplength=760,
).pack(anchor="w", pady=(5, 0))
def _build_settings_page(self, page: tk.Frame) -> None: def _build_settings_page(self, page: tk.Frame) -> None:
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=(UI_FONT[0], 18, "bold")).pack(anchor="w") self._label(panel, "Darstellung", 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, "Das Theme wird beim App-Start angewandt.", bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", pady=(4, 12))
theme_label = self._label(panel, "Theme", bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL)
theme_label.pack(anchor="w")
self.settings_vars["theme"] = tk.StringVar() self.settings_vars["theme"] = tk.StringVar()
self.settings_vars["term"] = tk.StringVar() self._option_menu(panel, self.settings_vars["theme"], tuple(THEMES.keys())).pack(fill="x", pady=(6, 18), ipady=3)
self.settings_vars["enable_kitty_fix"] = tk.BooleanVar()
for label, key in (("Theme", "theme"), ("TERM Override", "term")): 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, 12))
self.settings_vars["term"] = tk.StringVar()
for label, key in (("TERM Override", "term"),):
self._label(panel, label, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w") self._label(panel, label, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w")
self._entry(panel, self.settings_vars[key]).pack(fill="x", pady=(6, 14), ipady=3) self._entry(panel, self.settings_vars[key]).pack(fill="x", pady=(6, 14), ipady=3)
self.settings_vars["enable_kitty_fix"] = tk.BooleanVar()
tk.Checkbutton( tk.Checkbutton(
panel, panel,
text="Kitty Fix global aktivieren", text="Kitty Fix global aktivieren",
@@ -439,61 +561,179 @@ class PulseGateDesktop(tk.Tk):
font=FONT, font=FONT,
).pack(anchor="w", pady=(0, 18)) ).pack(anchor="w", pady=(0, 18))
self._button(panel, "Settings speichern", self._save_settings).pack(anchor="e") actions = tk.Frame(panel, bg=COLORS["surface"])
actions.pack(fill="x")
self._button(actions, "Config-Ordner öffnen", self._open_config_folder, variant="ghost").pack(side="left")
self._button(actions, "Settings speichern", self._save_settings).pack(side="right")
def _refresh_all(self) -> None: def _refresh_all(self) -> None:
self.server_count_label.configure(text=f"{len(self.config_data['servers'])} Server") self._refresh_group_chips()
self._refresh_server_cards() self._refresh_server_cards()
self._load_selected_server() self._load_selected_server()
self._refresh_commands() self._refresh_commands()
self._refresh_settings() self._refresh_settings()
self._show_view(self.current_view) self._show_view(self.current_view)
def _refresh_group_chips(self) -> None:
for child in self.filter_row.winfo_children():
child.destroy()
groups = ["Alle"] + sorted({server.get("group") or "Keine Gruppe" for server in self.config_data["servers"]})
for group in groups:
selected = group == self.group_filter
chip = tk.Label(
self.filter_row,
text=group,
bg=COLORS["accent"] if selected else COLORS["surface_2"],
fg="#001a10" if selected else COLORS["text"],
font=(UI_FONT[0], 9, "bold"),
padx=10,
pady=5,
cursor="hand2",
highlightthickness=1,
highlightbackground=COLORS["line"] if not selected else COLORS["accent_hi"],
)
chip.pack(side="left", padx=(0, 6))
chip.bind("<Button-1>", lambda _event, value=group: self._set_group_filter(value))
def _enter(_event, label=chip, is_selected=selected) -> None:
if not is_selected:
label.configure(bg=COLORS["surface_3"], highlightbackground=COLORS["accent_hi"])
def _leave(_event, label=chip, is_selected=selected) -> None:
if not is_selected:
label.configure(bg=COLORS["surface_2"], highlightbackground=COLORS["line"])
chip.bind("<Enter>", _enter)
chip.bind("<Leave>", _leave)
def _set_group_filter(self, group: str) -> None:
self.group_filter = group
self._refresh_group_chips()
self._refresh_server_cards()
def _refresh_server_cards(self) -> None: def _refresh_server_cards(self) -> None:
for child in self.server_cards.winfo_children(): for child in self.server_cards.winfo_children():
child.destroy() child.destroy()
self.reach_labels.clear()
query = self.search_var.get().strip().lower() query = (self.search_var.get() or "").strip().lower()
rendered = 0 rendered = 0
for index, server in enumerate(self.config_data["servers"]): for index, server in enumerate(self.config_data["servers"]):
haystack = " ".join(str(server.get(key, "")) for key in ("name", "host", "user", "group", "auth")).lower() haystack = " ".join(str(server.get(key, "")) for key in ("name", "host", "user", "group", "auth")).lower()
if query and query not in haystack: if query and query not in haystack:
continue continue
group = server.get("group") or "Keine Gruppe"
if self.group_filter != "Alle" and group != self.group_filter:
continue
self._server_card(self.server_cards, server, index).pack(fill="x", pady=(0, 10)) self._server_card(self.server_cards, server, index).pack(fill="x", pady=(0, 10))
rendered += 1 rendered += 1
self._check_reachability(server, index)
if rendered == 0: if rendered == 0:
self._empty_state(self.server_cards, "Keine Server gefunden").pack(fill="x") self._empty_state(self.server_cards, "Keine Server gefunden").pack(fill="x")
total = len(self.config_data["servers"])
text = f"{rendered} / {total} Server" if rendered != total else f"{total} Server"
self.server_count_label.configure(text=text)
def _server_card(self, parent: tk.Frame, server: dict, index: int) -> tk.Frame: def _server_card(self, parent: tk.Frame, server: dict, index: int) -> tk.Frame:
selected = index == self.selected_index selected = index == self.selected_index
bg = COLORS["surface_2"] if selected else COLORS["surface"] bg = COLORS["surface_2"] if selected else COLORS["surface"]
border = COLORS["accent"] if selected else COLORS["line"] border = COLORS["accent"] if selected else COLORS["line"]
card = tk.Frame(parent, bg=border, bd=0) card = tk.Frame(parent, bg=border, bd=0)
inner = tk.Frame(card, bg=bg, padx=14, pady=12) inner = tk.Frame(card, bg=bg, padx=14, pady=12)
inner.pack(fill="both", expand=True, padx=1, pady=1) inner.pack(fill="both", expand=True, padx=1, pady=1)
target = f"{server.get('user')}@{server.get('host')}:{server.get('port') or 22}" head = tk.Frame(inner, bg=bg)
name = server.get("name") or "Unbenannt" head.pack(fill="x", pady=(0, 6))
group = server.get("group") or "Keine Gruppe"
self._label(inner, name, bg=bg, fg=COLORS["text"], font=FONT_CARD).pack(anchor="w") name = server.get("name") or "Unbenannt"
self._label(inner, target, bg=bg, fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", pady=(5, 8)) self._label(head, name, bg=bg, fg=COLORS["text"], font=FONT_CARD).pack(side="left")
status = self.reachability.get(index)
if status is None:
status_text, status_fg = "…", COLORS["muted"]
elif status:
status_text, status_fg = "● erreichbar", STATUS_OK
else:
status_text, status_fg = "● offline", COLORS["danger"]
status_label = self._label(head, status_text, bg=bg, fg=status_fg, font=FONT_SMALL)
status_label.pack(side="right", anchor="e", pady=(2, 0))
self.reach_labels[index] = status_label
target = f"{server.get('user')}@{server.get('host')}:{server.get('port') or 22}"
self._label(inner, target, bg=bg, fg=COLORS["muted"], font=UI_FONT_MONO).pack(anchor="w", pady=(0, 10))
chips = tk.Frame(inner, bg=bg) chips = tk.Frame(inner, bg=bg)
chips.pack(fill="x") chips.pack(fill="x")
self._chip(chips, group, COLORS["accent_2"], bg=bg).pack(side="left") self._chip(chips, server.get("group") or "Keine Gruppe", COLORS["accent_2"], bg=bg).pack(side="left")
self._chip(chips, server.get("auth") or "key", COLORS["accent"], bg=bg).pack(side="left", padx=(6, 0)) self._chip(chips, server.get("auth") or "key", COLORS["accent"], bg=bg).pack(side="left", padx=(6, 0))
if server.get("kitty_fix"): if server.get("kitty_fix"):
self._chip(chips, "kitty", COLORS["muted"], bg=bg).pack(side="left", padx=(6, 0)) self._chip(chips, "kitty", COLORS["muted"], bg=bg).pack(side="left", padx=(6, 0))
connect_hint = tk.Label(inner, text="Verbinden ›", bg=bg, fg=COLORS["accent_hi"], font=FONT_SMALL)
connect_hint.place(relx=1.0, rely=0.0, anchor="ne", x=-14, y=6)
def _card_enter(_event) -> None:
card.configure(bg=COLORS["accent_hi"])
connect_hint.lift()
def _card_leave(_event) -> None:
card.configure(bg=border)
connect_hint.lower()
for widget in (card, inner): for widget in (card, inner):
widget.bind("<Button-1>", lambda _event, value=index: self._select_server(value)) widget.bind("<Button-1>", lambda _event, value=index: self._select_server(value))
widget.bind("<Double-Button-1>", lambda _event, value=index: self._connect_at(value))
widget.bind("<Button-3>", lambda event, value=index: self._server_menu(event, value))
widget.bind("<Enter>", _card_enter)
widget.bind("<Leave>", _card_leave)
for child in inner.winfo_children(): for child in inner.winfo_children():
child.bind("<Button-1>", lambda _event, value=index: self._select_server(value)) child.bind("<Button-1>", lambda _event, value=index: self._select_server(value))
child.bind("<Double-Button-1>", lambda _event, value=index: self._connect_at(value))
child.bind("<Button-3>", lambda event, value=index: self._server_menu(event, value))
child.bind("<Enter>", _card_enter)
child.bind("<Leave>", _card_leave)
return card return card
def _check_reachability(self, server: dict, index: int) -> None:
host = str(server.get("host") or "").strip()
if not host:
return
try:
port = int(server.get("port") or 22)
except (TypeError, ValueError):
return
cache_key = (host, port)
if cache_key in self.checked_hosts:
self.reachability[index] = self.checked_hosts[cache_key]
self._update_reachability_indicator(index, self.checked_hosts[cache_key])
return
def worker() -> None:
try:
with socket.create_connection((host, port), timeout=CONNECT_TIMEOUT):
reachable = True
except OSError:
reachable = False
self.checked_hosts[cache_key] = reachable
self.reachability[index] = reachable
self.after(0, lambda: self._update_reachability_indicator(index, reachable))
threading.Thread(target=worker, daemon=True).start()
def _update_reachability_indicator(self, index: int, reachable: bool) -> None:
label = self.reach_labels.get(index)
if label is not None and label.winfo_exists():
label.configure(
text="● erreichbar" if reachable else "● nicht erreichbar",
fg=STATUS_OK if reachable else COLORS["danger"],
)
def _refresh_commands(self) -> None: def _refresh_commands(self) -> None:
for child in self.commands_host.winfo_children(): for child in self.commands_host.winfo_children():
child.destroy() child.destroy()
@@ -506,8 +746,27 @@ class PulseGateDesktop(tk.Tk):
for command in commands: for command in commands:
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("command") or "", bg=COLORS["surface"], fg=COLORS["accent"], font=UI_FONT_MONO).pack(anchor="w", pady=(7, 0)) head = tk.Frame(card, bg=COLORS["surface"])
head.pack(fill="x")
self._label(head, "»", bg=COLORS["surface"], fg=COLORS["accent"], font=(UI_FONT[0], 14, "bold")).pack(side="left", padx=(0, 8))
self._label(head, command.get("name") or "Command", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(side="left")
self._label(card, command.get("command") or "", bg=COLORS["surface"], fg=COLORS["accent"], font=UI_FONT_MONO).pack(anchor="w", pady=(9, 0))
actions = tk.Frame(card, bg=COLORS["surface"])
actions.pack(fill="x", pady=(12, 0))
self._button(
actions,
"Lokal ausführen",
lambda value=command: self._run_quick_command(value, via_ssh=False),
variant="ghost",
).pack(side="left")
self._button(
actions,
"Auf Server ausführen",
lambda value=command: self._run_quick_command(value, via_ssh=True),
).pack(side="left", padx=(8, 0))
def _refresh_settings(self) -> None: def _refresh_settings(self) -> None:
settings = self.config_data["settings"] settings = self.config_data["settings"]
@@ -609,6 +868,18 @@ class PulseGateDesktop(tk.Tk):
self.current_view = "servers" self.current_view = "servers"
self._refresh_all() self._refresh_all()
def _duplicate_current_server(self) -> None:
if not self.config_data["servers"]:
return
original = self.config_data["servers"][self.selected_index]
duplicate = dict(original)
duplicate["name"] = f"{original.get('name') or 'Server'} (Kopie)"
self.config_data["servers"].append(duplicate)
self.selected_index = len(self.config_data["servers"]) - 1
save_config(self.path, self.config_data)
self.status_var.set("Server dupliziert")
self._refresh_all()
def _delete_current_server(self) -> None: def _delete_current_server(self) -> None:
if not self.config_data["servers"]: if not self.config_data["servers"]:
return return
@@ -630,7 +901,29 @@ class PulseGateDesktop(tk.Tk):
}, },
} }
save_config(self.path, self.config_data) save_config(self.path, self.config_data)
self.status_var.set("Settings gespeichert") self.status_var.set("Settings gespeichert (Theme ab Neustart)")
self.config_data = load_config(self.path)
self._refresh_settings()
def _open_config_folder(self) -> None:
folder = self.path.parent
try:
if IS_WINDOWS:
os.startfile(str(folder))
else:
subprocess.Popen(["xdg-open", str(folder)])
except OSError as error:
messagebox.showerror(APP_TITLE, str(error))
return
self.status_var.set(f"Config-Ordner geöffnet: {folder}")
def _about(self) -> None:
messagebox.showinfo(
"Über PulseGate Desktop",
f"PulseGate Desktop {APP_VERSION}\n\n"
"SSH-Manager für Linux und Windows.\n\n"
f"{APP_URL}",
)
def _reload(self) -> None: def _reload(self) -> None:
self.config_data = load_config(self.path) self.config_data = load_config(self.path)
@@ -651,6 +944,20 @@ class PulseGateDesktop(tk.Tk):
self.command_text.delete("1.0", "end") self.command_text.delete("1.0", "end")
self.command_text.insert("1.0", display_command(server)) self.command_text.insert("1.0", display_command(server))
def _copy_command(self) -> None:
server = self._current_server()
if not server:
return
self.clipboard_clear()
self.clipboard_append(display_command(server))
self.update()
self.status_var.set(f"SSH Befehl kopiert: {server.get('name')}")
def _connect_at(self, index: int) -> None:
self.selected_index = index
self._load_selected_server()
self._connect()
def _connect(self) -> None: def _connect(self) -> None:
server = self._current_server() server = self._current_server()
if not server: if not server:
@@ -675,9 +982,46 @@ class PulseGateDesktop(tk.Tk):
self.status_var.set(f"SSH gestartet: {server.get('name')}") self.status_var.set(f"SSH gestartet: {server.get('name')}")
self._show_command() self._show_command()
def _on_mousewheel(self, event: tk.Event) -> None: def _run_quick_command(self, command: dict, via_ssh: bool = False) -> None:
if self.current_view == "servers": if not self.terminal:
self.server_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") messagebox.showerror(APP_TITLE, "Kein unterstützter Terminal-Emulator gefunden.")
return
raw = (command.get("command") or "").strip()
name = command.get("name") or "Quick Command"
if not raw:
messagebox.showwarning(APP_TITLE, f"'{name}' hat keinen Befehl.")
return
try:
if via_ssh:
server = self._current_server()
if not server:
return
inner = remote_ssh_command(server, raw)
title = f"PulseGate - {server.get('name')}"
else:
inner = raw
title = f"PulseGate - {name}"
subprocess.Popen(open_in_terminal(self.terminal, title, inner))
except OSError as error:
messagebox.showerror(APP_TITLE, str(error))
return
self.status_var.set(f"Befehl gestartet: {name}" + (" (via SSH)" if via_ssh else ""))
def _server_menu(self, event: tk.Event, index: int) -> None:
self._select_server(index)
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Verbinden", command=self._connect)
menu.add_command(label="SSH Befehl kopieren", command=self._copy_command)
menu.add_separator()
menu.add_command(label="Duplizieren", command=self._duplicate_current_server)
menu.add_command(label="Löschen ...", command=self._delete_current_server)
try:
menu.tk_popup(event.x_root, event.y_root)
finally:
menu.grab_release()
def _label(self, parent: tk.Widget, text: str, *, bg: str, fg: str, font: tuple, wraplength: int = 0) -> tk.Label: def _label(self, parent: tk.Widget, text: str, *, bg: str, fg: str, font: tuple, wraplength: int = 0) -> tk.Label:
return tk.Label(parent, text=text, bg=bg, fg=fg, font=font, anchor="w", justify="left", wraplength=wraplength) return tk.Label(parent, text=text, bg=bg, fg=fg, font=font, anchor="w", justify="left", wraplength=wraplength)
@@ -696,37 +1040,66 @@ class PulseGateDesktop(tk.Tk):
font=FONT, font=FONT,
) )
def _option_menu(self, parent: tk.Widget, variable: tk.StringVar, options: tuple[str, ...]) -> tk.OptionMenu:
widget = tk.OptionMenu(parent, variable, *options)
widget.configure(
bg=COLORS["field"],
fg=COLORS["text"],
activebackground=COLORS["surface_2"],
activeforeground=COLORS["text"],
highlightthickness=1,
highlightbackground=COLORS["line"],
relief="flat",
font=FONT,
)
widget["menu"].configure(
bg=COLORS["surface_2"],
fg=COLORS["text"],
activebackground=COLORS["accent"],
activeforeground="#001a10",
font=FONT,
)
return widget
def _button(self, parent: tk.Widget, text: str, command, *, variant: str = "primary") -> tk.Button: def _button(self, parent: tk.Widget, text: str, command, *, variant: str = "primary") -> tk.Button:
bg = COLORS["accent"] if variant == "primary":
fg = "#001a10" bg = COLORS["accent"]
active_bg = "#42ffb5" fg = "#001a10"
if variant == "ghost": hover_bg = COLORS["accent_hi"]
hover_fg = "#001a10"
elif variant == "ghost":
bg = COLORS["surface_2"] bg = COLORS["surface_2"]
fg = COLORS["text"] fg = COLORS["text"]
active_bg = COLORS["surface_3"] hover_bg = COLORS["surface_3"]
elif variant == "danger": hover_fg = COLORS["text"]
bg = "#351414" else:
bg = "#3a1717"
fg = COLORS["danger"] fg = COLORS["danger"]
active_bg = "#4a1d1d" hover_bg = "#522020"
hover_fg = "#ffd0d0"
return tk.Button( button = tk.Button(
parent, parent,
text=text, text=text,
command=command, command=command,
bg=bg, bg=bg,
fg=fg, fg=fg,
activebackground=active_bg, activebackground=hover_bg,
activeforeground=fg, activeforeground=hover_fg,
relief="flat", relief="flat",
bd=0, bd=0,
padx=14, padx=14,
pady=9, pady=9,
font=(UI_FONT[0], 10, "bold"), font=(UI_FONT[0], 10, "bold"),
cursor="hand2", cursor="hand2",
highlightthickness=0,
) )
button.bind("<Enter>", lambda _event: button.configure(bg=hover_bg, fg=hover_fg))
button.bind("<Leave>", lambda _event: button.configure(bg=bg, fg=fg))
return button
def _nav_button(self, parent: tk.Widget, text: str, command) -> tk.Button: def _nav_button(self, parent: tk.Widget, text: str, command) -> tk.Button:
return tk.Button( button = tk.Button(
parent, parent,
text=text, text=text,
command=command, command=command,
@@ -745,6 +1118,24 @@ class PulseGateDesktop(tk.Tk):
highlightbackground=COLORS["sidebar"], highlightbackground=COLORS["sidebar"],
) )
def _enter(_event) -> None:
if not self._view_active(text):
button.configure(bg=COLORS["surface_2"])
def _leave(_event) -> None:
if self._view_active(text):
button.configure(bg=COLORS["surface_2"], highlightbackground=COLORS["line"])
else:
button.configure(bg=COLORS["sidebar"], highlightbackground=COLORS["sidebar"])
button.bind("<Enter>", _enter)
button.bind("<Leave>", _leave)
return button
def _view_active(self, name: str) -> bool:
mapping = {"Server": "servers", "Commands": "commands", "Settings": "settings"}
return mapping.get(name) == self.current_view
def _card(self, parent: tk.Widget, *, bg: str, padx: int, pady: int) -> tk.Frame: def _card(self, parent: tk.Widget, *, bg: str, padx: int, pady: int) -> tk.Frame:
return tk.Frame(parent, bg=bg, padx=padx, pady=pady, highlightthickness=1, highlightbackground=COLORS["line"]) return tk.Frame(parent, bg=bg, padx=padx, pady=pady, highlightthickness=1, highlightbackground=COLORS["line"])
@@ -754,16 +1145,17 @@ class PulseGateDesktop(tk.Tk):
text=text, text=text,
bg=bg or parent.cget("bg"), bg=bg or parent.cget("bg"),
fg=fg, fg=fg,
font=FONT_SMALL, font=(UI_FONT[0], 9, "bold"),
padx=7, padx=7,
pady=3, pady=3,
highlightthickness=1, highlightthickness=1,
highlightbackground=COLORS["line"], highlightbackground=COLORS["surface_3"],
) )
def _empty_state(self, parent: tk.Widget, text: str) -> tk.Frame: def _empty_state(self, parent: tk.Widget, text: str, symbol: str = "◦") -> tk.Frame:
card = self._card(parent, bg=COLORS["surface"], padx=16, pady=18) card = self._card(parent, bg=COLORS["surface"], padx=16, pady=18)
self._label(card, text, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT).pack(anchor="w") self._label(card, symbol, bg=COLORS["surface"], fg=COLORS["accent"], font=(UI_FONT[0], 18, "bold")).pack(side="left", padx=(0, 12))
self._label(card, text, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT).pack(side="left")
return card return card
+1 -1
View File
@@ -2,7 +2,7 @@
; Kompilieren: iscc installer\installer.iss (Inno Setup 6) ; Kompilieren: iscc installer\installer.iss (Inno Setup 6)
#define MyAppName "PulseGate Desktop" #define MyAppName "PulseGate Desktop"
#define MyAppVersion "1.0.0" #define MyAppVersion "1.2.0"
#define MyAppPublisher "Pepe44DEV" #define MyAppPublisher "Pepe44DEV"
#define MyAppURL "https://git.pepe44.dev/Pepe44DEV/PulseGate-GUI" #define MyAppURL "https://git.pepe44.dev/Pepe44DEV/PulseGate-GUI"
#define MyAppExeName "PulseGate.exe" #define MyAppExeName "PulseGate.exe"