diff --git a/core/i18n.py b/core/i18n.py index d03fc93..02f5b10 100644 --- a/core/i18n.py +++ b/core/i18n.py @@ -111,6 +111,7 @@ _EN = { "term_connecting": "Connecting to {alias}...", "term_connected": "Connected to {alias}", "term_disconnected": "Disconnected", + "ctx_disconnect": "Disconnect", "term_click_to_connect": "Double-click to connect to {alias}", "sftp_click_to_connect": "Double-click server to browse files", "term_reconnecting": "Reconnecting ({n}/{max})...", @@ -636,6 +637,7 @@ _RU = { "term_connecting": "Подключение к {alias}...", "term_connected": "Подключено к {alias}", "term_disconnected": "Отключено", + "ctx_disconnect": "Отключиться", "term_click_to_connect": "Двойной клик для подключения к {alias}", "sftp_click_to_connect": "Двойной клик для просмотра файлов", "term_reconnecting": "Переподключение ({n}/{max})...", @@ -1161,6 +1163,7 @@ _ZH = { "term_connecting": "正在连接 {alias}...", "term_connected": "已连接到 {alias}", "term_disconnected": "已断开", + "ctx_disconnect": "断开连接", "term_click_to_connect": "双击连接 {alias}", "sftp_click_to_connect": "双击服务器浏览文件", "term_reconnecting": "重新连接中 ({n}/{max})...", diff --git a/core/icons.py b/core/icons.py index 7183892..1a73aca 100644 --- a/core/icons.py +++ b/core/icons.py @@ -210,6 +210,7 @@ CTX_ICONS = { "ctx_open_browser": "browser", "ctx_check_status": "status_check", "ctx_copy_alias": "copy", + "ctx_disconnect": "close", "edit": "edit", "delete": "delete", } diff --git a/core/session_pool.py b/core/session_pool.py index 14c1228..262ee31 100644 --- a/core/session_pool.py +++ b/core/session_pool.py @@ -255,4 +255,14 @@ class SessionPool: ) if has_active: active.append(alias) - return active \ No newline at end of file + return active + + def has_active_session(self, alias: str) -> bool: + with self._lock: + sd = self._sessions.get(alias) + if not sd: + return False + return bool( + (sd.shell_session and sd.shell_session.connected) or + (sd.sftp_session and sd.sftp_session.connected) + ) \ No newline at end of file diff --git a/gui/app.py b/gui/app.py index 550c89d..46031b1 100644 --- a/gui/app.py +++ b/gui/app.py @@ -163,6 +163,7 @@ class App(ctk.CTk): self.sidebar.open_tab_callback = self._context_open_tab self.sidebar.check_status_callback = self._context_check_status self.sidebar.open_browser_callback = self._context_open_browser + self.sidebar.disconnect_callback = self._on_server_disconnect # Main area self._main_frame = ctk.CTkFrame(self._paned, fg_color="transparent") @@ -264,6 +265,11 @@ class App(ctk.CTk): widget.pack(fill="both", expand=True) self._tab_instances[key] = widget + # Wire disconnect callback for terminal toolbar button + terminal = self._tab_instances.get("terminal") + if terminal and hasattr(terminal, "_on_disconnect_callback"): + terminal._on_disconnect_callback = self._on_server_disconnect + # Restore previously active tab if still available if restore_tab_key and restore_tab_key in self._tab_keys: try: @@ -313,6 +319,14 @@ class App(ctk.CTk): if hasattr(widget, "connect"): widget.connect() + def _on_server_disconnect(self, alias: str): + """Disconnect all sessions for a server.""" + for key, widget in self._tab_instances.items(): + if hasattr(widget, "disconnect"): + widget.disconnect() + self.session_pool.disconnect_session(alias) + self.after(500, self.sidebar.update_session_indicators) + def _add_server(self): dialog = ServerDialog(self, self.store) self.wait_window(dialog) diff --git a/gui/sidebar.py b/gui/sidebar.py index 3bbbe36..445c0dc 100644 --- a/gui/sidebar.py +++ b/gui/sidebar.py @@ -97,6 +97,7 @@ class Sidebar(ctk.CTkFrame): self.open_tab_callback = None # (alias, tab_key) → select server + switch tab self.check_status_callback = None # (alias) → check single server self.open_browser_callback = None # (alias) → open server URL in browser + self.disconnect_callback = None # (alias) → disconnect all sessions # Subscribe to store changes self.store.subscribe(self._refresh_list) @@ -467,6 +468,18 @@ class Sidebar(ctk.CTkFrame): if actions: menu.add_separator() + # Dynamic disconnect if session is active + if self.session_pool and self.session_pool.has_active_session(alias): + dc_icon = icon(CTX_ICONS.get("ctx_disconnect", "")) + dc_label = f"{dc_icon} {t('ctx_disconnect')}" if dc_icon else t("ctx_disconnect") + menu.add_command( + label=dc_label, + command=lambda a=alias: ( + self.disconnect_callback(a) if self.disconnect_callback else None + ), + ) + menu.add_separator() + # "Move to Group" submenu groups = self.store.get_groups() if groups: diff --git a/gui/tabs/files_tab.py b/gui/tabs/files_tab.py index 47e2e5a..678f556 100644 --- a/gui/tabs/files_tab.py +++ b/gui/tabs/files_tab.py @@ -318,6 +318,10 @@ class FilesTab(ctk.CTkFrame): if self._current_alias and not self._sftp: self._connect_sftp() + def disconnect(self): + """Disconnect SFTP and update UI (called by app).""" + self._disconnect_sftp() + # ── SFTP connection ── def _connect_sftp(self): diff --git a/gui/tabs/powershell_tab.py b/gui/tabs/powershell_tab.py index d990774..1b53355 100644 --- a/gui/tabs/powershell_tab.py +++ b/gui/tabs/powershell_tab.py @@ -104,6 +104,12 @@ class PowershellTab(ctk.CTkFrame): if self._current_alias and not self._client: self._connect(self._current_alias) + def disconnect(self): + """Disconnect WinRM and update UI (called by app).""" + self._disconnect() + if self._current_alias: + self._set_status(t("term_click_to_connect").format(alias=self._current_alias), "#f59e0b") + # ── Connection ─────────────────────────────────────────────────── def _connect(self, alias: str): diff --git a/gui/tabs/terminal_tab.py b/gui/tabs/terminal_tab.py index 2877d1b..b809d7c 100644 --- a/gui/tabs/terminal_tab.py +++ b/gui/tabs/terminal_tab.py @@ -29,6 +29,17 @@ class TerminalTab(ctk.CTkFrame): # Import here to avoid circular issues from gui.widgets.terminal_widget import TerminalWidget + self._toolbar = ctk.CTkFrame(self, fg_color="transparent", height=32) + self._toolbar.pack(fill="x", padx=5, pady=(5, 0)) + self._toolbar.pack_propagate(False) + self._disconnect_btn = ctk.CTkButton( + self._toolbar, text=t("ctx_disconnect"), width=110, height=28, + fg_color="#dc2626", hover_color="#b91c1c", + font=ctk.CTkFont(size=12), state="disabled", + command=self._on_disconnect_click, + ) + self._disconnect_btn.pack(side="right", padx=2) + self._terminal = TerminalWidget( self, send_callback=self._send_to_shell, @@ -45,6 +56,7 @@ class TerminalTab(ctk.CTkFrame): # Sudo auto-password detection self._sudo_buffer = b"" # Buffer for detecting sudo prompts self._sudo_sent = False # Prevent sending password twice for same prompt + self._on_disconnect_callback = None def set_server(self, alias: str | None): if alias == self._current_alias: @@ -61,6 +73,7 @@ class TerminalTab(ctk.CTkFrame): self._current_alias = alias if alias: + self._disconnect_btn.configure(state="disabled") self._terminal.set_status(t("term_click_to_connect").format(alias=alias), "#f59e0b") else: self._terminal.reset() @@ -71,6 +84,17 @@ class TerminalTab(ctk.CTkFrame): if self._current_alias and not self._session: self._connect() + def _on_disconnect_click(self): + if self._on_disconnect_callback and self._current_alias: + self._on_disconnect_callback(self._current_alias) + + def disconnect(self): + """Disconnect and update UI (called by app).""" + self._disconnect() + self._disconnect_btn.configure(state="disabled") + if self._current_alias: + self._terminal.set_status(t("term_click_to_connect").format(alias=self._current_alias), "#f59e0b") + def _connect(self): if not self._current_alias: return @@ -140,6 +164,7 @@ class TerminalTab(ctk.CTkFrame): # Only grab focus if terminal tab is currently visible if self._terminal.winfo_ismapped(): self._terminal.focus_terminal() + self._disconnect_btn.configure(state="normal") self.after(0, _set_session) except Exception as e: self.after(0, lambda: self._terminal.set_status( diff --git a/releases/ServerManager-v1.9.22-win-x64.exe b/releases/ServerManager-v1.9.27-win-x64.exe similarity index 98% rename from releases/ServerManager-v1.9.22-win-x64.exe rename to releases/ServerManager-v1.9.27-win-x64.exe index e9f481f..42fc747 100644 Binary files a/releases/ServerManager-v1.9.22-win-x64.exe and b/releases/ServerManager-v1.9.27-win-x64.exe differ diff --git a/version.py b/version.py index a118b47..6dbd9ba 100755 --- a/version.py +++ b/version.py @@ -1,6 +1,6 @@ """Version info for ServerManager.""" -__version__ = "1.9.26" +__version__ = "1.9.27" __app_name__ = "ServerManager" __author__ = "aibot777" __description__ = "Desktop GUI for managing remote servers"