#!/usr/bin/env python3 from __future__ import annotations import os import shlex import shutil import subprocess import sys import tkinter as tk from pathlib import Path from tkinter import messagebox import yaml APP_TITLE = "PulseGate Desktop" COLORS = { "bg": "#07110d", "sidebar": "#050c09", "surface": "#0d1b15", "surface_2": "#13251e", "surface_3": "#172c24", "field": "#09150f", "line": "#1f4b38", "text": "#d7ffe9", "muted": "#8aa99b", "accent": "#00ff99", "accent_2": "#33ccff", "danger": "#ff6666", } FONT = ("Inter", 11) FONT_SMALL = ("Inter", 9) FONT_TITLE = ("Inter", 24, "bold") FONT_CARD = ("Inter", 13, "bold") def config_path() -> Path: if len(sys.argv) > 1: return Path(sys.argv[1]).expanduser() config_home = os.environ.get("XDG_CONFIG_HOME") if config_home: return Path(config_home) / "pulsegate" / "config.yaml" return Path.home() / ".config" / "pulsegate" / "config.yaml" def default_config() -> dict: return { "settings": { "theme": "neon-green", "terminal": { "term": "xterm-256color", "enable_kitty_fix": True, }, }, "servers": [ { "name": "Example Server", "host": "10.0.0.10", "user": "root", "port": 22, "group": "Homelab", "auth": "key", "key": "~/.ssh/id_ed25519", "password_id": "", "kitty_fix": True, } ], "quick_commands": [ {"name": "Disk Usage", "command": "df -h"}, {"name": "RAM Usage", "command": "free -h"}, {"name": "Uptime", "command": "uptime"}, ], } 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) config.setdefault("servers", []) config.setdefault("quick_commands", []) for server in config["servers"]: server.setdefault("name", "") server.setdefault("host", "") server.setdefault("user", "root") server.setdefault("port", 22) server.setdefault("group", "Homelab") server.setdefault("auth", "key") server.setdefault("key", "") server.setdefault("password_id", "") server.setdefault("kitty_fix", True) return config def load_config(path: Path) -> dict: if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) save_config(path, default_config()) with path.open("r", encoding="utf-8") as handle: data = yaml.safe_load(handle) or {} return normalize_config(data) def save_config(path: Path, config: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8") as handle: yaml.safe_dump(normalize_config(config), handle, sort_keys=False, allow_unicode=True) path.chmod(0o600) def expand_home(value: str) -> str: return str(Path(value).expanduser()) if value.startswith("~/") else value def ssh_args(server: dict) -> list[str]: args = ["ssh", "-p", str(server.get("port") or 22)] if server.get("auth") == "key" and server.get("key"): args.extend(["-i", expand_home(str(server["key"]))]) args.append(f"{server.get('user', 'root')}@{server.get('host', '')}") return args def detect_terminal() -> str | None: for name in ("kitty", "alacritty", "konsole", "gnome-terminal", "xfce4-terminal", "xterm"): path = shutil.which(name) if path: return path return None def terminal_command(terminal: str, server: dict) -> list[str]: title = f"PulseGate - {server.get('name') or server.get('host')}" command = " ".join(shlex.quote(arg) for arg in ssh_args(server)) hold_command = f"{command}; printf '\\nSSH beendet. Enter zum Schliessen...'; read _" name = Path(terminal).name if name == "kitty": return [terminal, "--title", title, "sh", "-lc", hold_command] if name == "alacritty": return [terminal, "--title", title, "-e", "sh", "-lc", hold_command] if name == "konsole": return [terminal, "--new-tab", "-p", f"tabtitle={title}", "-e", "sh", "-lc", hold_command] if name == "gnome-terminal": return [terminal, "--title", title, "--", "sh", "-lc", hold_command] if name == "xfce4-terminal": return [terminal, "--title", title, "--command", f"sh -lc {shlex.quote(hold_command)}"] if name == "xterm": return [terminal, "-T", title, "-e", "sh", "-lc", hold_command] return [terminal, "-e", "sh", "-lc", hold_command] class PulseGateDesktop(tk.Tk): def __init__(self, path: Path) -> None: super().__init__() self.path = path self.config_data = load_config(path) self.selected_index = 0 self.current_view = "servers" self.terminal = detect_terminal() self.vars: dict[str, tk.Variable] = {} self.settings_vars: dict[str, tk.Variable] = {} self.nav_buttons: dict[str, tk.Button] = {} self.pages: dict[str, tk.Frame] = {} self.search_var = tk.StringVar() self.status_var = tk.StringVar(value="Bereit") self.title(APP_TITLE) self.geometry("1180x760") self.minsize(980, 640) self.configure(bg=COLORS["bg"]) self._build_layout() self._refresh_all() def _build_layout(self) -> None: root = tk.Frame(self, bg=COLORS["bg"]) root.pack(fill="both", expand=True) self._build_sidebar(root) self._build_content(root) def _build_sidebar(self, parent: tk.Frame) -> None: sidebar = tk.Frame(parent, bg=COLORS["sidebar"], width=282) 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)) mark = tk.Label( brand, text="PG", bg=COLORS["accent"], fg="#001a10", font=("Inter", 14, "bold"), width=4, height=2, ) mark.pack(side="left") brand_text = tk.Frame(brand, bg=COLORS["sidebar"]) brand_text.pack(side="left", padx=12) tk.Label(brand_text, text="PulseGate", bg=COLORS["sidebar"], fg=COLORS["text"], font=("Inter", 18, "bold")).pack(anchor="w") tk.Label(brand_text, text="Desktop SSH", bg=COLORS["sidebar"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w") self._label(sidebar, "Suche", bg=COLORS["sidebar"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", padx=22) search = self._entry(sidebar, self.search_var) search.pack(fill="x", padx=22, pady=(7, 22)) self.search_var.trace_add("write", lambda *_: self._refresh_server_cards()) nav = tk.Frame(sidebar, bg=COLORS["sidebar"]) nav.pack(fill="x", padx=22) for view, label in (("servers", "Server"), ("commands", "Commands"), ("settings", "Settings")): button = self._nav_button(nav, label, lambda value=view: self._show_view(value)) button.pack(fill="x", pady=4) 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") 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)) def _build_content(self, parent: tk.Frame) -> None: content = tk.Frame(parent, bg=COLORS["bg"]) content.pack(side="left", fill="both", expand=True, padx=24, pady=22) topbar = tk.Frame(content, bg=COLORS["bg"]) topbar.pack(fill="x", pady=(0, 18)) title_box = tk.Frame(topbar, bg=COLORS["bg"]) title_box.pack(side="left", fill="x", expand=True) tk.Label(title_box, textvariable=self.status_var, bg=COLORS["bg"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w") self.title_label = tk.Label(title_box, text="Server", bg=COLORS["bg"], fg=COLORS["text"], font=FONT_TITLE) self.title_label.pack(anchor="w") self._button(topbar, "Neu laden", self._reload, variant="ghost").pack(side="right", padx=(8, 0)) self._button(topbar, "Server hinzufuegen", self._add_server).pack(side="right") self.page_host = tk.Frame(content, bg=COLORS["bg"]) self.page_host.pack(fill="both", expand=True) self.pages["servers"] = tk.Frame(self.page_host, bg=COLORS["bg"]) self.pages["commands"] = tk.Frame(self.page_host, bg=COLORS["bg"]) self.pages["settings"] = tk.Frame(self.page_host, bg=COLORS["bg"]) self._build_servers_page(self.pages["servers"]) self._build_commands_page(self.pages["commands"]) 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.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"]) self.server_cards = tk.Frame(self.server_canvas, bg=COLORS["bg"]) self.server_cards.bind("", 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.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("", self._on_mousewheel) editor_outer = self._card(page, bg=COLORS["surface"], padx=18, pady=18) editor_outer.pack(side="left", fill="both", expand=True) header = tk.Frame(editor_outer, bg=COLORS["surface"]) header.pack(fill="x", pady=(0, 16)) self.form_title = self._label(header, "Server bearbeiten", bg=COLORS["surface"], fg=COLORS["text"], font=("Inter", 18, "bold")) self.form_title.pack(side="left") self.auth_chip = self._chip(header, "key", COLORS["accent_2"]) self.auth_chip.pack(side="right") form = tk.Frame(editor_outer, bg=COLORS["surface"]) form.pack(fill="x") fields = [ ("name", "Name"), ("host", "Host"), ("user", "User"), ("port", "Port"), ("group", "Group"), ("auth", "Auth"), ("key", "Key Path"), ("password_id", "Password ID"), ] for index, (key, label) in enumerate(fields): row = index // 2 col = index % 2 cell = tk.Frame(form, bg=COLORS["surface"]) cell.grid(row=row, column=col, sticky="ew", padx=(0 if col == 0 else 10, 10 if col == 0 else 0), pady=8) self._label(cell, label, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w") 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") else: widget = self._entry(cell, var) widget.pack(fill="x", pady=(6, 0), ipady=3) form.columnconfigure(0, weight=1) form.columnconfigure(1, weight=1) self.vars["kitty_fix"] = tk.BooleanVar(value=True) tk.Checkbutton( editor_outer, text="Kitty Fix fuer diesen Server nutzen", variable=self.vars["kitty_fix"], bg=COLORS["surface"], fg=COLORS["text"], activebackground=COLORS["surface"], activeforeground=COLORS["text"], selectcolor=COLORS["field"], font=FONT, ).pack(anchor="w", pady=(12, 4)) actions = tk.Frame(editor_outer, bg=COLORS["surface"]) actions.pack(fill="x", pady=(16, 14)) self._button(actions, "Verbinden", self._connect).pack(side="left") self._button(actions, "SSH Befehl", self._show_command, variant="ghost").pack(side="left", padx=(8, 0)) self._button(actions, "Loeschen", self._delete_current_server, variant="danger").pack(side="right") self._button(actions, "Speichern", self._save_current_server).pack(side="right", padx=(0, 8)) self.command_text = tk.Text( editor_outer, height=6, bg=COLORS["field"], fg=COLORS["accent"], insertbackground=COLORS["text"], relief="flat", wrap="word", font=("JetBrains Mono", 10), padx=12, pady=10, ) self.command_text.pack(fill="both", expand=True) 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) 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=("Inter", 18, "bold")).pack(anchor="w") self._label(panel, "Diese Werte werden beim Starten einer SSH-Verbindung genutzt.", bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", pady=(4, 18)) self.settings_vars["theme"] = tk.StringVar() self.settings_vars["term"] = tk.StringVar() self.settings_vars["enable_kitty_fix"] = tk.BooleanVar() for label, key in (("Theme", "theme"), ("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) tk.Checkbutton( panel, text="Kitty Fix global aktivieren", variable=self.settings_vars["enable_kitty_fix"], bg=COLORS["surface"], fg=COLORS["text"], activebackground=COLORS["surface"], activeforeground=COLORS["text"], selectcolor=COLORS["field"], font=FONT, ).pack(anchor="w", pady=(0, 18)) self._button(panel, "Settings speichern", self._save_settings).pack(anchor="e") def _refresh_all(self) -> None: self.server_count_label.configure(text=f"{len(self.config_data['servers'])} Server") self._refresh_server_cards() self._load_selected_server() self._refresh_commands() self._refresh_settings() self._show_view(self.current_view) def _refresh_server_cards(self) -> None: for child in self.server_cards.winfo_children(): child.destroy() query = self.search_var.get().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 self._server_card(self.server_cards, server, index).pack(fill="x", pady=(0, 10)) rendered += 1 if rendered == 0: self._empty_state(self.server_cards, "Keine Server gefunden").pack(fill="x") 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" 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)) 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("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)) for widget in (card, inner): widget.bind("", lambda _event, value=index: self._select_server(value)) for child in inner.winfo_children(): child.bind("", lambda _event, value=index: self._select_server(value)) return card def _refresh_commands(self) -> None: for child in self.commands_host.winfo_children(): child.destroy() commands = self.config_data["quick_commands"] if not commands: self._empty_state(self.commands_host, "Keine Quick Commands konfiguriert").pack(fill="x") return 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=("JetBrains Mono", 10)).pack(anchor="w", pady=(7, 0)) def _refresh_settings(self) -> None: settings = self.config_data["settings"] terminal = settings["terminal"] self.settings_vars["theme"].set(settings.get("theme", "neon-green")) self.settings_vars["term"].set(terminal.get("term", "xterm-256color")) self.settings_vars["enable_kitty_fix"].set(bool(terminal.get("enable_kitty_fix", True))) def _show_view(self, view: str) -> None: self.current_view = view for page in self.pages.values(): page.pack_forget() self.pages[view].pack(fill="both", expand=True) titles = {"servers": "Server", "commands": "Quick Commands", "settings": "Settings"} self.title_label.configure(text=titles[view]) for name, button in self.nav_buttons.items(): active = name == view button.configure( bg=COLORS["surface_2"] if active else COLORS["sidebar"], fg=COLORS["accent"] if active else COLORS["text"], highlightbackground=COLORS["line"] if active else COLORS["sidebar"], ) def _select_server(self, index: int) -> None: self.selected_index = index self._refresh_server_cards() self._load_selected_server() def _load_selected_server(self) -> None: if not self.config_data["servers"]: self.form_title.configure(text="Kein Server") for variable in self.vars.values(): variable.set(False if isinstance(variable, tk.BooleanVar) else "") return self.selected_index = min(self.selected_index, len(self.config_data["servers"]) - 1) server = self.config_data["servers"][self.selected_index] self.form_title.configure(text=f"{server.get('name') or 'Server'} bearbeiten") self.auth_chip.configure(text=server.get("auth") or "key") for key, variable in self.vars.items(): if key == "kitty_fix": variable.set(bool(server.get(key, True))) else: variable.set(str(server.get(key, ""))) def _collect_server(self) -> dict | None: name = self.vars["name"].get().strip() host = self.vars["host"].get().strip() user = self.vars["user"].get().strip() if not name or not host or not user: messagebox.showerror(APP_TITLE, "Name, Host und User sind Pflichtfelder.") return None try: port = int(self.vars["port"].get() or "22") except ValueError: messagebox.showerror(APP_TITLE, "Port muss eine Zahl sein.") return None return { "name": name, "host": host, "user": user, "port": port, "group": self.vars["group"].get().strip(), "auth": self.vars["auth"].get().strip() or "key", "key": self.vars["key"].get().strip(), "password_id": self.vars["password_id"].get().strip(), "kitty_fix": bool(self.vars["kitty_fix"].get()), } def _save_current_server(self) -> None: server = self._collect_server() if server is None: return self.config_data["servers"][self.selected_index] = server save_config(self.path, self.config_data) self.status_var.set("Server gespeichert") self._refresh_all() def _add_server(self) -> None: self.config_data["servers"].append( { "name": "Neuer Server", "host": "", "user": "root", "port": 22, "group": "Homelab", "auth": "key", "key": "", "password_id": "", "kitty_fix": True, } ) self.selected_index = len(self.config_data["servers"]) - 1 self.current_view = "servers" self._refresh_all() def _delete_current_server(self) -> None: if not self.config_data["servers"]: return server = self.config_data["servers"][self.selected_index] if not messagebox.askyesno(APP_TITLE, f"{server.get('name')} wirklich loeschen?"): return del self.config_data["servers"][self.selected_index] self.selected_index = max(0, self.selected_index - 1) save_config(self.path, self.config_data) self.status_var.set("Server geloescht") self._refresh_all() def _save_settings(self) -> None: self.config_data["settings"] = { "theme": self.settings_vars["theme"].get().strip() or "neon-green", "terminal": { "term": self.settings_vars["term"].get().strip() or "xterm-256color", "enable_kitty_fix": bool(self.settings_vars["enable_kitty_fix"].get()), }, } save_config(self.path, self.config_data) self.status_var.set("Settings gespeichert") def _reload(self) -> None: self.config_data = load_config(self.path) self.selected_index = min(self.selected_index, max(0, len(self.config_data["servers"]) - 1)) self.status_var.set("Neu geladen") self._refresh_all() def _current_server(self) -> dict | None: if not self.config_data["servers"]: messagebox.showerror(APP_TITLE, "Kein Server ausgewaehlt.") return None return self.config_data["servers"][self.selected_index] def _show_command(self) -> None: server = self._current_server() if not server: return self.command_text.delete("1.0", "end") self.command_text.insert("1.0", " ".join(shlex.quote(arg) for arg in ssh_args(server))) def _connect(self) -> None: server = self._current_server() if not server: return if not self.terminal: messagebox.showerror(APP_TITLE, "Kein unterstuetzter Terminal-Emulator gefunden.") return env = os.environ.copy() settings = self.config_data["settings"] terminal_settings = settings.get("terminal", {}) if terminal_settings.get("enable_kitty_fix", True) and server.get("kitty_fix", True): env["TERM"] = terminal_settings.get("term") or "xterm-256color" try: subprocess.Popen(terminal_command(self.terminal, server), env=env) except OSError as error: messagebox.showerror(APP_TITLE, str(error)) return 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 _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) def _entry(self, parent: tk.Widget, variable: tk.StringVar) -> tk.Entry: return tk.Entry( parent, textvariable=variable, bg=COLORS["field"], fg=COLORS["text"], insertbackground=COLORS["text"], relief="flat", highlightthickness=1, highlightbackground=COLORS["line"], highlightcolor=COLORS["accent"], font=FONT, ) 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": bg = COLORS["surface_2"] fg = COLORS["text"] active_bg = COLORS["surface_3"] elif variant == "danger": bg = "#351414" fg = COLORS["danger"] active_bg = "#4a1d1d" return tk.Button( parent, text=text, command=command, bg=bg, fg=fg, activebackground=active_bg, activeforeground=fg, relief="flat", bd=0, padx=14, pady=9, font=("Inter", 10, "bold"), cursor="hand2", ) def _nav_button(self, parent: tk.Widget, text: str, command) -> tk.Button: return tk.Button( parent, text=text, command=command, bg=COLORS["sidebar"], fg=COLORS["text"], activebackground=COLORS["surface_2"], activeforeground=COLORS["accent"], relief="flat", bd=0, padx=12, pady=10, anchor="w", font=("Inter", 10, "bold"), cursor="hand2", highlightthickness=1, highlightbackground=COLORS["sidebar"], ) 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"]) def _chip(self, parent: tk.Widget, text: str, fg: str, *, bg: str | None = None) -> tk.Label: return tk.Label( parent, text=text, bg=bg or parent.cget("bg"), fg=fg, font=FONT_SMALL, padx=7, pady=3, highlightthickness=1, highlightbackground=COLORS["line"], ) def _empty_state(self, parent: tk.Widget, text: 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") return card if __name__ == "__main__": app = PulseGateDesktop(config_path()) app.mainloop()