- SessionPool: LRU cache for SSH/SFTP sessions across server switches - Sidebar: green dot indicators for servers with active sessions - Sidebar: active sessions count label - Terminal: buffer preservation on server switch via get_current_buffer() - FilesTab/TerminalTab: pool integration for session reuse Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
9.2 KiB
Python
258 lines
9.2 KiB
Python
"""
|
|
Main application window — sidebar + tabview layout.
|
|
"""
|
|
|
|
import customtkinter as ctk
|
|
from tkinter import messagebox
|
|
|
|
from core.server_store import ServerStore
|
|
from core.status_checker import StatusChecker
|
|
from core import i18n
|
|
from core.i18n import t, LANGUAGES
|
|
from core.session_pool import SessionPool
|
|
from gui.sidebar import Sidebar
|
|
from gui.server_dialog import ServerDialog
|
|
from gui.about_dialog import AboutDialog
|
|
from gui.tabs.terminal_tab import TerminalTab
|
|
from gui.tabs.files_tab import FilesTab
|
|
from gui.tabs.info_tab import InfoTab
|
|
from gui.tabs.keys_tab import KeysTab
|
|
from gui.tabs.setup_tab import SetupTab
|
|
from gui.tabs.totp_tab import TOTPTab
|
|
|
|
|
|
class App(ctk.CTk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Window config
|
|
self.title("ServerManager")
|
|
self.geometry("1100x700")
|
|
self.minsize(900, 500)
|
|
|
|
ctk.set_appearance_mode("dark")
|
|
ctk.set_default_color_theme("blue")
|
|
|
|
# Core
|
|
self.store = ServerStore()
|
|
self.checker = StatusChecker(self.store)
|
|
self.session_pool = SessionPool(max_sessions=5) # Create session pool
|
|
|
|
# Layout
|
|
self._build_layout()
|
|
|
|
# Status checker
|
|
self.checker.set_gui_callback(lambda: self.after(0, self._on_status_update))
|
|
self.checker.start()
|
|
self.checker.check_all_now()
|
|
|
|
# Cleanup on close
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
def _build_layout(self):
|
|
# Sidebar
|
|
self.sidebar = Sidebar(self, self.store, on_select=self._on_server_select, session_pool=self.session_pool)
|
|
self.sidebar.pack(side="left", fill="y")
|
|
self.sidebar.add_callback = self._add_server
|
|
self.sidebar.edit_callback = self._edit_server
|
|
self.sidebar.delete_callback = self._delete_server
|
|
|
|
# Main area
|
|
main = ctk.CTkFrame(self, fg_color="transparent")
|
|
main.pack(side="right", fill="both", expand=True)
|
|
|
|
# Header bar (language + about)
|
|
header_bar = ctk.CTkFrame(main, fg_color="transparent", height=40)
|
|
header_bar.pack(fill="x", padx=10, pady=(8, 0))
|
|
header_bar.pack_propagate(False)
|
|
|
|
# Language selector
|
|
lang_values = list(LANGUAGES.values())
|
|
current_display = LANGUAGES.get(i18n.get_language(), "English")
|
|
self._lang_var = ctk.StringVar(value=current_display)
|
|
self.lang_menu = ctk.CTkOptionMenu(
|
|
header_bar, values=lang_values, variable=self._lang_var,
|
|
width=110, height=30, command=self._change_language
|
|
)
|
|
self.lang_menu.pack(side="right", padx=(5, 0))
|
|
|
|
# About button
|
|
self.about_btn = ctk.CTkButton(
|
|
header_bar, text="ⓘ", width=30, height=30,
|
|
corner_radius=15, fg_color="#6b7280", hover_color="#4b5563",
|
|
command=self._show_about
|
|
)
|
|
self.about_btn.pack(side="right", padx=(5, 5))
|
|
|
|
# Tabview
|
|
self.tabview = ctk.CTkTabview(main)
|
|
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
|
|
|
# Tab names stored for language updates
|
|
self._tab_keys = ["terminal", "files", "info", "keys", "totp", "setup"]
|
|
for key in self._tab_keys:
|
|
self.tabview.add(t(key))
|
|
|
|
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store, self.session_pool)
|
|
self.terminal_tab.pack(fill="both", expand=True)
|
|
|
|
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store, self.session_pool)
|
|
self.files_tab.pack(fill="both", expand=True)
|
|
|
|
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
|
|
self.info_tab.pack(fill="both", expand=True)
|
|
|
|
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
|
|
self.keys_tab.pack(fill="both", expand=True)
|
|
|
|
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
|
|
self.totp_tab.pack(fill="both", expand=True)
|
|
|
|
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
|
|
self.setup_tab.pack(fill="both", expand=True)
|
|
|
|
def _on_server_select(self, alias: str):
|
|
self.terminal_tab.set_server(alias)
|
|
self.files_tab.set_server(alias)
|
|
self.info_tab.set_server(alias)
|
|
self.keys_tab.set_server(alias)
|
|
self.totp_tab.set_server(alias)
|
|
# Update session indicators after a short delay (connection is async)
|
|
self.after(1500, self.sidebar.update_session_indicators)
|
|
|
|
def _add_server(self):
|
|
dialog = ServerDialog(self, self.store)
|
|
self.wait_window(dialog)
|
|
|
|
def _edit_server(self, alias: str):
|
|
server = self.store.get_server(alias)
|
|
if server:
|
|
dialog = ServerDialog(self, self.store, server=server)
|
|
self.wait_window(dialog)
|
|
self.info_tab.refresh()
|
|
|
|
def _delete_server(self, alias: str):
|
|
if messagebox.askyesno(t("delete_server"), t("delete_confirm").format(alias=alias)):
|
|
# Clean up sessions when deleting server
|
|
self.session_pool.cleanup_deleted_server(alias)
|
|
self.store.remove_server(alias)
|
|
self._on_server_select(None)
|
|
|
|
def _on_status_update(self):
|
|
self.sidebar.update_statuses()
|
|
self.sidebar.update_session_indicators()
|
|
self.info_tab.refresh()
|
|
|
|
def _show_about(self):
|
|
AboutDialog(self)
|
|
|
|
def _get_current_tab_key(self) -> str:
|
|
"""Get the i18n key of the currently active tab."""
|
|
try:
|
|
current_name = self.tabview.get()
|
|
# Match against current language translations
|
|
for key in self._tab_keys:
|
|
if t(key) == current_name:
|
|
return key
|
|
except Exception:
|
|
pass
|
|
return self._tab_keys[0]
|
|
|
|
def _change_language(self, display_name: str):
|
|
# Remember current tab KEY before language switch
|
|
active_tab_key = self._get_current_tab_key()
|
|
|
|
# Find lang code from display name
|
|
lang_code = "en"
|
|
for code, name in LANGUAGES.items():
|
|
if name == display_name:
|
|
lang_code = code
|
|
break
|
|
i18n.set_language(lang_code)
|
|
self.store._save_settings()
|
|
self._apply_language(active_tab_key)
|
|
|
|
def _apply_language(self, restore_tab_key: str | None = None):
|
|
# Remember selected server
|
|
alias = self.sidebar.get_selected()
|
|
# Use provided key or default to first tab
|
|
current_key = restore_tab_key or self._tab_keys[0]
|
|
|
|
# Save state before destroying tabs
|
|
saved_remote_path = self.files_tab._remote_path
|
|
saved_local_path = self.files_tab._local_path
|
|
had_sftp = self.files_tab._sftp is not None and self.files_tab._sftp.connected
|
|
|
|
# Disconnect all sessions in the pool
|
|
self.session_pool.disconnect_all()
|
|
|
|
# Detach tab contents
|
|
self.terminal_tab.pack_forget()
|
|
self.files_tab.pack_forget()
|
|
self.info_tab.pack_forget()
|
|
self.keys_tab.pack_forget()
|
|
self.totp_tab.pack_forget()
|
|
self.setup_tab.pack_forget()
|
|
|
|
# Get the main frame and destroy old tabview
|
|
main = self.tabview.master
|
|
self.tabview.destroy()
|
|
|
|
# Create new tabview with translated names
|
|
self.tabview = ctk.CTkTabview(main)
|
|
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
|
|
|
for key in self._tab_keys:
|
|
self.tabview.add(t(key))
|
|
|
|
# Re-parent tab contents
|
|
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store, self.session_pool)
|
|
self.terminal_tab.pack(fill="both", expand=True)
|
|
|
|
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store, self.session_pool)
|
|
self.files_tab.pack(fill="both", expand=True)
|
|
|
|
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
|
|
self.info_tab.pack(fill="both", expand=True)
|
|
|
|
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
|
|
self.keys_tab.pack(fill="both", expand=True)
|
|
|
|
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
|
|
self.totp_tab.pack(fill="both", expand=True)
|
|
|
|
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
|
|
self.setup_tab.pack(fill="both", expand=True)
|
|
|
|
# Restore active tab by key
|
|
try:
|
|
self.tabview.set(t(current_key))
|
|
except Exception:
|
|
pass
|
|
|
|
# Restore file paths and reconnect properly
|
|
self.files_tab._local_path = saved_local_path
|
|
self.files_tab._refresh_local()
|
|
if alias and had_sftp:
|
|
# Had active SFTP — reconnect and restore remote path
|
|
self.files_tab._remote_path = saved_remote_path
|
|
self.files_tab.set_server(alias)
|
|
elif alias:
|
|
self.files_tab.set_server(alias)
|
|
|
|
# Restore server selection for other tabs (terminal auto-reconnects)
|
|
if alias:
|
|
self.terminal_tab.set_server(alias)
|
|
self.info_tab.set_server(alias)
|
|
self.keys_tab.set_server(alias)
|
|
self.totp_tab.set_server(alias)
|
|
|
|
# Update sidebar
|
|
self.sidebar.update_language()
|
|
|
|
def _on_close(self):
|
|
# Disconnect all sessions before closing
|
|
self.session_pool.disconnect_all()
|
|
self.checker.stop()
|
|
self.destroy()
|