v1.9.27: add disconnect button in terminal + disconnect in context menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-03-06 05:27:03 -05:00
parent 7522908404
commit 064de8df8d
10 changed files with 78 additions and 2 deletions

View File

@@ -111,6 +111,7 @@ _EN = {
"term_connecting": "Connecting to {alias}...", "term_connecting": "Connecting to {alias}...",
"term_connected": "Connected to {alias}", "term_connected": "Connected to {alias}",
"term_disconnected": "Disconnected", "term_disconnected": "Disconnected",
"ctx_disconnect": "Disconnect",
"term_click_to_connect": "Double-click to connect to {alias}", "term_click_to_connect": "Double-click to connect to {alias}",
"sftp_click_to_connect": "Double-click server to browse files", "sftp_click_to_connect": "Double-click server to browse files",
"term_reconnecting": "Reconnecting ({n}/{max})...", "term_reconnecting": "Reconnecting ({n}/{max})...",
@@ -636,6 +637,7 @@ _RU = {
"term_connecting": "Подключение к {alias}...", "term_connecting": "Подключение к {alias}...",
"term_connected": "Подключено к {alias}", "term_connected": "Подключено к {alias}",
"term_disconnected": "Отключено", "term_disconnected": "Отключено",
"ctx_disconnect": "Отключиться",
"term_click_to_connect": "Двойной клик для подключения к {alias}", "term_click_to_connect": "Двойной клик для подключения к {alias}",
"sftp_click_to_connect": "Двойной клик для просмотра файлов", "sftp_click_to_connect": "Двойной клик для просмотра файлов",
"term_reconnecting": "Переподключение ({n}/{max})...", "term_reconnecting": "Переподключение ({n}/{max})...",
@@ -1161,6 +1163,7 @@ _ZH = {
"term_connecting": "正在连接 {alias}...", "term_connecting": "正在连接 {alias}...",
"term_connected": "已连接到 {alias}", "term_connected": "已连接到 {alias}",
"term_disconnected": "已断开", "term_disconnected": "已断开",
"ctx_disconnect": "断开连接",
"term_click_to_connect": "双击连接 {alias}", "term_click_to_connect": "双击连接 {alias}",
"sftp_click_to_connect": "双击服务器浏览文件", "sftp_click_to_connect": "双击服务器浏览文件",
"term_reconnecting": "重新连接中 ({n}/{max})...", "term_reconnecting": "重新连接中 ({n}/{max})...",

View File

@@ -210,6 +210,7 @@ CTX_ICONS = {
"ctx_open_browser": "browser", "ctx_open_browser": "browser",
"ctx_check_status": "status_check", "ctx_check_status": "status_check",
"ctx_copy_alias": "copy", "ctx_copy_alias": "copy",
"ctx_disconnect": "close",
"edit": "edit", "edit": "edit",
"delete": "delete", "delete": "delete",
} }

View File

@@ -255,4 +255,14 @@ class SessionPool:
) )
if has_active: if has_active:
active.append(alias) active.append(alias)
return active 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)
)

View File

@@ -163,6 +163,7 @@ class App(ctk.CTk):
self.sidebar.open_tab_callback = self._context_open_tab self.sidebar.open_tab_callback = self._context_open_tab
self.sidebar.check_status_callback = self._context_check_status self.sidebar.check_status_callback = self._context_check_status
self.sidebar.open_browser_callback = self._context_open_browser self.sidebar.open_browser_callback = self._context_open_browser
self.sidebar.disconnect_callback = self._on_server_disconnect
# Main area # Main area
self._main_frame = ctk.CTkFrame(self._paned, fg_color="transparent") self._main_frame = ctk.CTkFrame(self._paned, fg_color="transparent")
@@ -264,6 +265,11 @@ class App(ctk.CTk):
widget.pack(fill="both", expand=True) widget.pack(fill="both", expand=True)
self._tab_instances[key] = widget 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 # Restore previously active tab if still available
if restore_tab_key and restore_tab_key in self._tab_keys: if restore_tab_key and restore_tab_key in self._tab_keys:
try: try:
@@ -313,6 +319,14 @@ class App(ctk.CTk):
if hasattr(widget, "connect"): if hasattr(widget, "connect"):
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): def _add_server(self):
dialog = ServerDialog(self, self.store) dialog = ServerDialog(self, self.store)
self.wait_window(dialog) self.wait_window(dialog)

View File

@@ -97,6 +97,7 @@ class Sidebar(ctk.CTkFrame):
self.open_tab_callback = None # (alias, tab_key) → select server + switch tab self.open_tab_callback = None # (alias, tab_key) → select server + switch tab
self.check_status_callback = None # (alias) → check single server self.check_status_callback = None # (alias) → check single server
self.open_browser_callback = None # (alias) → open server URL in browser self.open_browser_callback = None # (alias) → open server URL in browser
self.disconnect_callback = None # (alias) → disconnect all sessions
# Subscribe to store changes # Subscribe to store changes
self.store.subscribe(self._refresh_list) self.store.subscribe(self._refresh_list)
@@ -467,6 +468,18 @@ class Sidebar(ctk.CTkFrame):
if actions: if actions:
menu.add_separator() 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 # "Move to Group" submenu
groups = self.store.get_groups() groups = self.store.get_groups()
if groups: if groups:

View File

@@ -318,6 +318,10 @@ class FilesTab(ctk.CTkFrame):
if self._current_alias and not self._sftp: if self._current_alias and not self._sftp:
self._connect_sftp() self._connect_sftp()
def disconnect(self):
"""Disconnect SFTP and update UI (called by app)."""
self._disconnect_sftp()
# ── SFTP connection ── # ── SFTP connection ──
def _connect_sftp(self): def _connect_sftp(self):

View File

@@ -104,6 +104,12 @@ class PowershellTab(ctk.CTkFrame):
if self._current_alias and not self._client: if self._current_alias and not self._client:
self._connect(self._current_alias) 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 ─────────────────────────────────────────────────── # ── Connection ───────────────────────────────────────────────────
def _connect(self, alias: str): def _connect(self, alias: str):

View File

@@ -29,6 +29,17 @@ class TerminalTab(ctk.CTkFrame):
# Import here to avoid circular issues # Import here to avoid circular issues
from gui.widgets.terminal_widget import TerminalWidget 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._terminal = TerminalWidget(
self, self,
send_callback=self._send_to_shell, send_callback=self._send_to_shell,
@@ -45,6 +56,7 @@ class TerminalTab(ctk.CTkFrame):
# Sudo auto-password detection # Sudo auto-password detection
self._sudo_buffer = b"" # Buffer for detecting sudo prompts self._sudo_buffer = b"" # Buffer for detecting sudo prompts
self._sudo_sent = False # Prevent sending password twice for same prompt self._sudo_sent = False # Prevent sending password twice for same prompt
self._on_disconnect_callback = None
def set_server(self, alias: str | None): def set_server(self, alias: str | None):
if alias == self._current_alias: if alias == self._current_alias:
@@ -61,6 +73,7 @@ class TerminalTab(ctk.CTkFrame):
self._current_alias = alias self._current_alias = alias
if alias: if alias:
self._disconnect_btn.configure(state="disabled")
self._terminal.set_status(t("term_click_to_connect").format(alias=alias), "#f59e0b") self._terminal.set_status(t("term_click_to_connect").format(alias=alias), "#f59e0b")
else: else:
self._terminal.reset() self._terminal.reset()
@@ -71,6 +84,17 @@ class TerminalTab(ctk.CTkFrame):
if self._current_alias and not self._session: if self._current_alias and not self._session:
self._connect() 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): def _connect(self):
if not self._current_alias: if not self._current_alias:
return return
@@ -140,6 +164,7 @@ class TerminalTab(ctk.CTkFrame):
# Only grab focus if terminal tab is currently visible # Only grab focus if terminal tab is currently visible
if self._terminal.winfo_ismapped(): if self._terminal.winfo_ismapped():
self._terminal.focus_terminal() self._terminal.focus_terminal()
self._disconnect_btn.configure(state="normal")
self.after(0, _set_session) self.after(0, _set_session)
except Exception as e: except Exception as e:
self.after(0, lambda: self._terminal.set_status( self.after(0, lambda: self._terminal.set_status(

View File

@@ -1,6 +1,6 @@
"""Version info for ServerManager.""" """Version info for ServerManager."""
__version__ = "1.9.26" __version__ = "1.9.27"
__app_name__ = "ServerManager" __app_name__ = "ServerManager"
__author__ = "aibot777" __author__ = "aibot777"
__description__ = "Desktop GUI for managing remote servers" __description__ = "Desktop GUI for managing remote servers"