@@ -5,8 +5,10 @@ from __future__ import annotations
import os
import shlex
import shutil
import socket
import subprocess
import sys
import threading
import tkinter as tk
from pathlib import Path
from tkinter import messagebox
@@ -15,24 +17,77 @@ import yaml
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 "
CONNECT_TIMEOUT = 2.5
STATUS_OK = " #00e07a "
COLORS = {
" bg " : " #07110d " ,
" sidebar " : " #050c 09 " ,
" surface " : " #0d1b15 " ,
" surface_2 " : " #13251e " ,
" surface_3 " : " #172c24 " ,
" field " : " #09150f " ,
" line " : " #1f4b38 " ,
" text " : " #d7ffe9 " ,
" muted " : " #8aa99b " ,
" accent " : " #00ff 99 " ,
" accent_2 " : " #33ccff " ,
" danger " : " #ff6666 " ,
THEMES = {
" neon-green " : {
" bg " : " #050d 09 " ,
" sidebar " : " #040b08 " ,
" surface " : " #0c1a13 " ,
" surface_2 " : " #12241b " ,
" surface_3 " : " #193127 " ,
" field " : " #08130e " ,
" line " : " #1d4a37 " ,
" text " : " #d9ffe9 " ,
" muted " : " #86a 998 " ,
" accent " : " #00ff99 " ,
" 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_SMALL = ( " Segoe UI " , 9 ) if IS_WINDOWS else ( " Inter " , 9 )
UI_FONT_MONO = ( " Consolas " , 10 ) if IS_WINDOWS else ( " JetBrains Mono " , 10 )
@@ -84,17 +139,19 @@ def default_config() -> dict:
" quick_commands " : [
{ " name " : " Disk Usage " , " command " : " df -h " } ,
{ " name " : " RAM Usage " , " command " : " free -h " } ,
{ " name " : " Uptime " , " command " : " uptime " } ,
{ " name " : " Container Status " , " command " : " docker ps " } ,
] ,
}
def normalize_config ( config : dict ) - > dict :
config . setdefault ( " settings " , { } )
config [ " settings " ] . setdefault ( " theme " , " neon-green " )
config [ " settings " ] . setdefault ( " terminal " , { } )
config [ " settings " ] [ " terminal " ] . setdefault ( " term " , " xterm-256color " )
config [ " settings " ] [ " terminal " ] . setdefault ( " enable_kitty_fix " , True )
settings = config [ " settings " ]
settings . setdefault ( " theme " , " neon-green " )
settings . setdefault ( " quick_commands_enabled " , True )
settings . setdefault ( " terminal " , { } )
settings [ " terminal " ] . setdefault ( " term " , " xterm-256color " )
settings [ " terminal " ] . setdefault ( " enable_kitty_fix " , True )
config . setdefault ( " servers " , [ ] )
config . setdefault ( " quick_commands " , [ ] )
@@ -139,7 +196,7 @@ def expand_home(value: str) -> str:
if IS_WINDOWS :
profile = os . environ . get ( " USERPROFILE " )
if profile :
return os . path . join ( profile , value [ 2 : ] . replace ( " / " , " \\ " ) )
return profile + " \\ " + value [ 2 : ] . replace ( " / " , " \\ " )
return str ( Path ( value ) . expanduser ( ) )
@@ -158,6 +215,15 @@ def display_command(server: dict) -> str:
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 :
if IS_WINDOWS :
for name in ( " wt " , " cmd " , " powershell " ) :
@@ -173,37 +239,44 @@ def detect_terminal() -> str | None:
return None
def terminal_command ( terminal : str , server : dict ) - > list [ str ] :
title = f " PulseGate - { server . get ( ' name ' ) or server . get ( ' host ' ) } "
def open_in_terminal ( terminal : str , title : str , command : str , hold : bool = True , done_message : str = " Fertig. " ) - > list [ str ] :
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 "
suffix = f " ; Write-Host ' ' ; Write-Host ' { done_message } ' ; Read-Host ' Enter zum Schliessen... ' " if hold else " "
return [ terminal , " -NoExit " , " -Command " , f " & { command } { suffix } " ]
suffix = f " & echo. & echo { done_message } & pause " if hold else " "
inner = command + suffix
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 _ "
return [ terminal , " new-tab " , " --title " , title , " cmd.exe " , " /k " , inner ]
return [ terminal , " /k " , inner ]
suffix = f " ; printf ' \\ n { done_message } Enter zum Schließen... ' ; read _ " if hold else " "
inner = command + suffix
name = Path ( terminal ) . name
if name == " kitty " :
return [ terminal , " --title " , title , " sh " , " -lc " , hold ]
return [ terminal , " --title " , title , " sh " , " -lc " , inner ]
if name == " alacritty " :
return [ terminal , " --title " , title , " -e " , " sh " , " -lc " , hold ]
return [ terminal , " --title " , title , " -e " , " sh " , " -lc " , inner ]
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 " :
return [ terminal , " --title " , title , " -- " , " sh " , " -lc " , hold ]
return [ terminal , " --title " , title , " -- " , " sh " , " -lc " , inner ]
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 " :
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 ) :
@@ -213,7 +286,13 @@ class PulseGateDesktop(tk.Tk):
self . config_data = load_config ( path )
self . selected_index = 0
self . current_view = " servers "
self . group_filter = " Alle "
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 . settings_vars : dict [ str , tk . Variable ] = { }
@@ -223,13 +302,17 @@ class PulseGateDesktop(tk.Tk):
self . status_var = tk . StringVar ( value = " Bereit " )
self . title ( APP_TITLE )
self . geometry ( " 1180x760 " )
self . minsize ( 980 , 640 )
self . geometry ( " 1240x800 " )
self . minsize ( 1000 , 640 )
self . configure ( bg = COLORS [ " bg " ] )
self . _build_layout ( )
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 :
root = tk . Frame ( self , bg = COLORS [ " bg " ] )
root . pack ( fill = " both " , expand = True )
@@ -238,12 +321,12 @@ class PulseGateDesktop(tk.Tk):
self . _build_content ( root )
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_propagate ( False )
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 (
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 )
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 . _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 . pack ( fill = " x " , padx = 22 )
for view , label in ( ( " servers " , " Server " ) , ( " commands " , " Commands " ) , ( " settings " , " Settings " ) ) :
@@ -274,12 +361,19 @@ class PulseGateDesktop(tk.Tk):
self . nav_buttons [ view ] = button
stats = self . _card ( sidebar , bg = COLORS [ " surface " ] , padx = 14 , pady = 12 )
stats . pack ( fill = " x " , padx = 22 , pady = ( 28 , 0 ) )
self . server_count_label = self . _label ( stats , " " , bg = COLORS [ " surface " ] , fg = COLORS [ " text " ] , font = FONT_CARD )
self . server_count_label . pack ( anchor = " w " )
stats . pack ( fill = " x " , padx = 22 , pady = ( 28 , 10 ) )
head = tk . Frame ( stats , bg = COLORS [ " surface " ] )
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 "
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 ) )
terminal_fg = STATUS_OK if self . terminal else COLORS [ " danger " ]
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 :
content = tk . Frame ( parent , bg = COLORS [ " bg " ] )
@@ -309,19 +403,28 @@ class PulseGateDesktop(tk.Tk):
self . _build_settings_page ( self . pages [ " settings " ] )
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_propagate ( False )
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 . 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 . pack ( side = " left " , fill = " both " , expand = True )
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 . pack ( side = " left " , fill = " both " , expand = True )
@@ -355,18 +458,7 @@ class PulseGateDesktop(tk.Tk):
var = tk . StringVar ( )
self . vars [ key ] = var
if key == " auth " :
widget = tk . OptionMenu ( 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 " )
widget = self . _option_menu ( cell , var , ( " key " , " password " ) )
else :
widget = self . _entry ( cell , var )
widget . pack ( fill = " x " , pady = ( 6 , 0 ) , ipady = 3 )
@@ -385,18 +477,31 @@ class PulseGateDesktop(tk.Tk):
activeforeground = COLORS [ " text " ] ,
selectcolor = COLORS [ " field " ] ,
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 . pack ( fill = " x " , pady = ( 16 , 14 ) )
actions . pack ( fill = " x " )
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 , " 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 , " 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 . command_text = tk . Text (
editor_outer ,
height = 6 ,
height = 5 ,
bg = COLORS [ " field " ] ,
fg = COLORS [ " accent " ] ,
insertbackground = COLORS [ " text " ] ,
@@ -406,27 +511,44 @@ class PulseGateDesktop(tk.Tk):
padx = 12 ,
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 :
self . commands_host = tk . Frame ( page , bg = COLORS [ " bg " ] )
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 :
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 = ( UI_FONT [ 0 ] , 18 , " bold " ) ) . pack ( anchor = " w " )
self . _label ( panel , " Diese Wert e we rden beim Starten einer SSH-Verbindung genutz t. " , bg = COLORS [ " surface " ] , fg = COLORS [ " muted " ] , font = FONT_SMALL ) . pack ( anchor = " w " , pady = ( 4 , 18 ) )
self . _label ( panel , " Darstellung " , bg = COLORS [ " surface " ] , fg = COLORS [ " text " ] , font = ( UI_FONT [ 0 ] , 18 , " bold " ) ) . pack ( anchor = " w " )
self . _label ( panel , " Das Them e wi rd beim App- Start angewand t. " , 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 [ " term " ] = tk . StringVar ( )
self . settings_vars [ " enable_kitty_fix " ] = tk . BooleanVar ( )
self . _option_menu ( panel , self . settings_vars [ " theme " ] , tuple ( THEMES . keys ( ) ) ) . pack ( fill = " x " , pady = ( 6 , 18 ) , ipady = 3 )
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 . _entry ( panel , self . settings_vars [ key ] ) . pack ( fill = " x " , pady = ( 6 , 14 ) , ipady = 3 )
self . settings_vars [ " enable_kitty_fix " ] = tk . BooleanVar ( )
tk . Checkbutton (
panel ,
text = " Kitty Fix global aktivieren " ,
@@ -439,61 +561,179 @@ class PulseGateDesktop(tk.Tk):
font = FONT ,
) . 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 :
self . server_count_label . configure ( text = f " { len ( self . config_data [ ' servers ' ] ) } Server " )
self . _refresh_group_chips ( )
self . _refresh_server_cards ( )
self . _load_selected_server ( )
self . _refresh_commands ( )
self . _refresh_settings ( )
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 :
for child in self . server_cards . winfo_children ( ) :
child . destroy ( )
self . reach_labels . clear ( )
query = self . search_var . get ( ) . strip ( ) . lower ( )
query = ( self . search_var . get ( ) or " " ) . strip ( ) . lower ( )
rendered = 0
for index , server in enumerate ( self . config_data [ " servers " ] ) :
haystack = " " . join ( str ( server . get ( key , " " ) ) for key in ( " name " , " host " , " user " , " group " , " auth " ) ) . lower ( )
if query and query not in haystack :
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 ) )
rendered + = 1
self . _check_reachability ( server , index )
if rendered == 0 :
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 :
selected = index == self . selected_index
bg = COLORS [ " surface_2 " ] if selected else COLORS [ " surface " ]
border = COLORS [ " accent " ] if selected else COLORS [ " line " ]
card = tk . Frame ( parent , bg = border , bd = 0 )
inner = tk . Frame ( card , bg = bg , padx = 14 , pady = 12 )
inner . pack ( fill = " both " , expand = True , padx = 1 , pady = 1 )
target = f " { server . get ( ' user ' ) } @ { server . get ( ' host ' ) } : { server . get ( ' port ' ) or 22 } "
name = server . get ( " name " ) or " Unbenannt "
group = server . get ( " group " ) or " Keine Gruppe "
head = tk . Frame ( inner , bg = bg )
head . pack ( fill = " x " , pady = ( 0 , 6 ) )
self . _label ( inner , name , bg = bg , fg = COLORS [ " text " ] , font = FONT_CARD ) . pack ( anchor = " w " )
self . _label ( inner , target , bg = bg , fg = COLORS [ " muted " ] , font = FONT_SMALL ) . pack ( anchor = " w " , pady = ( 5 , 8 ) )
name = server . get ( " name " ) or " Unbenannt "
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 . 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 ) )
if server . get ( " kitty_fix " ) :
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 ) :
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 ( ) :
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
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 :
for child in self . commands_host . winfo_children ( ) :
child . destroy ( )
@@ -506,8 +746,27 @@ class PulseGateDesktop(tk.Tk):
for command in commands :
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 = 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 :
settings = self . config_data [ " settings " ]
@@ -609,6 +868,18 @@ class PulseGateDesktop(tk.Tk):
self . current_view = " servers "
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 :
if not self . config_data [ " servers " ] :
return
@@ -630,7 +901,29 @@ class PulseGateDesktop(tk.Tk):
} ,
}
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 :
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 . 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 :
server = self . _current_server ( )
if not server :
@@ -675,9 +982,46 @@ class PulseGateDesktop(tk.Tk):
self . status_var . set ( f " SSH gestartet: { server . get ( ' name ' ) } " )
self . _show_command ( )
def _on_mousewheel ( self , event : tk . Event ) - > None :
if self . current_view == " servers " :
self . server_canvas . yview_scroll ( int ( - 1 * ( event . delta / 120 ) ) , " units " )
def _run_quick_command ( self , command : dict , via_ssh : bool = False ) - > None :
if not self . terminal :
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 :
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 ,
)
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 :
bg = COLORS [ " accent " ]
fg = " #001a10 "
active_bg = " #42ffb5 "
if variant == " ghost " :
if variant == " primary " :
bg = COLORS [ " accent " ]
fg = " #001a10 "
hover_bg = COLORS [ " accent_hi " ]
hover_fg = " #001a10 "
elif variant == " ghost " :
bg = COLORS [ " surface_2 " ]
fg = COLORS [ " text " ]
active_bg = COLORS [ " surface_3 " ]
elif variant == " danger " :
bg = " #351414 "
hover_bg = COLORS [ " surface_3 " ]
hover_fg = COLORS [ " text " ]
else :
bg = " #3a1717 "
fg = COLORS [ " danger " ]
active_bg = " #4a1d1d "
hover_bg = " #522020 "
hover_fg = " #ffd0d0 "
return tk . Button (
button = tk . Button (
parent ,
text = text ,
command = command ,
bg = bg ,
fg = fg ,
activebackground = active_bg ,
activeforeground = fg ,
activebackground = hover_bg ,
activeforeground = hover_fg ,
relief = " flat " ,
bd = 0 ,
padx = 14 ,
pady = 9 ,
font = ( UI_FONT [ 0 ] , 10 , " bold " ) ,
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 :
return tk . Button (
button = tk . Button (
parent ,
text = text ,
command = command ,
@@ -745,6 +1118,24 @@ class PulseGateDesktop(tk.Tk):
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 :
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 ,
bg = bg or parent . cget ( " bg " ) ,
fg = fg ,
font = FONT_SMALL ,
font = ( UI_FONT [ 0 ] , 9 , " bold " ) ,
padx = 7 ,
pady = 3 ,
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 )
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