Features: Quick Commands ausfuehrbar, Gruppenfilter, Duplizieren, Erreichbarkeits-Check, Themes, Kontextmenue
- 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
This commit is contained in:
@@ -8,11 +8,15 @@ Läuft nativ auf **Linux** und **Windows**. Für Windows liegt ein fertiger Inst
|
|||||||
|
|
||||||
- Desktop-UI ohne Browser
|
- Desktop-UI ohne Browser
|
||||||
- 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
|
||||||
|
|||||||
+377
-65
@@ -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,10 +17,14 @@ import yaml
|
|||||||
|
|
||||||
|
|
||||||
APP_TITLE = "PulseGate Desktop"
|
APP_TITLE = "PulseGate Desktop"
|
||||||
APP_VERSION = "1.0.0"
|
APP_VERSION = "1.1.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 = {
|
||||||
|
"neon-green": {
|
||||||
"bg": "#07110d",
|
"bg": "#07110d",
|
||||||
"sidebar": "#050c09",
|
"sidebar": "#050c09",
|
||||||
"surface": "#0d1b15",
|
"surface": "#0d1b15",
|
||||||
@@ -31,8 +37,53 @@ COLORS = {
|
|||||||
"accent": "#00ff99",
|
"accent": "#00ff99",
|
||||||
"accent_2": "#33ccff",
|
"accent_2": "#33ccff",
|
||||||
"danger": "#ff6666",
|
"danger": "#ff6666",
|
||||||
|
},
|
||||||
|
"ocean-blue": {
|
||||||
|
"bg": "#071018",
|
||||||
|
"sidebar": "#040b12",
|
||||||
|
"surface": "#0e1b26",
|
||||||
|
"surface_2": "#142638",
|
||||||
|
"surface_3": "#1a324a",
|
||||||
|
"field": "#091420",
|
||||||
|
"line": "#1e4a6e",
|
||||||
|
"text": "#d7ecff",
|
||||||
|
"muted": "#8aa7c0",
|
||||||
|
"accent": "#3ddcff",
|
||||||
|
"accent_2": "#5a8cff",
|
||||||
|
"danger": "#ff6a6a",
|
||||||
|
},
|
||||||
|
"sunset-orange": {
|
||||||
|
"bg": "#120d07",
|
||||||
|
"sidebar": "#0b0804",
|
||||||
|
"surface": "#221711",
|
||||||
|
"surface_2": "#2f2215",
|
||||||
|
"surface_3": "#3b2d1d",
|
||||||
|
"field": "#17100a",
|
||||||
|
"line": "#6e4a1e",
|
||||||
|
"text": "#ffe9d7",
|
||||||
|
"muted": "#c0a98a",
|
||||||
|
"accent": "#ff9d3d",
|
||||||
|
"accent_2": "#ff6a5a",
|
||||||
|
"danger": "#ff6a6a",
|
||||||
|
},
|
||||||
|
"cyber-purple": {
|
||||||
|
"bg": "#0f0711",
|
||||||
|
"sidebar": "#09050b",
|
||||||
|
"surface": "#1d0e22",
|
||||||
|
"surface_2": "#2a1133",
|
||||||
|
"surface_3": "#371a44",
|
||||||
|
"field": "#140918",
|
||||||
|
"line": "#5a1f6e",
|
||||||
|
"text": "#f0d7ff",
|
||||||
|
"muted": "#b38ac0",
|
||||||
|
"accent": "#c95aff",
|
||||||
|
"accent_2": "#6affcc",
|
||||||
|
"danger": "#ff6a6a",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 +135,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 +192,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 +211,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 +235,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 +282,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 +298,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 +317,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 +342,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 +357,14 @@ 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)
|
self.server_count_label = self._label(stats, "", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD)
|
||||||
self.server_count_label.pack(anchor="w")
|
self.server_count_label.pack(anchor="w")
|
||||||
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))
|
self._label(stats, terminal_text, bg=COLORS["surface"], fg=COLORS["accent_2"], font=FONT_SMALL).pack(anchor="w", pady=(7, 0))
|
||||||
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, 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,7 +394,7 @@ 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)
|
||||||
|
|
||||||
@@ -317,11 +402,10 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
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["bg"], troughcolor=COLORS["bg"])
|
||||||
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 +439,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 +458,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 +492,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,32 +542,71 @@ 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=FONT_SMALL,
|
||||||
|
padx=9,
|
||||||
|
pady=4,
|
||||||
|
cursor="hand2",
|
||||||
|
highlightthickness=1,
|
||||||
|
highlightbackground=COLORS["line"],
|
||||||
|
)
|
||||||
|
chip.pack(side="left", padx=(0, 6))
|
||||||
|
chip.bind("<Button-1>", lambda _event, value=group: self._set_group_filter(value))
|
||||||
|
|
||||||
|
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"]
|
||||||
@@ -487,13 +629,63 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
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))
|
||||||
|
|
||||||
|
status = self.reachability.get(index)
|
||||||
|
if status is None:
|
||||||
|
status_text, status_fg = "● prüfe ...", COLORS["muted"]
|
||||||
|
elif status:
|
||||||
|
status_text, status_fg = "● erreichbar", STATUS_OK
|
||||||
|
else:
|
||||||
|
status_text, status_fg = "● nicht erreichbar", COLORS["danger"]
|
||||||
|
status_label = self._label(inner, status_text, bg=bg, fg=status_fg, font=FONT_SMALL)
|
||||||
|
status_label.pack(anchor="w", pady=(8, 0))
|
||||||
|
self.reach_labels[index] = status_label
|
||||||
|
|
||||||
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))
|
||||||
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))
|
||||||
|
|
||||||
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()
|
||||||
@@ -509,6 +701,20 @@ class PulseGateDesktop(tk.Tk):
|
|||||||
self._label(card, command.get("name") or "Command", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(anchor="w")
|
self._label(card, command.get("name") or "Command", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(anchor="w")
|
||||||
self._label(card, command.get("command") or "", bg=COLORS["surface"], fg=COLORS["accent"], font=UI_FONT_MONO).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))
|
||||||
|
|
||||||
|
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"]
|
||||||
terminal = settings["terminal"]
|
terminal = settings["terminal"]
|
||||||
@@ -609,6 +815,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 +848,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 +891,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 +929,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,10 +987,31 @@ 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"]
|
bg = COLORS["accent"]
|
||||||
fg = "#001a10"
|
fg = "#001a10"
|
||||||
active_bg = "#42ffb5"
|
active_bg = COLORS["accent"]
|
||||||
if variant == "ghost":
|
if variant == "ghost":
|
||||||
bg = COLORS["surface_2"]
|
bg = COLORS["surface_2"]
|
||||||
fg = COLORS["text"]
|
fg = COLORS["text"]
|
||||||
|
|||||||
@@ -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.1.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"
|
||||||
|
|||||||
Reference in New Issue
Block a user