diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 8956fb1242e52a..0ca77aecf82f06 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -71,7 +71,7 @@ def __init__( self.output_fd = f_out.fileno() @abstractmethod - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... + def refresh(self, screen: list[str], xy: tuple[int, int], clear_to_end: bool = False) -> None: ... @abstractmethod def prepare(self) -> None: ... @@ -82,6 +82,9 @@ def restore(self) -> None: ... @abstractmethod def move_cursor(self, x: int, y: int) -> None: ... + @abstractmethod + def reset_cursor(self) -> None: ... + @abstractmethod def set_cursor_vis(self, visible: bool) -> None: ... diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 0ebd9162eca4bb..ad486314d29ba9 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -344,7 +344,9 @@ def calc_screen(self) -> list[str]: pos -= line_len + 1 prompt, prompt_len = self.process_prompt(prompt) chars, char_widths = disp_str(line, colors, offset) - wrapcount = (sum(char_widths) + prompt_len) // self.console.width + wrapcount = (sum(char_widths) + prompt_len) // (self.console.width - 1) # 1 for line continuations + if (sum(char_widths) + prompt_len) % (self.console.width - 1) == 0: + wrapcount -= 1 if wrapcount == 0 or not char_widths: offset += line_len + 1 # Takes all of the line plus the newline last_refresh_line_end_offsets.append(offset) @@ -639,6 +641,17 @@ def refresh(self) -> None: self.console.refresh(self.screen, self.cxy) self.dirty = False + def handle_resize(self) -> None: + """Handle a resize event.""" + self.console.height, self.console.width = self.console.getheightwidth() + self.console.reset_cursor() + ns = len(self.console.screen) * ["\000" * self.console.width] + self.console.screen = ns + + self.screen = self.calc_screen() + self.console.refresh(self.screen, self.cxy, clear_to_end=True) + self.dirty = True + def do_cmd(self, cmd: tuple[str, list[str]]) -> None: """`cmd` is a tuple of "event_name" and "event", which in the current implementation is always just the "buffer" which happens to be a list @@ -714,7 +727,7 @@ def handle1(self, block: bool = True) -> bool: elif event.evt == "scroll": self.refresh() elif event.evt == "resize": - self.refresh() + self.handle_resize() else: translate = False diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index d21cdd9b076d86..f08f5ba5fb3284 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -223,7 +223,7 @@ def change_encoding(self, encoding: str) -> None: """ self.encoding = encoding - def refresh(self, screen, c_xy): + def refresh(self, screen, c_xy, clear_to_end = False): """ Refresh the console screen. @@ -271,8 +271,9 @@ def refresh(self, screen, c_xy): self.posxy = 0, old_offset for i in range(old_offset - offset): self.__write_code(self._ri) - oldscr.pop(-1) oldscr.insert(0, "") + if len(oldscr) > height: + oldscr.pop(-1) elif old_offset < offset and self._ind: self.__hide_cursor() self.__write_code(self._cup, self.height - 1, 0) @@ -300,6 +301,12 @@ def refresh(self, screen, c_xy): self.__write_code(self._el) y += 1 + if clear_to_end: + self.__move(wlen(newscr[-1]), len(newscr) - 1 + self.__offset) + self.posxy = wlen(newscr[-1]), len(newscr) - 1 + self.__offset + self.__write_code(b"\x1b[J") # clear to end of line + self.flushoutput() + self.__show_cursor() self.screen = screen.copy() @@ -321,6 +328,10 @@ def move_cursor(self, x, y): self.posxy = x, y self.flushoutput() + def reset_cursor(self) -> None: + self.posxy = 0, self.__offset + self.__write_code(self._cup, 0, 0) + def prepare(self): """ Prepare the console for input/output operations. @@ -683,13 +694,18 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__write(newline[x_pos]) self.posxy = character_width + 1, y + if newline[-1] != oldline[-1]: + self.__move(self.width, y) + self.__write(newline[-1]) + self.posxy = self.width - 1, y + else: self.__hide_cursor() self.__move(x_coord, y) if wlen(oldline) > wlen(newline): self.__write_code(self._el) self.__write(newline[x_pos:]) - self.posxy = wlen(newline), y + self.posxy = min(wlen(newline), self.width - 1), y if "\x1b" in newline: # ANSI escape characters are present, so we can't assume @@ -752,7 +768,6 @@ def __move_tall(self, x, y): self.__write_code(self._cup, y - self.__offset, x) def __sigwinch(self, signum, frame): - self.height, self.width = self.getheightwidth() self.event_queue.insert(Event("resize", None)) def __hide_cursor(self): diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 95749198b3b2f9..5dfa5b3582e1a2 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -111,6 +111,7 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: MOVE_UP = "\x1b[{}A" MOVE_DOWN = "\x1b[{}B" CLEAR = "\x1b[H\x1b[J" +HOME = "\x1b[H" # State of control keys: https://learn.microsoft.com/en-us/windows/console/key-event-record-str ALT_ACTIVE = 0x01 | 0x02 @@ -171,7 +172,7 @@ def __init__( # Console I/O is redirected, fallback... self.out = None - def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: + def refresh(self, screen: list[str], c_xy: tuple[int, int], clear_to_end: bool = False) -> None: """ Refresh the console screen. @@ -234,6 +235,12 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self._erase_to_end() y += 1 + if clear_to_end: + self._move_relative(wlen(newscr[-1]), self.__offset + len(newscr) - 1) + self.posxy = wlen(newscr[-1]), self.__offset + len(newscr) - 1 + self.__write("\x1b[J") + self.flushoutput() + self._show_cursor() self.screen = screen @@ -287,7 +294,7 @@ def __write_changed_line( self._move_relative(0, y + 1) self.posxy = 0, y + 1 else: - self.posxy = wlen(newline), y + self.posxy = min(wlen(newline), self.width - 1), y if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline: # ANSI escape characters are present, so we can't assume @@ -395,6 +402,10 @@ def move_cursor(self, x: int, y: int) -> None: self._move_relative(x, y) self.posxy = x, y + def reset_cursor(self) -> None: + self.posxy = 0, self.__offset + self.__write(HOME) + def set_cursor_vis(self, visible: bool) -> None: if visible: self._show_cursor() diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index 4f7f9d77933336..c2a46152f21857 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -131,7 +131,7 @@ def getpending(self) -> Event: def getheightwidth(self) -> tuple[int, int]: return self.height, self.width - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: + def refresh(self, screen: list[str], xy: tuple[int, int], clear_to_end: bool = False) -> None: pass def prepare(self) -> None: @@ -143,6 +143,9 @@ def restore(self) -> None: def move_cursor(self, x: int, y: int) -> None: pass + def reset_cursor(self) -> None: + pass + def set_cursor_vis(self, visible: bool) -> None: pass diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index c447b310c49a06..406d859724b7a1 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -258,7 +258,7 @@ def test_resize_bigger_on_multiline_function(self, _os_write): reader, console = handle_events_short_unix_console(events) console.height = 2 - console.getheightwidth = MagicMock(lambda _: (2, 80)) + console.getheightwidth = MagicMock(return_value=(2, 80)) def same_reader(_): return reader @@ -294,7 +294,7 @@ def test_resize_smaller_on_multiline_function(self, _os_write): reader, console = handle_events_unix_console_height_3(events) console.height = 1 - console.getheightwidth = MagicMock(lambda _: (1, 80)) + console.getheightwidth = MagicMock(return_value=(1, 80)) def same_reader(_): return reader diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index e7bab226b31ddf..5f01abcfc67862 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -24,6 +24,7 @@ MOVE_UP, MOVE_DOWN, ERASE_IN_LINE, + HOME, ) import _pyrepl.windows_console as wc except ImportError: @@ -102,7 +103,7 @@ def test_resize_wider(self): console.height = 20 console.width = 80 - console.getheightwidth = MagicMock(lambda _: (20, 80)) + console.getheightwidth = MagicMock(return_value=(20, 80)) def same_reader(_): return reader @@ -117,9 +118,8 @@ def same_console(events): prepare_console=same_console, ) - con.out.write.assert_any_call(self.move_right(2)) - con.out.write.assert_any_call(self.move_up(2)) - con.out.write.assert_any_call(b"567890") + con.out.write.assert_any_call(self.home()) + con.out.write.assert_any_call(b"1234567890") con.restore() @@ -130,7 +130,7 @@ def test_resize_narrower(self): console.height = 20 console.width = 4 - console.getheightwidth = MagicMock(lambda _: (20, 4)) + console.getheightwidth = MagicMock(return_value=(20, 4)) def same_reader(_): return reader @@ -264,7 +264,7 @@ def test_resize_bigger_on_multiline_function(self): reader, console = self.handle_events_short(events) console.height = 2 - console.getheightwidth = MagicMock(lambda _: (2, 80)) + console.getheightwidth = MagicMock(return_value=(2, 80)) def same_reader(_): return reader @@ -280,8 +280,9 @@ def same_console(events): ) con.out.write.assert_has_calls( [ - call(self.move_left(5)), + call(self.home()), call(self.move_up()), + call(self.erase_in_line()), call(b"def f():"), call(self.move_left(3)), call(self.move_down()), @@ -302,7 +303,7 @@ def test_resize_smaller_on_multiline_function(self): reader, console = self.handle_events_height_3(events) console.height = 1 - console.getheightwidth = MagicMock(lambda _: (1, 80)) + console.getheightwidth = MagicMock(return_value=(1, 80)) def same_reader(_): return reader @@ -318,8 +319,7 @@ def same_console(events): ) con.out.write.assert_has_calls( [ - call(self.move_left(5)), - call(self.move_up()), + call(self.home()), call(self.erase_in_line()), call(b" foo"), ] @@ -342,6 +342,9 @@ def move_right(self, cols=1): def erase_in_line(self): return ERASE_IN_LINE.encode("utf8") + def home(self): + return HOME.encode("utf8") + def test_multiline_ctrl_z(self): # see gh-126332 code = "abcdefghi" diff --git a/Misc/NEWS.d/next/Library/2025-05-25-14-28-22.gh-issue-132267.lJOMvh.rst b/Misc/NEWS.d/next/Library/2025-05-25-14-28-22.gh-issue-132267.lJOMvh.rst new file mode 100644 index 00000000000000..16d7735cd1fe61 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-25-14-28-22.gh-issue-132267.lJOMvh.rst @@ -0,0 +1,2 @@ +In the New REPL, resize triggers redraw now. Also fixed cross-line width +calculation and content rendering during scrolling.