fix: proper terminal buffer save/restore with full pyte screen state

Save complete pyte screen state (characters, colors, attributes,
cursor position, modes) via pickle serialization instead of just
plain text. Restore via direct buffer manipulation + full redraw.
Fixes broken/garbled layout when switching between servers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-24 03:16:47 -05:00
parent bf5c4b14a4
commit afa75b6d9c
3 changed files with 79 additions and 18 deletions

View File

@@ -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

View File

@@ -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 ──────────────────────────────────────────────────────────