v1.9.1: PNG Material Design icons — 28 icons, dark/light theme, HiDPI, graceful Unicode fallback

- 56 PNG icons (28 unique × 2 color variants) from Material Design Icons (round style, 96×96px)
- core/icons.py: ctk_icon(), make_icon_button(), reconfigure_icon_button() with CTkImage cache
- Updated 15 GUI files: app.py, sidebar.py, server_dialog.py, all tabs
- build.py: auto-include assets/icons/ in PyInstaller bundle, patch rollover at 99→minor+1
- tools/download_icons.py: icon download script
- Automatic dark↔light theme switching via CTkImage dual-image support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-03-03 07:27:49 -05:00
parent 9b0e4c76a3
commit 1e729fcf3a
76 changed files with 397 additions and 148 deletions

View File

@@ -11,7 +11,7 @@ from core.status_checker import StatusChecker
from core.updater import UpdateChecker
from core import i18n
from core.i18n import t, LANGUAGES
from core.icons import icon, TAB_ICONS
from core.icons import icon, TAB_ICONS, ctk_icon
from core.session_pool import SessionPool
from gui.sidebar import Sidebar
from gui.server_dialog import ServerDialog
@@ -147,7 +147,11 @@ class App(ctk.CTk):
header_bar.pack_propagate(False)
# Language selector
self._lang_icon = ctk.CTkLabel(header_bar, text="\U0001f310", font=ctk.CTkFont(size=14), width=20)
_lang_img = ctk_icon("globe", 18)
self._lang_icon = ctk.CTkLabel(
header_bar, text="" if _lang_img else "\U0001f310",
image=_lang_img, font=ctk.CTkFont(size=14), width=20,
)
self._lang_icon.pack(side="right", padx=(5, 0))
lang_values = list(LANGUAGES.values())
current_display = LANGUAGES.get(i18n.get_language(), "English")
@@ -159,16 +163,20 @@ class App(ctk.CTk):
self.lang_menu.pack(side="right", padx=(5, 0))
# Check Updates button
_sync_img = ctk_icon("refresh", 18)
self._update_check_btn = ctk.CTkButton(
header_bar, text="\u21bb", width=30, height=30,
header_bar, text="" if _sync_img else "\u21bb",
image=_sync_img, width=30, height=30,
corner_radius=15, fg_color="#6b7280", hover_color="#4b5563",
command=self._check_updates_manual,
)
self._update_check_btn.pack(side="right", padx=(5, 0))
# About button
_info_img = ctk_icon("info", 18)
self.about_btn = ctk.CTkButton(
header_bar, text="", width=30, height=30,
header_bar, text="" if _info_img else "",
image=_info_img, width=30, height=30,
corner_radius=15, fg_color="#6b7280", hover_color="#4b5563",
command=self._show_about
)

View File

@@ -6,7 +6,7 @@ Form adapts visible fields based on selected server type.
import customtkinter as ctk
from core.server_store import SERVER_TYPES, DEFAULT_PORTS
from core.i18n import t
from core.icons import icon_text, type_display, type_from_display
from core.icons import icon_text, type_display, type_from_display, make_icon_button, reconfigure_icon_button
# Which conditional fields to show for each server type.
@@ -160,7 +160,7 @@ class ServerDialog(ctk.CTkToplevel):
pass_inner.pack(fill="x", padx=20, pady=(2, 5))
self.password_entry = ctk.CTkEntry(pass_inner, show="*", placeholder_text=t("placeholder_password"))
self.password_entry.pack(side="left", fill="x", expand=True, padx=(0, 5))
self.show_pass = ctk.CTkButton(pass_inner, text=icon_text("eye", t("show")), width=70, command=self._toggle_password)
self.show_pass = make_icon_button(pass_inner, "eye", t("show"), width=70, command=self._toggle_password)
self.show_pass.pack(side="right")
self._pass_visible = False
self._field_frames["password"] = f
@@ -286,8 +286,8 @@ class ServerDialog(ctk.CTkToplevel):
# ── Always visible: Buttons ──
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
btn_frame.pack(fill="x", padx=20, pady=(15, 20))
ctk.CTkButton(btn_frame, text=icon_text("delete", t("cancel")), fg_color="#6b7280", command=self.destroy).pack(side="left", expand=True, padx=(0, 5))
ctk.CTkButton(btn_frame, text=icon_text("confirm", t("save")), command=self._save).pack(side="right", expand=True, padx=(5, 0))
make_icon_button(btn_frame, "close", t("cancel"), fg_color="#6b7280", command=self.destroy).pack(side="left", expand=True, padx=(0, 5))
make_icon_button(btn_frame, "confirm", t("save"), command=self._save).pack(side="right", expand=True, padx=(5, 0))
# Fill values if editing
if server:
@@ -360,7 +360,7 @@ class ServerDialog(ctk.CTkToplevel):
def _toggle_password(self):
self._pass_visible = not self._pass_visible
self.password_entry.configure(show="" if self._pass_visible else "*")
self.show_pass.configure(text=icon_text("eye", t("hide") if self._pass_visible else t("show")))
reconfigure_icon_button(self.show_pass, "eye", t("hide") if self._pass_visible else t("show"))
def _save(self):
alias = self.alias_entry.get().strip()

View File

@@ -8,6 +8,7 @@ import customtkinter as ctk
from core.i18n import t
from core.icons import (
icon_text, TYPE_COLORS, TYPE_LABELS, CTX_ICONS, icon,
make_icon_button, reconfigure_icon_button,
)
from gui.widgets.status_badge import StatusBadge
@@ -80,11 +81,11 @@ class Sidebar(ctk.CTkFrame):
# Buttons
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
btn_frame.pack(fill="x", padx=10, pady=10)
self.add_btn = ctk.CTkButton(btn_frame, text=icon_text("add", t("add")), width=70, height=30, command=self._on_add)
self.add_btn = make_icon_button(btn_frame, "add", t("add"), width=70, height=30, command=self._on_add)
self.add_btn.pack(side="left", padx=(0, 3))
self.edit_btn = ctk.CTkButton(btn_frame, text=icon_text("edit", t("edit")), width=70, height=30, fg_color="#6b7280", command=self._on_edit)
self.edit_btn = make_icon_button(btn_frame, "edit", t("edit"), width=70, height=30, fg_color="#6b7280", command=self._on_edit)
self.edit_btn.pack(side="left", padx=3)
self.del_btn = ctk.CTkButton(btn_frame, text=icon_text("delete", t("delete")), width=70, height=30, fg_color="#ef4444", hover_color="#dc2626", command=self._on_delete)
self.del_btn = make_icon_button(btn_frame, "delete", t("delete"), width=70, height=30, fg_color="#ef4444", hover_color="#dc2626", command=self._on_delete)
self.del_btn.pack(side="right", padx=(3, 0))
# Callbacks — set by app.py
@@ -103,9 +104,9 @@ class Sidebar(ctk.CTkFrame):
def update_language(self):
self.title_label.configure(text=t("servers"))
self.search_entry.configure(placeholder_text=t("search"))
self.add_btn.configure(text=icon_text("add", t("add")))
self.edit_btn.configure(text=icon_text("edit", t("edit")))
self.del_btn.configure(text=icon_text("delete", t("delete")))
reconfigure_icon_button(self.add_btn, "add", t("add"))
reconfigure_icon_button(self.edit_btn, "edit", t("edit"))
reconfigure_icon_button(self.del_btn, "delete", t("delete"))
self._update_sessions_label()
# ── Refresh / Render ──────────────────────────────

View File

@@ -13,7 +13,7 @@ from tkinter import messagebox, filedialog
import customtkinter as ctk
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, ctk_icon, make_icon_button
from core.ssh_client import SFTPSession
from gui.widgets.file_list import FileListWidget
@@ -90,28 +90,34 @@ class FilesTab(ctk.CTkFrame):
ctk.CTkLabel(left_header, text=t("local_files"),
font=ctk.CTkFont(size=13, weight="bold")).pack(side="left")
_back_img = ctk_icon("back", 16)
self._local_back_btn = ctk.CTkButton(
left_header, text="\u2190", width=30, height=28,
left_header, text="" if _back_img else "\u2190",
image=_back_img, width=30, height=28,
command=self._local_go_back,
)
self._local_back_btn.pack(side="left", padx=(8, 2))
_up_img = ctk_icon("up", 16)
self._local_up_btn = ctk.CTkButton(
left_header, text="\u2191", width=30, height=28,
left_header, text="" if _up_img else "\u2191",
image=_up_img, width=30, height=28,
command=self._local_go_up,
)
self._local_up_btn.pack(side="left", padx=2)
# Local refresh button
_ref_img = ctk_icon("refresh", 16)
self._local_refresh_btn = ctk.CTkButton(
left_header, text="\u21BB", width=30, height=28,
left_header, text="" if _ref_img else "\u21BB",
image=_ref_img, width=30, height=28,
command=self._refresh_local,
)
self._local_refresh_btn.pack(side="left", padx=2)
# Browse button
self._browse_btn = ctk.CTkButton(
left_header, text=icon_text("folder_open", t("browse")), width=75, height=28,
self._browse_btn = make_icon_button(
left_header, "folder_open", t("browse"), width=75, height=28,
command=self._browse_local,
)
self._browse_btn.pack(side="left", padx=2)
@@ -158,19 +164,22 @@ class FilesTab(ctk.CTkFrame):
font=ctk.CTkFont(size=13, weight="bold")).pack(side="left")
self._remote_back_btn = ctk.CTkButton(
right_header, text="\u2190", width=30, height=28,
right_header, text="" if _back_img else "\u2190",
image=_back_img, width=30, height=28,
command=self._remote_go_back,
)
self._remote_back_btn.pack(side="left", padx=(8, 2))
self._remote_up_btn = ctk.CTkButton(
right_header, text="\u2191", width=30, height=28,
right_header, text="" if _up_img else "\u2191",
image=_up_img, width=30, height=28,
command=self._remote_go_up,
)
self._remote_up_btn.pack(side="left", padx=2)
self._remote_refresh_btn = ctk.CTkButton(
right_header, text="\u21BB", width=30, height=28,
right_header, text="" if _ref_img else "\u21BB",
image=_ref_img, width=30, height=28,
command=self._refresh_remote,
)
self._remote_refresh_btn.pack(side="left", padx=2)
@@ -204,14 +213,14 @@ class FilesTab(ctk.CTkFrame):
toolbar = ctk.CTkFrame(self, fg_color="transparent")
toolbar.pack(fill="x", padx=10, pady=4)
self._upload_btn = ctk.CTkButton(
toolbar, text=icon_text("upload", t("upload")), width=110, height=30,
self._upload_btn = make_icon_button(
toolbar, "upload", t("upload"), width=110, height=30,
command=self._upload_selected,
)
self._upload_btn.pack(side="left", padx=(0, 4))
self._download_btn = ctk.CTkButton(
toolbar, text=icon_text("download", t("download")), width=110, height=30,
self._download_btn = make_icon_button(
toolbar, "download", t("download"), width=110, height=30,
command=self._download_selected,
)
self._download_btn.pack(side="left", padx=4)
@@ -219,21 +228,21 @@ class FilesTab(ctk.CTkFrame):
sep = ctk.CTkFrame(toolbar, width=2, height=24, fg_color="gray40")
sep.pack(side="left", padx=8)
self._mkdir_btn = ctk.CTkButton(
toolbar, text=icon_text("folder", t("new_folder")), width=110, height=30,
self._mkdir_btn = make_icon_button(
toolbar, "folder", t("new_folder"), width=110, height=30,
command=self._mkdir_remote,
)
self._mkdir_btn.pack(side="left", padx=4)
self._delete_btn = ctk.CTkButton(
toolbar, text=icon_text("delete", t("delete_files")), width=90, height=30,
self._delete_btn = make_icon_button(
toolbar, "delete", t("delete_files"), width=90, height=30,
fg_color="#dc2626", hover_color="#b91c1c",
command=self._delete_remote,
)
self._delete_btn.pack(side="left", padx=4)
self._rename_btn = ctk.CTkButton(
toolbar, text=icon_text("edit", t("rename_file")), width=110, height=30,
self._rename_btn = make_icon_button(
toolbar, "edit", t("rename_file"), width=110, height=30,
command=self._rename_remote,
)
self._rename_btn.pack(side="left", padx=4)

View File

@@ -9,7 +9,7 @@ from tkinter import ttk
import customtkinter as ctk
from core.grafana_client import GrafanaClient
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button, reconfigure_icon_button
from gui.tabs.query_tab import apply_dark_scrollbar_style
@@ -33,8 +33,8 @@ class GrafanaTab(ctk.CTkFrame):
font=ctk.CTkFont(size=18, weight="bold"))
title.pack(side="left")
self._refresh_btn = ctk.CTkButton(header_frame, text=icon_text("refresh", t("grafana_refresh")), width=110,
command=self._refresh)
self._refresh_btn = make_icon_button(header_frame, "refresh", t("grafana_refresh"), width=110,
command=self._refresh)
self._refresh_btn.pack(side="right")
# ── Dashboards section ──
@@ -131,8 +131,10 @@ class GrafanaTab(ctk.CTkFrame):
except Exception as e:
self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444"))
finally:
self.after(0, lambda: self._refresh_btn.configure(
state="normal", text=icon_text("refresh", t("grafana_refresh"))))
self.after(0, lambda: (
self._refresh_btn.configure(state="normal"),
reconfigure_icon_button(self._refresh_btn, "refresh", t("grafana_refresh")),
))
threading.Thread(target=_do, daemon=True).start()

View File

@@ -4,7 +4,7 @@ Info tab — display server details, edit button.
import customtkinter as ctk
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button
class InfoTab(ctk.CTkFrame):
@@ -56,7 +56,7 @@ class InfoTab(ctk.CTkFrame):
self._fields[key] = val
# Edit button
self.edit_btn = ctk.CTkButton(self, text=icon_text("edit", t("edit_server_btn")), command=self._on_edit)
self.edit_btn = make_icon_button(self, "edit", t("edit_server_btn"), command=self._on_edit)
self.edit_btn.pack(pady=15)
def set_server(self, alias: str | None):

View File

@@ -7,7 +7,7 @@ import threading
import customtkinter as ctk
from core.ssh_client import SSHClientWrapper
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button
class KeysTab(ctk.CTkFrame):
@@ -30,13 +30,13 @@ class KeysTab(ctk.CTkFrame):
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
btn_frame.pack(fill="x", padx=15, pady=5)
self.gen_btn = ctk.CTkButton(btn_frame, text=icon_text("key", t("generate_key")), command=self._generate)
self.gen_btn = make_icon_button(btn_frame, "key", t("generate_key"), command=self._generate)
self.gen_btn.pack(side="left", padx=(0, 10))
self.install_btn = ctk.CTkButton(btn_frame, text=icon_text("upload", t("install_on_server")), fg_color="#22c55e", hover_color="#16a34a", command=self._install)
self.install_btn = make_icon_button(btn_frame, "upload", t("install_on_server"), fg_color="#22c55e", hover_color="#16a34a", command=self._install)
self.install_btn.pack(side="left")
self.copy_btn = ctk.CTkButton(btn_frame, text=icon_text("copy", t("copy_public_key")), fg_color="#6b7280", command=self._copy_key)
self.copy_btn = make_icon_button(btn_frame, "copy", t("copy_public_key"), fg_color="#6b7280", command=self._copy_key)
self.copy_btn.pack(side="right")
# Status log

View File

@@ -10,7 +10,7 @@ import threading
import customtkinter as ctk
from core.remote_desktop import RemoteDesktopLauncher
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button, reconfigure_icon_button
from core.logger import log
@@ -33,15 +33,15 @@ class LaunchTab(ctk.CTkFrame):
# ── Toolbar (shown when RDP connected) ──
self._toolbar = ctk.CTkFrame(self, height=36, fg_color="transparent")
self._disconnect_btn = ctk.CTkButton(
self._toolbar, text=icon_text("delete", t("rdp_disconnect")),
self._disconnect_btn = make_icon_button(
self._toolbar, "delete", t("rdp_disconnect"),
width=120, height=30, fg_color="#ef4444", hover_color="#dc2626",
command=self._disconnect,
)
self._disconnect_btn.pack(side="left", padx=(8, 4))
self._fullscreen_btn = ctk.CTkButton(
self._toolbar, text=icon_text("launch", t("rdp_fullscreen")),
self._fullscreen_btn = make_icon_button(
self._toolbar, "launch", t("rdp_fullscreen"),
width=130, height=30, fg_color="#6b7280", hover_color="#4b5563",
command=self._toggle_fullscreen,
)
@@ -135,8 +135,9 @@ class LaunchTab(ctk.CTkFrame):
).pack(fill="x", padx=15, pady=(3, 12))
# Connect button
self._connect_btn = ctk.CTkButton(
self._settings_panel, text=icon_text("execute", t("launch_connect")),
self._connect_btn = make_icon_button(
self._settings_panel, "execute", t("launch_connect"),
icon_size=20,
font=ctk.CTkFont(size=18, weight="bold"),
width=220, height=50,
command=self._on_connect,
@@ -374,9 +375,7 @@ class LaunchTab(ctk.CTkFrame):
if self._is_fullscreen:
# Exit fullscreen — reattach
self._is_fullscreen = False
self._fullscreen_btn.configure(
text=icon_text("launch", t("rdp_fullscreen")),
)
reconfigure_icon_button(self._fullscreen_btn, "launch", t("rdp_fullscreen"))
self._rdp_frame.update_idletasks()
parent_hwnd = self._rdp_frame.winfo_id()
w = self._rdp_frame.winfo_width()
@@ -385,9 +384,7 @@ class LaunchTab(ctk.CTkFrame):
else:
# Go fullscreen — detach, then maximize after event loop settles
self._is_fullscreen = True
self._fullscreen_btn.configure(
text=icon_text("back", t("rdp_exit_fullscreen")),
)
reconfigure_icon_button(self._fullscreen_btn, "back", t("rdp_exit_fullscreen"))
self._embedded_rdp.detach()
self.after(300, self._maximize_detached)

View File

@@ -8,7 +8,7 @@ import threading
import customtkinter as ctk
from core.winrm_client import WinRMClient
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button
class PowershellTab(ctk.CTkFrame):
@@ -68,8 +68,8 @@ class PowershellTab(ctk.CTkFrame):
self._entry.bind("<Up>", lambda e: self._history_navigate(-1))
self._entry.bind("<Down>", lambda e: self._history_navigate(1))
self._exec_btn = ctk.CTkButton(
input_row, text=icon_text("execute", t("ps_execute")), width=100,
self._exec_btn = make_icon_button(
input_row, "execute", t("ps_execute"), width=100,
command=self._execute,
)
self._exec_btn.pack(side="right")

View File

@@ -8,7 +8,7 @@ from tkinter import ttk
import customtkinter as ctk
from core.prometheus_client import PrometheusClient
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button, reconfigure_icon_button
from gui.tabs.query_tab import apply_dark_scrollbar_style
@@ -37,8 +37,8 @@ class PrometheusTab(ctk.CTkFrame):
self._query_entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
self._query_entry.bind("<Return>", lambda e: self._execute_query())
self._exec_btn = ctk.CTkButton(query_frame, text=icon_text("execute", t("prom_execute")), width=100,
command=self._execute_query)
self._exec_btn = make_icon_button(query_frame, "execute", t("prom_execute"), width=100,
command=self._execute_query)
self._exec_btn.pack(side="left")
# ── Query results ──
@@ -59,8 +59,8 @@ class PrometheusTab(ctk.CTkFrame):
font=ctk.CTkFont(size=14, weight="bold"), anchor="w")
targets_label.pack(side="left")
self._refresh_btn = ctk.CTkButton(targets_header, text=icon_text("refresh", t("prom_refresh")), width=100,
command=self._refresh_all)
self._refresh_btn = make_icon_button(targets_header, "refresh", t("prom_refresh"), width=100,
command=self._refresh_all)
self._refresh_btn.pack(side="right")
targets_frame = ctk.CTkFrame(self, fg_color="transparent")
@@ -201,8 +201,10 @@ class PrometheusTab(ctk.CTkFrame):
except Exception as e:
self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444"))
finally:
self.after(0, lambda: self._refresh_btn.configure(
state="normal", text=icon_text("refresh", t("prom_refresh"))))
self.after(0, lambda: (
self._refresh_btn.configure(state="normal"),
reconfigure_icon_button(self._refresh_btn, "refresh", t("prom_refresh")),
))
threading.Thread(target=_do, daemon=True).start()

View File

@@ -14,7 +14,7 @@ from tkinter import ttk, filedialog
import customtkinter as ctk
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button
from core.sql_client import SQLClient
_TREE_THEME_APPLIED = False
@@ -227,9 +227,8 @@ class QueryTab(ctk.CTkFrame):
btn_row = ctk.CTkFrame(right_ctk, fg_color="transparent")
btn_row.pack(fill="x", padx=8, pady=4)
self._exec_btn = ctk.CTkButton(
btn_row,
text=icon_text("execute", t("query_execute")),
self._exec_btn = make_icon_button(
btn_row, "execute", t("query_execute"),
command=self._execute_query,
width=130,
fg_color="#2563eb",
@@ -237,9 +236,8 @@ class QueryTab(ctk.CTkFrame):
)
self._exec_btn.pack(side="left", padx=(0, 6))
self._clear_btn = ctk.CTkButton(
btn_row,
text=icon_text("clear", t("query_clear")),
self._clear_btn = make_icon_button(
btn_row, "clear", t("query_clear"),
command=self._clear_all,
width=80,
fg_color="#6b7280",
@@ -247,9 +245,8 @@ class QueryTab(ctk.CTkFrame):
)
self._clear_btn.pack(side="left", padx=(0, 6))
self._export_btn = ctk.CTkButton(
btn_row,
text=icon_text("save", t("query_export_csv")),
self._export_btn = make_icon_button(
btn_row, "save", t("query_export_csv"),
command=self._export_csv,
width=110,
fg_color="#059669",

View File

@@ -6,7 +6,7 @@ import threading
import customtkinter as ctk
from core.redis_client import RedisClient
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button
class RedisTab(ctk.CTkFrame):
@@ -67,28 +67,28 @@ class RedisTab(ctk.CTkFrame):
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
btn_frame.pack(fill="x", padx=15, pady=5)
self._exec_btn = ctk.CTkButton(btn_frame, text=icon_text("execute", t("redis_execute")), width=100,
command=self._execute_command)
self._exec_btn = make_icon_button(btn_frame, "execute", t("redis_execute"), width=100,
command=self._execute_command)
self._exec_btn.pack(side="left", padx=(0, 5))
self._info_btn = ctk.CTkButton(btn_frame, text=icon_text("info", "INFO"), width=80,
fg_color="#6b7280", hover_color="#4b5563",
command=lambda: self._run_quick("INFO"))
self._info_btn = make_icon_button(btn_frame, "info", "INFO", width=80,
fg_color="#6b7280", hover_color="#4b5563",
command=lambda: self._run_quick("INFO"))
self._info_btn.pack(side="left", padx=(0, 5))
self._dbsize_btn = ctk.CTkButton(btn_frame, text=icon_text("hash", "DBSIZE"), width=90,
fg_color="#6b7280", hover_color="#4b5563",
command=lambda: self._run_quick("DBSIZE"))
self._dbsize_btn = make_icon_button(btn_frame, "info", "DBSIZE", width=90,
fg_color="#6b7280", hover_color="#4b5563",
command=lambda: self._run_quick("DBSIZE"))
self._dbsize_btn.pack(side="left", padx=(0, 5))
self._scan_btn = ctk.CTkButton(btn_frame, text=icon_text("search", "SCAN"), width=80,
fg_color="#6b7280", hover_color="#4b5563",
command=lambda: self._run_quick("SCAN 0 COUNT 100"))
self._scan_btn = make_icon_button(btn_frame, "search", "SCAN", width=80,
fg_color="#6b7280", hover_color="#4b5563",
command=lambda: self._run_quick("SCAN 0 COUNT 100"))
self._scan_btn.pack(side="left", padx=(0, 5))
self._clear_btn = ctk.CTkButton(btn_frame, text=icon_text("clear", t("redis_clear")), width=80,
fg_color="#374151", hover_color="#1f2937",
command=self._clear_output)
self._clear_btn = make_icon_button(btn_frame, "clear", t("redis_clear"), width=80,
fg_color="#374151", hover_color="#1f2937",
command=self._clear_output)
self._clear_btn.pack(side="right")
# ── Output console ──

View File

@@ -12,7 +12,7 @@ from tkinter import ttk, filedialog
import customtkinter as ctk
from core.s3_client import S3Client
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button
from gui.tabs.query_tab import apply_dark_scrollbar_style
@@ -103,32 +103,32 @@ class S3Tab(ctk.CTkFrame):
btn_frame = ctk.CTkFrame(header, fg_color="transparent")
btn_frame.pack(side="right")
self._back_btn = ctk.CTkButton(
btn_frame, text=icon_text("back", t("s3_back")), width=80,
self._back_btn = make_icon_button(
btn_frame, "back", t("s3_back"), width=80,
command=self._go_back, state="disabled",
)
self._back_btn.pack(side="left", padx=(0, 5))
self._refresh_btn = ctk.CTkButton(
btn_frame, text=icon_text("refresh", t("s3_refresh")), width=100,
self._refresh_btn = make_icon_button(
btn_frame, "refresh", t("s3_refresh"), width=100,
command=self._refresh,
)
self._refresh_btn.pack(side="left", padx=(0, 5))
self._upload_btn = ctk.CTkButton(
btn_frame, text=icon_text("upload", t("s3_upload")), width=100,
self._upload_btn = make_icon_button(
btn_frame, "upload", t("s3_upload"), width=100,
command=self._upload,
)
self._upload_btn.pack(side="left", padx=(0, 5))
self._download_btn = ctk.CTkButton(
btn_frame, text=icon_text("download", t("s3_download")), width=110,
self._download_btn = make_icon_button(
btn_frame, "download", t("s3_download"), width=110,
command=self._download,
)
self._download_btn.pack(side="left", padx=(0, 5))
self._delete_btn = ctk.CTkButton(
btn_frame, text=icon_text("delete", t("s3_delete")), width=100,
self._delete_btn = make_icon_button(
btn_frame, "delete", t("s3_delete"), width=100,
fg_color="#dc2626", hover_color="#b91c1c",
command=self._delete,
)

View File

@@ -10,7 +10,7 @@ from tkinter import filedialog, messagebox
import customtkinter as ctk
from core.claude_setup import check_status, install_all, install_ssh_script, install_skill, generate_ssh_key
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button
from core.logger import log
@@ -70,8 +70,8 @@ class SetupTab(ctk.CTkFrame):
btn_frame = ctk.CTkFrame(self._scroll, fg_color="transparent")
btn_frame.pack(fill="x", padx=20, pady=15)
self.install_all_btn = ctk.CTkButton(
btn_frame, text=icon_text("confirm", t("install_everything")),
self.install_all_btn = make_icon_button(
btn_frame, "confirm", t("install_everything"),
font=ctk.CTkFont(size=14, weight="bold"),
height=40, fg_color="#22c55e", hover_color="#16a34a",
command=self._install_all
@@ -82,16 +82,16 @@ class SetupTab(ctk.CTkFrame):
ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent")
ind_frame.pack(fill="x")
self.ssh_py_btn = ctk.CTkButton(ind_frame, text=icon_text("confirm", t("install_ssh_py")), width=110, fg_color="#6b7280",
self.ssh_py_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_py"), width=110, fg_color="#6b7280",
command=self._install_script)
self.ssh_py_btn.pack(side="left", padx=(0, 5))
self.skill_btn = ctk.CTkButton(ind_frame, text=icon_text("confirm", t("install_skill")), width=110, fg_color="#6b7280",
self.skill_btn = make_icon_button(ind_frame, "confirm", t("install_skill"), width=110, fg_color="#6b7280",
command=self._install_skill)
self.skill_btn.pack(side="left", padx=5)
self.ssh_key_btn = ctk.CTkButton(ind_frame, text=icon_text("confirm", t("install_ssh_key")), width=110, fg_color="#6b7280",
self.ssh_key_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_key"), width=110, fg_color="#6b7280",
command=self._gen_key)
self.ssh_key_btn.pack(side="left", padx=5)
self.refresh_btn = ctk.CTkButton(ind_frame, text=icon_text("refresh", t("refresh")), width=90, fg_color="#3b82f6",
self.refresh_btn = make_icon_button(ind_frame, "refresh", t("refresh"), width=90, fg_color="#3b82f6",
command=self._refresh_status)
self.refresh_btn.pack(side="right")
@@ -185,8 +185,8 @@ class SetupTab(ctk.CTkFrame):
font=ctk.CTkFont(family="Consolas", size=11)
)
self._path_label.pack(side="left", fill="x", expand=True, padx=(5, 10))
self.change_path_btn = ctk.CTkButton(
path_row, text=icon_text("folder", t("change_path")), width=120, fg_color="#6b7280",
self.change_path_btn = make_icon_button(
path_row, "folder", t("change_path"), width=120, fg_color="#6b7280",
command=self._change_config_path
)
self.change_path_btn.pack(side="right")
@@ -195,8 +195,8 @@ class SetupTab(ctk.CTkFrame):
backup_row = ctk.CTkFrame(config_frame, fg_color="transparent")
backup_row.pack(fill="x", padx=15, pady=(5, 10))
self.backup_btn = ctk.CTkButton(
backup_row, text=icon_text("save", t("backup_now")), width=120, fg_color="#3b82f6",
self.backup_btn = make_icon_button(
backup_row, "save", t("backup_now"), width=120, fg_color="#3b82f6",
command=self._backup_now
)
self.backup_btn.pack(side="left", padx=(0, 10))
@@ -210,8 +210,8 @@ class SetupTab(ctk.CTkFrame):
)
self._backup_menu.pack(side="left", padx=(0, 10))
self.restore_btn = ctk.CTkButton(
backup_row, text=icon_text("refresh", t("restore")), width=100, fg_color="#ef4444", hover_color="#dc2626",
self.restore_btn = make_icon_button(
backup_row, "refresh", t("restore"), width=100, fg_color="#ef4444", hover_color="#dc2626",
command=self._restore_backup
)
self.restore_btn.pack(side="left")
@@ -220,26 +220,26 @@ class SetupTab(ctk.CTkFrame):
ie_row = ctk.CTkFrame(config_frame, fg_color="transparent")
ie_row.pack(fill="x", padx=15, pady=(0, 10))
self.export_config_btn = ctk.CTkButton(
ie_row, text=icon_text("upload", t("export_config")), width=130, fg_color="#6b7280",
self.export_config_btn = make_icon_button(
ie_row, "upload", t("export_config"), width=130, fg_color="#6b7280",
command=self._export_config
)
self.export_config_btn.pack(side="left", padx=(0, 5))
self.import_config_btn = ctk.CTkButton(
ie_row, text=icon_text("download", t("import_config")), width=130, fg_color="#6b7280",
self.import_config_btn = make_icon_button(
ie_row, "download", t("import_config"), width=130, fg_color="#6b7280",
command=self._import_config
)
self.import_config_btn.pack(side="left", padx=5)
self.export_backup_btn = ctk.CTkButton(
ie_row, text=icon_text("upload", t("export_backup")), width=130, fg_color="#6b7280",
self.export_backup_btn = make_icon_button(
ie_row, "upload", t("export_backup"), width=130, fg_color="#6b7280",
command=self._export_backup
)
self.export_backup_btn.pack(side="left", padx=5)
self.import_backup_btn = ctk.CTkButton(
ie_row, text=icon_text("download", t("import_backup")), width=130, fg_color="#6b7280",
self.import_backup_btn = make_icon_button(
ie_row, "download", t("import_backup"), width=130, fg_color="#6b7280",
command=self._import_backup
)
self.import_backup_btn.pack(side="left", padx=5)

View File

@@ -6,7 +6,7 @@ Live countdown, one-click copy, per-server secrets.
import threading
import customtkinter as ctk
from core.i18n import t
from core.icons import icon_text
from core.icons import icon_text, make_icon_button, reconfigure_icon_button
class TOTPTab(ctk.CTkFrame):
@@ -81,8 +81,8 @@ class TOTPTab(ctk.CTkFrame):
widget.bind("<Button-1>", lambda e: self._copy_code())
# Copy button
self.copy_btn = ctk.CTkButton(
self, text=icon_text("copy", t("totp_copy")), width=200, height=40,
self.copy_btn = make_icon_button(
self, "copy", t("totp_copy"), width=200, height=40,
font=ctk.CTkFont(size=14),
fg_color="#22c55e", hover_color="#16a34a",
command=self._copy_code
@@ -108,30 +108,30 @@ class TOTPTab(ctk.CTkFrame):
)
self.secret_entry.pack(side="left", fill="x", expand=True, padx=(0, 5))
self.show_secret_btn = ctk.CTkButton(
entry_row, text=icon_text("eye", t("show")), width=80,
self.show_secret_btn = make_icon_button(
entry_row, "eye", t("show"), width=80,
fg_color="#6b7280", hover_color="#4b5563",
command=self._toggle_secret
)
self.show_secret_btn.pack(side="left", padx=(0, 5))
self._secret_visible = False
self.save_secret_btn = ctk.CTkButton(
entry_row, text=icon_text("confirm", t("totp_save_secret")), width=110,
self.save_secret_btn = make_icon_button(
entry_row, "confirm", t("totp_save_secret"), width=110,
command=self._save_secret
)
self.save_secret_btn.pack(side="left", padx=(0, 5))
self.remove_secret_btn = ctk.CTkButton(
entry_row, text=icon_text("delete", t("totp_remove_secret")), width=110,
self.remove_secret_btn = make_icon_button(
entry_row, "delete", t("totp_remove_secret"), width=110,
fg_color="#ef4444", hover_color="#dc2626",
command=self._remove_secret
)
self.remove_secret_btn.pack(side="left")
# Generate random secret button
self.gen_secret_btn = ctk.CTkButton(
secret_frame, text=icon_text("key", t("totp_generate_secret")), width=200,
self.gen_secret_btn = make_icon_button(
secret_frame, "key", t("totp_generate_secret"), width=200,
fg_color="#6b7280", hover_color="#4b5563",
command=self._generate_secret
)
@@ -267,8 +267,8 @@ class TOTPTab(ctk.CTkFrame):
def _toggle_secret(self):
self._secret_visible = not self._secret_visible
self.secret_entry.configure(show="" if self._secret_visible else "*")
self.show_secret_btn.configure(
text=icon_text("eye", t("hide") if self._secret_visible else t("show"))
reconfigure_icon_button(
self.show_secret_btn, "eye", t("hide") if self._secret_visible else t("show")
)
def _save_secret(self):
@@ -331,12 +331,12 @@ class TOTPTab(ctk.CTkFrame):
def update_language(self):
self.title_label.configure(text=t("totp_title"))
self.desc_label.configure(text=t("totp_desc"))
self.copy_btn.configure(text=icon_text("copy", t("totp_copy")))
self.save_secret_btn.configure(text=icon_text("confirm", t("totp_save_secret")))
self.remove_secret_btn.configure(text=icon_text("delete", t("totp_remove_secret")))
self.gen_secret_btn.configure(text=icon_text("key", t("totp_generate_secret")))
self.show_secret_btn.configure(
text=icon_text("eye", t("hide") if self._secret_visible else t("show"))
reconfigure_icon_button(self.copy_btn, "copy", t("totp_copy"))
reconfigure_icon_button(self.save_secret_btn, "confirm", t("totp_save_secret"))
reconfigure_icon_button(self.remove_secret_btn, "delete", t("totp_remove_secret"))
reconfigure_icon_button(self.gen_secret_btn, "key", t("totp_generate_secret"))
reconfigure_icon_button(
self.show_secret_btn, "eye", t("hide") if self._secret_visible else t("show")
)
if not self._current_alias:
self.server_label.configure(text=t("no_server_selected"))