Files
server-manager/core/icons.py
2026-03-06 05:27:03 -05:00

297 lines
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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))