Skip to content

Commit 1739bb4

Browse files
committed
pythongh-132267: fix unsynchronized cursor position and buffer mismatch after resize
1 parent c55c020 commit 1739bb4

File tree

4 files changed

+104
-9
lines changed

4 files changed

+104
-9
lines changed

Lib/_pyrepl/console.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(
7171
self.output_fd = f_out.fileno()
7272

7373
@abstractmethod
74-
def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ...
74+
def refresh(self, screen: list[str], xy: tuple[int, int], repaint: bool) -> None: ...
7575

7676
@abstractmethod
7777
def prepare(self) -> None: ...
@@ -82,6 +82,9 @@ def restore(self) -> None: ...
8282
@abstractmethod
8383
def move_cursor(self, x: int, y: int) -> None: ...
8484

85+
@abstractmethod
86+
def sync_screen(self) -> None: ...
87+
8588
@abstractmethod
8689
def set_cursor_vis(self, visible: bool) -> None: ...
8790

Lib/_pyrepl/reader.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -631,14 +631,14 @@ def update_screen(self) -> None:
631631
if self.dirty:
632632
self.refresh()
633633

634-
def refresh(self) -> None:
634+
def refresh(self, repaint: bool = False) -> None:
635635
"""Recalculate and refresh the screen."""
636636
if self.in_bracketed_paste and self.buffer and not self.buffer[-1] == "\n":
637637
return
638638

639639
# this call sets up self.cxy, so call it first.
640640
self.screen = self.calc_screen()
641-
self.console.refresh(self.screen, self.cxy)
641+
self.console.refresh(self.screen, self.cxy, repaint)
642642
self.dirty = False
643643

644644
def do_cmd(self, cmd: tuple[str, list[str]]) -> None:
@@ -716,7 +716,8 @@ def handle1(self, block: bool = True) -> bool:
716716
elif event.evt == "scroll":
717717
self.refresh()
718718
elif event.evt == "resize":
719-
self.refresh()
719+
self.console.sync_screen()
720+
self.refresh(repaint=True)
720721
else:
721722
translate = False
722723

Lib/_pyrepl/unix_console.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,14 @@ def change_encoding(self, encoding: str) -> None:
226226
"""
227227
self.encoding = encoding
228228

229-
def refresh(self, screen, c_xy):
229+
def refresh(self, screen, c_xy, repaint: bool = False):
230230
"""
231231
Refresh the console screen.
232232
233233
Parameters:
234234
- screen (list): List of strings representing the screen contents.
235235
- c_xy (tuple): Cursor position (x, y) on the screen.
236+
- repaint (bool): If True, overwrite the old screen and not reuse.
236237
"""
237238
cx, cy = c_xy
238239
if not self.__gone_tall:
@@ -292,7 +293,14 @@ def refresh(self, screen, c_xy):
292293
oldline,
293294
newline,
294295
) in zip(range(offset, offset + height), oldscr, newscr):
295-
if oldline != newline:
296+
if repaint:
297+
# repaint the whole line
298+
self.__hide_cursor()
299+
self.__move(0, y)
300+
self.__write_code(self._el)
301+
self.__write(newline)
302+
self.posxy = wlen(newline), y
303+
elif oldline != newline:
296304
self.__write_changed_line(y, oldline, newline, px)
297305

298306
y = len(newscr)
@@ -324,6 +332,44 @@ def move_cursor(self, x, y):
324332
self.posxy = x, y
325333
self.flushoutput()
326334

335+
def sync_screen(self):
336+
"""
337+
Synchronize self.posxy, self.width and self.height.
338+
Assuming that the content of the screen doesn't change, only the width changes.
339+
"""
340+
if not self.screen:
341+
self.posxy = 0, 0
342+
return
343+
344+
px, py = self.posxy
345+
346+
groups = []
347+
new_line = True
348+
for line in self.screen[:py]:
349+
l = wlen(line)
350+
if new_line:
351+
groups.append(l)
352+
new_line = False
353+
else:
354+
groups[-1] += l
355+
if l != self.width:
356+
new_line = True
357+
358+
if new_line:
359+
groups.append(px)
360+
else:
361+
groups[-1] += px
362+
363+
new_height, new_width = self.getheightwidth()
364+
365+
ny = 0
366+
for group in groups:
367+
ny += group // new_width
368+
nx = groups[-1] % new_width
369+
370+
self.posxy = nx, ny
371+
self.height, self.width = new_height, new_width
372+
327373
def prepare(self):
328374
"""
329375
Prepare the console for input/output operations.
@@ -757,7 +803,6 @@ def __move_tall(self, x, y):
757803
self.__write_code(self._cup, y - self.__offset, x)
758804

759805
def __sigwinch(self, signum, frame):
760-
self.height, self.width = self.getheightwidth()
761806
self.event_queue.insert(Event("resize", None))
762807

763808
def __hide_cursor(self):

Lib/_pyrepl/windows_console.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,14 @@ def __init__(
158158
# Console I/O is redirected, fallback...
159159
self.out = None
160160

161-
def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
161+
def refresh(self, screen: list[str], c_xy: tuple[int, int], repaint: bool = False) -> None:
162162
"""
163163
Refresh the console screen.
164164
165165
Parameters:
166166
- screen (list): List of strings representing the screen contents.
167167
- c_xy (tuple): Cursor position (x, y) on the screen.
168+
- repaint (bool): If True, overwrite the old screen and not reuse.
168169
"""
169170
cx, cy = c_xy
170171

@@ -211,7 +212,13 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
211212
oldline,
212213
newline,
213214
) in zip(range(offset, offset + height), oldscr, newscr):
214-
if oldline != newline:
215+
if repaint:
216+
self._hide_cursor()
217+
self._move_relative(0, y)
218+
self._erase_to_end()
219+
self.__write(newline)
220+
self.posxy = wlen(newline), y
221+
elif oldline != newline:
215222
self.__write_changed_line(y, oldline, newline, px)
216223

217224
y = len(newscr)
@@ -384,6 +391,45 @@ def move_cursor(self, x: int, y: int) -> None:
384391
self._move_relative(x, y)
385392
self.posxy = x, y
386393

394+
def sync_screen(self):
395+
"""
396+
Synchronize self.posxy, self.width and self.height.
397+
Assuming that the content of the screen doesn't change, only the width changes.
398+
"""
399+
if not self.screen:
400+
self.posxy = 0, 0
401+
return
402+
403+
px, py = self.posxy
404+
405+
groups = []
406+
new_line = True
407+
for line in self.screen[:py]:
408+
l = wlen(line)
409+
if new_line:
410+
groups.append(l)
411+
new_line = False
412+
else:
413+
groups[-1] += l
414+
if l != self.width:
415+
new_line = True
416+
417+
if new_line:
418+
groups.append(px)
419+
else:
420+
groups[-1] += px
421+
422+
423+
new_height, new_width = self.getheightwidth()
424+
425+
ny = 0
426+
for group in groups:
427+
ny += group // new_width
428+
nx = groups[-1] % new_width
429+
430+
self.posxy = nx, ny
431+
self.height, self.width = new_height, new_width
432+
387433
def set_cursor_vis(self, visible: bool) -> None:
388434
if visible:
389435
self._show_cursor()

0 commit comments

Comments
 (0)