From 641f5a41d003b9fbdbbb258b3e2278a3ef104525 Mon Sep 17 00:00:00 2001 From: chrome-storm-c442 Date: Mon, 23 Feb 2026 14:32:19 -0500 Subject: [PATCH] Fix TUI apps (mc, htop, vim) freezing in SSH terminal - Remove LNM mode that corrupted cursor positioning for TUI programs - Add render debouncing (~60fps) to prevent UI thread blocking - Add data batching in terminal tab to reduce render calls - Increase SSH recv buffer from 4KB to 64KB Co-Authored-By: Claude Opus 4.6 --- core/ssh_client.py | 2 +- gui/tabs/terminal_tab.py | 15 ++++++++++++++- gui/widgets/terminal_widget.py | 15 +++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/ssh_client.py b/core/ssh_client.py index 73d3cc7..780e11f 100644 --- a/core/ssh_client.py +++ b/core/ssh_client.py @@ -114,7 +114,7 @@ class ShellSession: try: while self._running: try: - data = self._channel.recv(4096) + data = self._channel.recv(65536) if not data: break if self.on_data: diff --git a/gui/tabs/terminal_tab.py b/gui/tabs/terminal_tab.py index 9bbfecc..19ce2da 100644 --- a/gui/tabs/terminal_tab.py +++ b/gui/tabs/terminal_tab.py @@ -30,6 +30,10 @@ class TerminalTab(ctk.CTkFrame): self._terminal.pack(fill="both", expand=True, padx=5, pady=5) self._terminal.set_status(t("term_disconnected"), "#888888") + # Data batching buffer + self._data_buffer = bytearray() + self._flush_after_id = None + def set_server(self, alias: str | None): if alias == self._current_alias: return @@ -84,7 +88,16 @@ class TerminalTab(ctk.CTkFrame): self._session = None def _on_data_received(self, data: bytes): - self.after(0, lambda d=data: self._terminal.feed(d)) + self._data_buffer.extend(data) + if self._flush_after_id is None: + self._flush_after_id = self.after(8, self._flush_data_buffer) + + def _flush_data_buffer(self): + self._flush_after_id = None + if self._data_buffer: + chunk = bytes(self._data_buffer) + self._data_buffer.clear() + self._terminal.feed(chunk) def _on_disconnected(self): if self._intentional_disconnect: diff --git a/gui/widgets/terminal_widget.py b/gui/widgets/terminal_widget.py index 6524e74..5192bbb 100644 --- a/gui/widgets/terminal_widget.py +++ b/gui/widgets/terminal_widget.py @@ -73,12 +73,15 @@ class TerminalWidget(tk.Frame): # pyte screen + stream self._screen = pyte.Screen(cols, rows) - self._screen.set_mode(pyte.modes.LNM) self._stream = pyte.Stream(self._screen) # Previous buffer for diff rendering self._prev_buffer: dict[int, list] = {} + # Render debouncing (~60fps) + self._render_pending = False + self._render_after_id = None + # Font self._font = tkfont.Font(family="Consolas", size=11) self._bold_font = tkfont.Font(family="Consolas", size=11, weight="bold") @@ -147,12 +150,20 @@ class TerminalWidget(tk.Frame): self._status_label.configure(text=text, fg=color) def feed(self, data: bytes): - """Feed raw bytes from SSH into pyte and re-render.""" + """Feed raw bytes from SSH into pyte, schedule debounced render.""" try: text = data.decode("utf-8", errors="replace") except Exception: text = data.decode("latin-1", errors="replace") self._stream.feed(text) + if not self._render_pending: + self._render_pending = True + self._render_after_id = self.after(16, self._debounced_render) + + def _debounced_render(self): + """Execute pending render.""" + self._render_pending = False + self._render_after_id = None self._render() def reset(self):