@@ -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 " : " #050c 09 " ,
" bg " : " #050d 09 " ,
" 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 " ,
" muted " : " #86a998 " ,
" accent " : " #00ff99 " ,
" accent " : " #00ff99 " ,
" accent_2 " : " #33c cff " ,
" accent_hi " : " #5 cffb9 " ,
" danger " : " #ff6666 " ,
" 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 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 , " 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 [ " 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 :
if variant == " primary " :
bg = COLORS [ " accent " ]
bg = COLORS [ " accent " ]
fg = " #001a10 "
fg = " #001a10 "
active_bg = " #42ffb5 "
hover_bg = COLORS [ " accent_hi " ]
if variant == " ghost " :
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