""" Icon registry — semantic Unicode symbols + PNG Material Design icons. Centralized icon management for buttons, tabs, menus, and type badges. PNG icons (assets/icons/dark/ + light/) auto-switch with CTk dark/light theme. If PNG files or PIL are missing, all functions gracefully fall back to Unicode. """ import os import sys from typing import Optional # ── Asset path resolution ────────────────────────────── if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): _ASSETS_DIR = os.path.join(sys._MEIPASS, "assets", "icons") else: _ASSETS_DIR = os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "icons", ) _HAS_PNG = os.path.isdir(_ASSETS_DIR) _HAS_PIL: Optional[bool] = None # lazy-checked on first ctk_icon() call # ── Semantic name → Material icon filename ───────────── ICON_FILES = { "back": "arrow_back", "up": "arrow_upward", "refresh": "refresh", "add": "add", "edit": "edit", "delete": "delete", "confirm": "check", "upload": "file_upload", "download": "file_download", "execute": "play_arrow", "info": "info", "clear": "backspace", "search": "search", "folder": "folder", "folder_open": "folder_open", "save": "save", "key": "vpn_key", "lock": "lock", "eye": "visibility", "copy": "content_copy", "gear": "settings", "globe": "language", "terminal": "code", "query": "play_arrow", "dashboards": "dashboard", "metrics": "trending_up", "powershell": "code", "launch": "computer", "totp": "lock", "objects": "storage", "connect": "play_arrow", "browser": "language", "close": "close", } # Semantic icon mapping ICONS = { # Navigation "back": "\u2190", # ← "up": "\u2191", # ↑ "refresh": "\u21bb", # ↻ # CRUD "add": "\uff0b", # + "edit": "\u270e", # ✎ "delete": "\u2715", # ✕ "confirm": "\u2713", # ✓ # Transfer "upload": "\u2b06", # ⬆ "download": "\u2b07", # ⬇ # Actions "execute": "\u25b6", # ▶ "info": "\u2139", # ℹ "clear": "\u232b", # ⌫ "search": "\U0001f50d", # 🔍 "hash": "#", # Files "folder": "\U0001f4c1", # 📁 "folder_open": "\U0001f4c2", # 📂 "save": "\U0001f4be", # 💾 # Keys & security "key": "\U0001f511", # 🔑 "lock": "\U0001f510", # 🔐 "eye": "\U0001f441", # 👁 # Clipboard "copy": "\U0001f4cb", # 📋 # Settings "gear": "\u2699", # ⚙ "globe": "\U0001f310", # 🌐 # Status "online": "\u25cf", # ● "checking": "\u25d0", # ◐ "offline": "\u2014", # — # Tabs "terminal": "\u2328", # ⌨ "query": "\u25b6", # ▶ "dashboards": "\U0001f4ca", # 📊 "metrics": "\U0001f4c8", # 📈 "powershell": "\u2328", # ⌨ "launch": "\U0001f5a5", # 🖥 "totp": "\U0001f510", # 🔐 "objects": "\U0001faa3", # 🪣 # Context menu "connect": "\u25b6", # ▶ "browser": "\U0001f310", # 🌐 "status_check": "\u25cf",# ● } def icon(name: str) -> str: """Get icon symbol by semantic name.""" return ICONS.get(name, "") def icon_text(name: str, label: str) -> str: """Format 'icon label' string for buttons/menus.""" sym = ICONS.get(name, "") if sym: return f"{sym} {label}" return label # Server type colors TYPE_COLORS = { "ssh": "#22c55e", "telnet": "#a855f7", "rdp": "#3b82f6", "vnc": "#6366f1", "winrm": "#0ea5e9", "mariadb": "#f59e0b", "mssql": "#ef4444", "postgresql": "#3b82f6", "redis": "#dc2626", "grafana": "#f97316", "prometheus": "#e11d48", "s3": "#16a34a", } # Unicode symbols for each server type (reliable, no PIL needed) TYPE_SYMBOLS = { "ssh": "\U0001f5a5", # 🖥 "telnet": "\U0001f4df", # 📟 "rdp": "\U0001f5b5", # 🖵 "vnc": "\U0001f5b5", # 🖵 "winrm": "\u229e", # ⊞ "mariadb": "\U0001f4be", # 💾 "mssql": "\U0001f4be", # 💾 "postgresql": "\U0001f418",# 🐘 "redis": "\u25c6", # ◆ "grafana": "\U0001f4ca", # 📊 "prometheus": "\U0001f525", # 🔥 "s3": "\U0001faa3", # 🪣 } # Short text labels for sidebar badge TYPE_LABELS = { "ssh": "SSH", "telnet": "TEL", "rdp": "RDP", "vnc": "VNC", "winrm": "PS", "mariadb": "MDB", "mssql": "SQL", "postgresql": "PG", "redis": "RDS", "grafana": "GRF", "prometheus": "PRM", "s3": "S3", } def type_display(server_type: str) -> str: """Return 'symbol type' for dropdown display, e.g. '🖥 ssh'.""" sym = TYPE_SYMBOLS.get(server_type, "") if sym: return f"{sym} {server_type}" return server_type def type_from_display(display: str) -> str: """Extract raw type from display string, e.g. '🖥 ssh' -> 'ssh'.""" # Strip the leading symbol + space for stype in TYPE_SYMBOLS: suffix = f" {stype}" if display.endswith(suffix) and len(display) == len(suffix) + len(TYPE_SYMBOLS.get(stype, "")): return stype # Fallback: return as-is (might already be raw type) return display.strip() # Tab icon mapping (tab_key -> icon_name) TAB_ICONS = { "terminal": "terminal", "files": "folder", "info": "info", "keys": "key", "totp": "totp", "setup": "gear", "query": "query", "console": "terminal", "dashboards": "dashboards", "metrics": "metrics", "powershell": "powershell", "launch": "launch", "objects": "objects", } # Context menu icon mapping (i18n_key -> icon_name) CTX_ICONS = { "ctx_open_terminal": "terminal", "ctx_browse_files": "folder", "ctx_install_key": "key", "ctx_open_powershell": "powershell", "ctx_open_query": "query", "ctx_open_console": "terminal", "ctx_connect": "connect", "ctx_open_browser": "browser", "ctx_check_status": "status_check", "ctx_copy_alias": "copy", "ctx_disconnect": "close", "edit": "edit", "delete": "delete", } # ── CTkImage cache + loader ──────────────────────────── _icon_cache: dict[tuple, object] = {} def ctk_icon(name: str, size: int = 20) -> Optional[object]: """PNG icon as CTkImage, or None (fallback to Unicode). Lazy-imports PIL and customtkinter on first call so that icons.py stays usable as a pure data module for CLI tools. """ global _HAS_PIL if _HAS_PIL is None: try: from PIL import Image as _img # noqa: F401 import customtkinter as _ctk # noqa: F401 _HAS_PIL = True except ImportError: _HAS_PIL = False if not _HAS_PIL or not _HAS_PNG: return None cache_key = (name, size) if cache_key in _icon_cache: return _icon_cache[cache_key] file_stem = ICON_FILES.get(name) if not file_stem: return None from PIL import Image import customtkinter as ctk dark_path = os.path.join(_ASSETS_DIR, "dark", f"{file_stem}.png") light_path = os.path.join(_ASSETS_DIR, "light", f"{file_stem}.png") if not os.path.exists(dark_path) or not os.path.exists(light_path): return None try: light_img = Image.open(light_path) # black icons for light bg dark_img = Image.open(dark_path) # white icons for dark bg result = ctk.CTkImage( light_image=light_img, dark_image=dark_img, size=(size, size), ) _icon_cache[cache_key] = result return result except Exception: return None def make_icon_button(parent, icon_name: str, label: str, icon_size: int = 16, **kwargs): """CTkButton with PNG icon or Unicode fallback.""" import customtkinter as _ctk img = ctk_icon(icon_name, icon_size) if img: return _ctk.CTkButton(parent, text=label, image=img, compound="left", **kwargs) return _ctk.CTkButton(parent, text=icon_text(icon_name, label), **kwargs) def make_icon_label(parent, icon_name: str, icon_size: int = 18, **kwargs): """CTkLabel with PNG icon or Unicode fallback.""" import customtkinter as _ctk img = ctk_icon(icon_name, icon_size) if img: return _ctk.CTkLabel(parent, text="", image=img, **kwargs) return _ctk.CTkLabel(parent, text=ICONS.get(icon_name, ""), **kwargs) def reconfigure_icon_button(btn, icon_name: str, label: str, icon_size: int = 16): """Update existing button text + image (for update_language).""" img = ctk_icon(icon_name, icon_size) if img: btn.configure(text=label, image=img) else: btn.configure(text=icon_text(icon_name, label))