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:
@@ -86,12 +86,13 @@ class TerminalTab(ctk.CTkFrame):
|
|||||||
session.rows = rows
|
session.rows = rows
|
||||||
session.connect()
|
session.connect()
|
||||||
else:
|
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)
|
saved_buf = self.session_pool.get_shell_state(alias)
|
||||||
def _restore_buffer():
|
def _restore_buffer():
|
||||||
self._terminal.reset()
|
|
||||||
if saved_buf:
|
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)
|
self.after(0, _restore_buffer)
|
||||||
|
|
||||||
# Set up callbacks even if session already existed
|
# Set up callbacks even if session already existed
|
||||||
|
|||||||
@@ -362,21 +362,81 @@ class TerminalWidget(tk.Frame):
|
|||||||
return self._cols, self._rows
|
return self._cols, self._rows
|
||||||
|
|
||||||
def get_current_buffer(self) -> bytes:
|
def get_current_buffer(self) -> bytes:
|
||||||
"""Export current pyte screen content as ANSI bytes for session pool preservation."""
|
"""Export full pyte screen state as serialized data for session pool."""
|
||||||
screen = self._screen
|
import pickle
|
||||||
lines = []
|
|
||||||
for row in range(screen.lines):
|
screen_state = {
|
||||||
line = screen.buffer[row]
|
'buffer': {},
|
||||||
chars = []
|
'cursor_x': self._screen.cursor.x,
|
||||||
for col in range(screen.columns):
|
'cursor_y': self._screen.cursor.y,
|
||||||
chars.append(line[col].data)
|
'cursor_attrs': self._screen.cursor.attrs,
|
||||||
# Strip trailing spaces
|
'cursor_hidden': self._screen.cursor.hidden,
|
||||||
text = "".join(chars).rstrip()
|
'mode': self._screen.mode.copy(),
|
||||||
lines.append(text)
|
'columns': self._screen.columns,
|
||||||
# Remove trailing empty lines
|
'lines': self._screen.lines,
|
||||||
while lines and not lines[-1]:
|
}
|
||||||
lines.pop()
|
|
||||||
return "\n".join(lines).encode("utf-8")
|
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 ──────────────────────────────────────────────────────────
|
# ── Rendering ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user