diff --git a/gui/tabs/terminal_tab.py b/gui/tabs/terminal_tab.py index 2e1d505..7f7d704 100644 --- a/gui/tabs/terminal_tab.py +++ b/gui/tabs/terminal_tab.py @@ -86,12 +86,13 @@ class TerminalTab(ctk.CTkFrame): session.rows = rows session.connect() else: - # Reused session — restore saved buffer instead of resetting + # Reused session — restore full screen state from pool saved_buf = self.session_pool.get_shell_state(alias) def _restore_buffer(): - self._terminal.reset() if saved_buf: - self._terminal.feed(saved_buf + b"\n") + self._terminal.restore_buffer(saved_buf) + else: + self._terminal.reset() self.after(0, _restore_buffer) # Set up callbacks even if session already existed diff --git a/gui/widgets/terminal_widget.py b/gui/widgets/terminal_widget.py index b5af99f..6d8b85f 100644 --- a/gui/widgets/terminal_widget.py +++ b/gui/widgets/terminal_widget.py @@ -362,21 +362,81 @@ class TerminalWidget(tk.Frame): return self._cols, self._rows def get_current_buffer(self) -> bytes: - """Export current pyte screen content as ANSI bytes for session pool preservation.""" - screen = self._screen - lines = [] - for row in range(screen.lines): - line = screen.buffer[row] - chars = [] - for col in range(screen.columns): - chars.append(line[col].data) - # Strip trailing spaces - text = "".join(chars).rstrip() - lines.append(text) - # Remove trailing empty lines - while lines and not lines[-1]: - lines.pop() - return "\n".join(lines).encode("utf-8") + """Export full pyte screen state as serialized data for session pool.""" + import pickle + + screen_state = { + 'buffer': {}, + 'cursor_x': self._screen.cursor.x, + 'cursor_y': self._screen.cursor.y, + 'cursor_attrs': self._screen.cursor.attrs, + 'cursor_hidden': self._screen.cursor.hidden, + 'mode': self._screen.mode.copy(), + 'columns': self._screen.columns, + 'lines': self._screen.lines, + } + + for y in range(self._screen.lines): + line_dict = {} + for x in range(self._screen.columns): + c = self._screen.buffer[y][x] + line_dict[x] = { + 'data': c.data, 'fg': c.fg, 'bg': c.bg, + 'bold': c.bold, 'italics': c.italics, + 'underscore': c.underscore, 'reverse': c.reverse, + 'strikethrough': c.strikethrough, 'width': c.width, + } + screen_state['buffer'][y] = line_dict + + return pickle.dumps(screen_state) + + def restore_buffer(self, buffer_data: bytes): + """Restore full pyte screen state from serialized data.""" + import pickle + + try: + state = pickle.loads(buffer_data) + if not isinstance(state, dict) or 'buffer' not in state: + return + + cols, rows = self._screen.columns, self._screen.lines + self._screen.reset() + self._screen.resize(rows, cols) + + for y, line_dict in state['buffer'].items(): + if y >= self._screen.lines: + continue + for x, cd in line_dict.items(): + if x >= self._screen.columns: + continue + ch = self._screen.buffer[y][x] + ch.data = cd['data'] + ch.fg = cd['fg'] + ch.bg = cd['bg'] + ch.bold = cd['bold'] + ch.italics = cd['italics'] + ch.underscore = cd['underscore'] + ch.reverse = cd['reverse'] + ch.strikethrough = cd['strikethrough'] + ch.width = cd['width'] + + self._screen.cursor.x = state.get('cursor_x', 0) + self._screen.cursor.y = state.get('cursor_y', 0) + if 'cursor_attrs' in state: + self._screen.cursor.attrs = state['cursor_attrs'] + if 'cursor_hidden' in state: + self._screen.cursor.hidden = state['cursor_hidden'] + if 'mode' in state: + self._screen.mode.clear() + self._screen.mode.update(state['mode']) + + # Force full redraw + self._screen.dirty.update(range(self._screen.lines)) + self._prev_buffer.clear() + self._render() + + except Exception: + self.reset() # ── Rendering ────────────────────────────────────────────────────────── diff --git a/releases/ServerManager-v1.8.3-win-x64.exe b/releases/ServerManager-v1.8.3-win-x64.exe index 2aba754..22bc10b 100644 Binary files a/releases/ServerManager-v1.8.3-win-x64.exe and b/releases/ServerManager-v1.8.3-win-x64.exe differ