From 1061d2209e779fc6598a2c61a6bef740c97cb86d Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 16 May 2024 18:58:54 -0700 Subject: [PATCH 01/66] Windows repl support --- Lib/_pyrepl/__main__.py | 4 +- Lib/_pyrepl/readline.py | 14 +- Lib/_pyrepl/simple_interact.py | 12 +- Lib/_pyrepl/windows_console.py | 501 +++++++++++++++++++++++++++++++++ 4 files changed, 523 insertions(+), 8 deletions(-) create mode 100644 Lib/_pyrepl/windows_console.py diff --git a/Lib/_pyrepl/__main__.py b/Lib/_pyrepl/__main__.py index c598019e7cd4ad..784dc16ae00f79 100644 --- a/Lib/_pyrepl/__main__.py +++ b/Lib/_pyrepl/__main__.py @@ -1,7 +1,7 @@ import os import sys -CAN_USE_PYREPL = sys.platform != "win32" +CAN_USE_PYREPL = True #sys.platform != "win32" def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): @@ -36,6 +36,8 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): except Exception as e: from .trace import trace msg = f"warning: can't use pyrepl: {e}" + import traceback + traceback.print_exc() trace(msg) print(msg, file=sys.stderr) CAN_USE_PYREPL = False diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index d28a7f3779f302..423db4ce33b197 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -31,13 +31,19 @@ from dataclasses import dataclass, field import os -import readline +try: + import readline +except ImportError: + readline = None from site import gethistoryfile # type: ignore[attr-defined] import sys from . import commands, historical_reader from .completing_reader import CompletingReader -from .unix_console import UnixConsole, _error +try: + from .unix_console import UnixConsole as Console, _error +except: + from .windows_console import WindowsConsole as Console, _error ENCODING = sys.getdefaultencoding() or "latin1" @@ -81,7 +87,7 @@ @dataclass class ReadlineConfig: - readline_completer: Completer | None = readline.get_completer() + readline_completer: Completer | None = readline.get_completer() if readline is not None else None completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?") @@ -274,7 +280,7 @@ def __post_init__(self) -> None: def get_reader(self) -> ReadlineAlikeReader: if self.reader is None: - console = UnixConsole(self.f_in, self.f_out, encoding=ENCODING) + console = Console(self.f_in, self.f_out, encoding=ENCODING) self.reader = ReadlineAlikeReader(console=console, config=self.config) return self.reader diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 31b2097a78a226..f18d42a3bf9a98 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -33,8 +33,11 @@ from types import ModuleType from .readline import _get_reader, multiline_input -from .unix_console import _error +try: + from .unix_console import _error +except ModuleNotFoundError: + _error = OSError def check() -> str: """Returns the error message if there is a problem initializing the state.""" @@ -82,8 +85,11 @@ def run_multiline_interactive_console( mainmodule: ModuleType | None= None, future_flags: int = 0 ) -> None: import __main__ - from .readline import _setup - _setup() + try: + from .readline import _setup + _setup() + except ImportError: + pass mainmodule = mainmodule or __main__ console = InteractiveColoredConsole(mainmodule.__dict__, filename="") diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py new file mode 100644 index 00000000000000..a56f7dc544a244 --- /dev/null +++ b/Lib/_pyrepl/windows_console.py @@ -0,0 +1,501 @@ +# Copyright 2000-2004 Michael Hudson-Doyle +# +# All Rights Reserved +# +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose is hereby granted without fee, +# provided that the above copyright notice appear in all copies and +# that both that copyright notice and this permission notice appear in +# supporting documentation. +# +# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO +# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, +# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from _pyrepl.console import Event, Console +import ctypes +from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR +from ctypes import Structure, POINTER, Union +from ctypes import windll +import os +if False: + from typing import IO + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [ + ('dwSize', _COORD), + ('dwCursorPosition', _COORD), + ('wAttributes', WORD), + ('srWindow', SMALL_RECT), + ('dwMaximumWindowSize', _COORD), + ] + +class CONSOLE_CURSOR_INFO(Structure): + _fields_ = [ + ('dwSize', DWORD), + ('bVisible', BOOL), + ] + +STD_INPUT_HANDLE = -10 +STD_OUTPUT_HANDLE = -11 +GetStdHandle = windll.kernel32.GetStdHandle +GetStdHandle.argtypes = [ctypes.wintypes.DWORD] +GetStdHandle.restype = ctypes.wintypes.HANDLE + +GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo +GetConsoleScreenBufferInfo.use_last_error = True +GetConsoleScreenBufferInfo.argtypes = [HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] +GetConsoleScreenBufferInfo.restype = BOOL + +SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo +SetConsoleCursorInfo.use_last_error = True +SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +SetConsoleCursorInfo.restype = BOOL + +GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo +GetConsoleCursorInfo.use_last_error = True +GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +GetConsoleCursorInfo.restype = BOOL + +SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition +SetConsoleCursorPosition.argtypes = [HANDLE, _COORD] +SetConsoleCursorPosition.restype = BOOL +SetConsoleCursorPosition.use_last_error = True + +FillConsoleOutputCharacter = windll.kernel32.FillConsoleOutputCharacterW +FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] +FillConsoleOutputCharacter.restype = BOOL + +LOG = open('out.txt', 'w+') + +def log(*args): + LOG.write(" ".join((str(x) for x in args)) + "\n") + LOG.flush() + +class Char(Union): + _fields_ = [ + ("UnicodeChar",WCHAR), + ("Char", CHAR), + ] + +class KeyEvent(ctypes.Structure): + _fields_ = [ + ("bKeyDown", BOOL), + ("wRepeatCount", WORD), + ("wVirtualeyCode", WORD), + ("wVirtualScanCode", WORD), + ("uChar", Char), + ("dwControlKeyState", DWORD), + ] + +class ConsoleEvent(ctypes.Union): + _fields_ = [ + ("KeyEvent", KeyEvent), +# ("MouseEvent", ), +# ("WindowsBufferSizeEvent", ), +# ("MenuEvent", ) +# ("FocusEvent", ) + ] + + +KEY_EVENT = 0x01 +FOCUS_EVENT = 0x10 +MENU_EVENT = 0x08 +MOUSE_EVENT = 0x02 +WINDOW_BUFFER_SIZE_EVENT = 0x04 + +class INPUT_RECORD(Structure): + _fields_ = [ + ("EventType", WORD), + ("Event", ConsoleEvent) + ] + +ReadConsoleInput = windll.kernel32.ReadConsoleInputW +ReadConsoleInput.use_last__error = True +ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] +ReadConsoleInput.restype = BOOL + + + +OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) +InHandle = GetStdHandle(STD_INPUT_HANDLE) + +class _error(Exception): + pass + +def wlen(s: str) -> int: + return len(s) + +class WindowsConsole(Console): + def __init__( + self, + f_in: IO[bytes] | int = 0, + f_out: IO[bytes] | int = 1, + term: str = "", + encoding: str = "", + ): + self.encoding = encoding or sys.getdefaultencoding() + + if isinstance(f_in, int): + self.input_fd = f_in + else: + self.input_fd = f_in.fileno() + + if isinstance(f_out, int): + self.output_fd = f_out + else: + self.output_fd = f_out.fileno() + + + def refresh(self, screen, c_xy): + """ + Refresh the console screen. + + Parameters: + - screen (list): List of strings representing the screen contents. + - c_xy (tuple): Cursor position (x, y) on the screen. + """ + cx, cy = c_xy + log('Refresh', c_xy, self.screen_xy, self.__posxy) + if not self.__gone_tall: + while len(self.screen) < min(len(screen), self.height): + log('extend') + self.__hide_cursor() + self.__move(0, len(self.screen) - 1) + self.__write("\n") + self.__posxy = 0, len(self.screen) + self.screen.append("") + else: + while len(self.screen) < len(screen): + self.screen.append("") + + if len(screen) > self.height: + self.__gone_tall = 1 + self.__move = self.__move_absolute + + px, py = self.__posxy + old_offset = offset = self.__offset + height = self.height + + # we make sure the cursor is on the screen, and that we're + # using all of the screen if we can + if cy < offset: + offset = cy + elif cy >= offset + height: + offset = cy - height + 1 + elif offset > 0 and len(screen) < offset + height: + offset = max(len(screen) - height, 0) + screen.append("") + + oldscr = self.screen[old_offset : old_offset + height] + newscr = screen[offset : offset + height] + + # use hardware scrolling if we have it. + log('offsets', old_offset, offset) + if old_offset > offset: + log('old_offset > offset') + elif old_offset < offset: + log('old_offset < offset') + if False: + if old_offset > offset and self._ri: + self.__hide_cursor() + self.__write_code(self._cup, 0, 0) + self.__posxy = 0, old_offset + for i in range(old_offset - offset): + self.__write_code(self._ri) + oldscr.pop(-1) + oldscr.insert(0, "") + elif old_offset < offset and self._ind: + self.__hide_cursor() + self.__write_code(self._cup, self.height - 1, 0) + self.__posxy = 0, old_offset + self.height - 1 + for i in range(offset - old_offset): + self.__write_code(self._ind) + oldscr.pop(0) + oldscr.append("") + + log('new offset', offset, px) + self.__offset = offset + + for ( + y, + oldline, + newline, + ) in zip(range(offset, offset + height), oldscr, newscr): + if oldline != newline: + self.__write_changed_line(y, oldline, newline, px) + + y = len(newscr) + while y < len(oldscr): + log('need to erase', y) + self.__hide_cursor() + self.__move(0, y) + self.__posxy = 0, y +# self.__write_code(self._el) + y += 1 + + self.__show_cursor() + + self.screen = screen + log(f"Writing {self.screen} {cx} {cy}") + #self.move_cursor(cx, cy) + self.flushoutput() + + def __hide_cursor(self): + info = CONSOLE_CURSOR_INFO() + if not GetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + info.bVisible = False + if not SetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + def __show_cursor(self): + info = CONSOLE_CURSOR_INFO() + if not GetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + info.bVisible = True + if not SetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + def __write(self, text): + self.__buffer.append((text, 0)) + + def __move(self, x, y): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + x += info.dwCursorPosition.X + y += info.dwCursorPosition.Y + log('..', x, y) + self.move_cursor(x, y) + + @property + def screen_xy(self): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + return info.dwCursorPosition.X, info.dwCursorPosition.Y + + def __write_changed_line(self, y, oldline, newline, px_coord): + # this is frustrating; there's no reason to test (say) + # self.dch1 inside the loop -- but alternative ways of + # structuring this function are equally painful (I'm trying to + # avoid writing code generators these days...) + minlen = min(wlen(oldline), wlen(newline)) + x_pos = 0 + x_coord = 0 + + px_pos = 0 + j = 0 + for c in oldline: + if j >= px_coord: break + j += wlen(c) + px_pos += 1 + + # reuse the oldline as much as possible, but stop as soon as we + # encounter an ESCAPE, because it might be the start of an escape + # sequene + while x_coord < minlen and oldline[x_pos] == newline[x_pos] and newline[x_pos] != "\x1b": + x_coord += wlen(newline[x_pos]) + x_pos += 1 + + # if we need to insert a single character right after the first detected change + if oldline[x_pos:] == newline[x_pos + 1 :]: # and self.ich1: + if ( + y == self.__posxy[1] + and x_coord > self.__posxy[0] + and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1] + ): + x_pos = px_pos + x_coord = px_coord + character_width = wlen(newline[x_pos]) + log('sinle char', x_coord, y, px_coord) + self.__move(x_coord, y) +# self.__write_code(self.ich1) + self.__write(newline[x_pos]) + self.__posxy = x_coord + character_width, y + + # if it's a single character change in the middle of the line + elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]): + character_width = wlen(newline[x_pos]) + self.__move(x_coord, y) + self.__write(newline[x_pos]) + self.__posxy = x_coord + character_width, y + + # if this is the last character to fit in the line and we edit in the middle of the line + elif ( +# self.dch1 +# and self.ich1 and + wlen(newline) == self.width + and x_coord < wlen(newline) - 2 + and newline[x_pos + 1 : -1] == oldline[x_pos:-2] + ): + self.__hide_cursor() + self.__move(self.width - 2, y) + self.__posxy = self.width - 2, y +# self.__write_code(self.dch1) + + character_width = wlen(newline[x_pos]) + self.__move(x_coord, y) +# self.__write_code(self.ich1) + self.__write(newline[x_pos]) + self.__posxy = character_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 + + if "\x1b" in newline: + # ANSI escape characters are present, so we can't assume + # anything about the position of the cursor. Moving the cursor + # to the left margin should work to get to a known position. + self.move_cursor(0, y) + + def prepare(self) -> None: + self.screen = [] + self.height, self.width = self.getheightwidth() + + self.__buffer = [] + + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + self.__posxy = 0, 0 #info.dwCursorPosition.X, info.dwCursorPosition.Y + self.__gone_tall = 0 + self.__move = self.__move_relative + self.__offset = 0 + + def restore(self) -> None: ... + + def __move_relative(self, x, y): + log('move relative', x, y) + cur_x, cur_y = self.screen_xy + dx = x - self.__posxy[0] + dy = y - self.__posxy[1] + cur_x += dx + cur_y += dy + log('move is', cur_x, cur_y) + self.__move_absolute(cur_x, cur_y) + + def __move_absolute(self, x, y): + assert 0 <= y - self.__offset < self.height, y - self.__offset + cord = _COORD() + cord.X = x + cord.Y = y + if not SetConsoleCursorPosition(OutHandle, cord): + raise ctypes.WinError(ctypes.GetLastError()) + + def move_cursor(self, x: int, y: int) -> None: + log(f'move to {x} {y}') + + if x < 0 or y < 0: + raise ValueError(f"Bad cussor position {x}, {y}") + + self.__move(x, y) + self.__posxy = x, y + self.flushoutput() + + + def set_cursor_vis(self, visible: bool) -> None: + if visible: + self.__show_cursor() + else: + self.__hide_cursor() + + def getheightwidth(self) -> tuple[int, int]: + """Return (height, width) where height and width are the height + and width of the terminal window in characters.""" + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + return (info.srWindow.Bottom - info.srWindow.Top + 1, + info.srWindow.Right - info.srWindow.Left + 1) + + def get_event(self, block: bool = True) -> Event | None: + """Return an Event instance. Returns None if |block| is false + and there is no event pending, otherwise waits for the + completion of an event.""" + rec = INPUT_RECORD() + read = DWORD() + while True: + if not ReadConsoleInput(InHandle, rec, 1, read): + raise ctypes.WinError(ctypes.GetLastError()) + if read.value == 0 or rec.Event.KeyEvent.uChar.Char == b'\x00': + if block: + continue + return None + if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: + return False + key = chr(rec.Event.KeyEvent.uChar.Char[0]) + # self.push_char(key) + if rec.Event.KeyEvent.uChar.Char == b'\r': + return Event(evt="key", data="\n", raw="\n") + log('virtual key code', rec.Event.KeyEvent.wVirtualeyCode, rec.Event.KeyEvent.uChar.Char) + if rec.Event.KeyEvent.wVirtualeyCode == 8: + return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) + #print(key, rec.Event.KeyEvent.wVirtualeyCode) + return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.Char) + + def push_char(self, char: int | bytes) -> None: + """ + Push a character to the console event queue. + """ + log(f'put_char {char}') + + def beep(self) -> None: ... + + def clear(self) -> None: + """Wipe the screen""" + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + size = info.dwSize.X * info.dwSize.Y + if not FillConsoleOutputCharacter(OutHandle, b' ', size, _COORD(), DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + + def finish(self) -> None: + """Move the cursor to the end of the display and otherwise get + ready for end. XXX could be merged with restore? Hmm.""" + ... + + def flushoutput(self) -> None: + """Flush all output to the screen (assuming there's some + buffering going on somewhere).""" + for text, iscode in self.__buffer: + if iscode: + self.__tputs(text) + else: + os.write(self.output_fd, text.encode(self.encoding, "replace")) + del self.__buffer[:] + + def forgetinput(self) -> None: + """Forget all pending, but not yet processed input.""" + ... + + def getpending(self) -> Event: + """Return the characters that have been typed but not yet + processed.""" + ... + + def wait(self) -> None: + """Wait for an event.""" + ... + + def repaint(self) -> None: + log('repaint') From fa0f538ee01d1c3e5ee538468dc3c439e2bb53d9 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 11:04:53 -0700 Subject: [PATCH 02/66] Arrow key support --- Lib/_pyrepl/windows_console.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index a56f7dc544a244..d7613b41d76b3c 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -91,7 +91,7 @@ class KeyEvent(ctypes.Structure): _fields_ = [ ("bKeyDown", BOOL), ("wRepeatCount", WORD), - ("wVirtualeyCode", WORD), + ("wVirtualKeyCode", WORD), ("wVirtualScanCode", WORD), ("uChar", Char), ("dwControlKeyState", DWORD), @@ -129,6 +129,15 @@ class INPUT_RECORD(Structure): OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) InHandle = GetStdHandle(STD_INPUT_HANDLE) +VK_MAP: dict[int, str] = { + 0x23: "end", # VK_END + 0x24: "home", # VK_HOME + 0x25: "left", # VK_LEFT + 0x26: "up", # VK_UP + 0x27: "right", # VK_RIGHT + 0x28: "down", # VK_DOWN +} + class _error(Exception): pass @@ -436,7 +445,7 @@ def get_event(self, block: bool = True) -> Event | None: while True: if not ReadConsoleInput(InHandle, rec, 1, read): raise ctypes.WinError(ctypes.GetLastError()) - if read.value == 0 or rec.Event.KeyEvent.uChar.Char == b'\x00': + if read.value == 0: if block: continue return None @@ -446,10 +455,15 @@ def get_event(self, block: bool = True) -> Event | None: # self.push_char(key) if rec.Event.KeyEvent.uChar.Char == b'\r': return Event(evt="key", data="\n", raw="\n") - log('virtual key code', rec.Event.KeyEvent.wVirtualeyCode, rec.Event.KeyEvent.uChar.Char) - if rec.Event.KeyEvent.wVirtualeyCode == 8: + log('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) + if rec.Event.KeyEvent.wVirtualKeyCode == 8: return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) - #print(key, rec.Event.KeyEvent.wVirtualeyCode) + if rec.Event.KeyEvent.uChar.Char == b'\x00': + code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) + if code: + return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.Char) + continue + #print(key, rec.Event.KeyEvent.wVirtualKeyCode) return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.Char) def push_char(self, char: int | bytes) -> None: From 6f351002b389380d11bf44bc43d0e9f7be5cc9c0 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 12:10:43 -0700 Subject: [PATCH 03/66] Make backspace clear char --- Lib/_pyrepl/windows_console.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index d7613b41d76b3c..bfb307a7f08ab7 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -362,10 +362,18 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = character_width + 1, y else: + log("Rewrite all", x_coord, len(oldline)) self.__hide_cursor() self.__move(x_coord, y) -# if wlen(oldline) > wlen(newline): -# self.__write_code(self._el) + if wlen(oldline) > wlen(newline): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X + if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + self.__write(newline[x_pos:]) self.__posxy = wlen(newline), y From 77727879343dd7c55ca9265fc86e0742614e28af Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 14:41:46 -0700 Subject: [PATCH 04/66] Fix missing newline after input --- Lib/_pyrepl/windows_console.py | 71 ++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index bfb307a7f08ab7..88c2ee944df485 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -22,6 +22,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from _pyrepl.console import Event, Console +from .trace import trace import ctypes from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR from ctypes import Structure, POINTER, Union @@ -75,12 +76,6 @@ class CONSOLE_CURSOR_INFO(Structure): FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] FillConsoleOutputCharacter.restype = BOOL -LOG = open('out.txt', 'w+') - -def log(*args): - LOG.write(" ".join((str(x) for x in args)) + "\n") - LOG.flush() - class Char(Union): _fields_ = [ ("UnicodeChar",WCHAR), @@ -136,6 +131,27 @@ class INPUT_RECORD(Structure): 0x26: "up", # VK_UP 0x27: "right", # VK_RIGHT 0x28: "down", # VK_DOWN + 0x2E: "delete", # VK_DELETE + 0x70: "f1", # VK_F1 + 0x71: "f2", # VK_F2 + 0x72: "f3", # VK_F3 + 0x73: "f4", # VK_F4 + 0x74: "f5", # VK_F5 + 0x75: "f6", # VK_F6 + 0x76: "f7", # VK_F7 + 0x77: "f8", # VK_F8 + 0x78: "f9", # VK_F9 + 0x79: "f10", # VK_F10 + 0x7A: "f11", # VK_F11 + 0x7B: "f12", # VK_F12 + 0x7C: "f13", # VK_F13 + 0x7D: "f14", # VK_F14 + 0x7E: "f15", # VK_F15 + 0x7F: "f16", # VK_F16 + 0x79: "f17", # VK_F17 + 0x80: "f18", # VK_F18 + 0x81: "f19", # VK_F19 + 0x82: "f20", # VK_F20 } class _error(Exception): @@ -174,10 +190,10 @@ def refresh(self, screen, c_xy): - c_xy (tuple): Cursor position (x, y) on the screen. """ cx, cy = c_xy - log('Refresh', c_xy, self.screen_xy, self.__posxy) + trace('!!Refresh {}', screen) if not self.__gone_tall: while len(self.screen) < min(len(screen), self.height): - log('extend') + trace('extend') self.__hide_cursor() self.__move(0, len(self.screen) - 1) self.__write("\n") @@ -209,11 +225,11 @@ def refresh(self, screen, c_xy): newscr = screen[offset : offset + height] # use hardware scrolling if we have it. - log('offsets', old_offset, offset) + trace('offsets', old_offset, offset) if old_offset > offset: - log('old_offset > offset') + trace('old_offset > offset') elif old_offset < offset: - log('old_offset < offset') + trace('old_offset < offset') if False: if old_offset > offset and self._ri: self.__hide_cursor() @@ -232,7 +248,7 @@ def refresh(self, screen, c_xy): oldscr.pop(0) oldscr.append("") - log('new offset', offset, px) + trace('new offset', offset, px) self.__offset = offset for ( @@ -245,7 +261,7 @@ def refresh(self, screen, c_xy): y = len(newscr) while y < len(oldscr): - log('need to erase', y) + trace('need to erase', y) self.__hide_cursor() self.__move(0, y) self.__posxy = 0, y @@ -255,7 +271,7 @@ def refresh(self, screen, c_xy): self.__show_cursor() self.screen = screen - log(f"Writing {self.screen} {cx} {cy}") + trace(f"Writing {self.screen} {cx} {cy}") #self.move_cursor(cx, cy) self.flushoutput() @@ -286,7 +302,7 @@ def __move(self, x, y): raise ctypes.WinError(ctypes.GetLastError()) x += info.dwCursorPosition.X y += info.dwCursorPosition.Y - log('..', x, y) + trace('..', x, y) self.move_cursor(x, y) @property @@ -329,7 +345,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): x_pos = px_pos x_coord = px_coord character_width = wlen(newline[x_pos]) - log('sinle char', x_coord, y, px_coord) + trace('sinle char', x_coord, y, px_coord) self.__move(x_coord, y) # self.__write_code(self.ich1) self.__write(newline[x_pos]) @@ -362,7 +378,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = character_width + 1, y else: - log("Rewrite all", x_coord, len(oldline)) + trace("Rewrite all", x_coord, len(oldline)) self.__hide_cursor() self.__move(x_coord, y) if wlen(oldline) > wlen(newline): @@ -401,13 +417,13 @@ def prepare(self) -> None: def restore(self) -> None: ... def __move_relative(self, x, y): - log('move relative', x, y) + trace('move relative', x, y) cur_x, cur_y = self.screen_xy dx = x - self.__posxy[0] dy = y - self.__posxy[1] cur_x += dx cur_y += dy - log('move is', cur_x, cur_y) + trace('move is', cur_x, cur_y) self.__move_absolute(cur_x, cur_y) def __move_absolute(self, x, y): @@ -419,7 +435,7 @@ def __move_absolute(self, x, y): raise ctypes.WinError(ctypes.GetLastError()) def move_cursor(self, x: int, y: int) -> None: - log(f'move to {x} {y}') + trace(f'move to {x} {y}') if x < 0 or y < 0: raise ValueError(f"Bad cussor position {x}, {y}") @@ -463,9 +479,11 @@ def get_event(self, block: bool = True) -> Event | None: # self.push_char(key) if rec.Event.KeyEvent.uChar.Char == b'\r': return Event(evt="key", data="\n", raw="\n") - log('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) + trace('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) if rec.Event.KeyEvent.wVirtualKeyCode == 8: return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) + if rec.Event.KeyEvent.wVirtualKeyCode == 27: + return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.Char) if rec.Event.KeyEvent.uChar.Char == b'\x00': code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: @@ -478,7 +496,7 @@ def push_char(self, char: int | bytes) -> None: """ Push a character to the console event queue. """ - log(f'put_char {char}') + trace(f'put_char {char}') def beep(self) -> None: ... @@ -494,7 +512,12 @@ def clear(self) -> None: def finish(self) -> None: """Move the cursor to the end of the display and otherwise get ready for end. XXX could be merged with restore? Hmm.""" - ... + y = len(self.screen) - 1 + while y >= 0 and not self.screen[y]: + y -= 1 + self.__move(0, min(y, self.height + self.__offset - 1)) + self.__write("\r\n") + self.flushoutput() def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some @@ -520,4 +543,4 @@ def wait(self) -> None: ... def repaint(self) -> None: - log('repaint') + trace('repaint') From aced5ae12a14a5e3f7b13ef2032c51e6cf884a6f Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 15:09:40 -0700 Subject: [PATCH 05/66] Make insert work --- Lib/_pyrepl/windows_console.py | 37 +++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 88c2ee944df485..04e18ccd7659dc 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -46,6 +46,12 @@ class CONSOLE_CURSOR_INFO(Structure): ('bVisible', BOOL), ] +class CHAR_INFO(Structure): + _fields_ = [ + ('UnicodeChar', WCHAR), + ('Attributes', WORD), + ] + STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 GetStdHandle = windll.kernel32.GetStdHandle @@ -73,9 +79,15 @@ class CONSOLE_CURSOR_INFO(Structure): SetConsoleCursorPosition.use_last_error = True FillConsoleOutputCharacter = windll.kernel32.FillConsoleOutputCharacterW +FillConsoleOutputCharacter.use_last_error = True FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] FillConsoleOutputCharacter.restype = BOOL +ScrollConsoleScreenBuffer = windll.kernel32.ScrollConsoleScreenBufferW +ScrollConsoleScreenBuffer.use_last_error = True +ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] +ScrollConsoleScreenBuffer.rettype = BOOL + class Char(Union): _fields_ = [ ("UnicodeChar",WCHAR), @@ -336,7 +348,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): x_pos += 1 # if we need to insert a single character right after the first detected change - if oldline[x_pos:] == newline[x_pos + 1 :]: # and self.ich1: + if oldline[x_pos:] == newline[x_pos + 1 :]: if ( y == self.__posxy[1] and x_coord > self.__posxy[0] @@ -345,9 +357,20 @@ def __write_changed_line(self, y, oldline, newline, px_coord): x_pos = px_pos x_coord = px_coord character_width = wlen(newline[x_pos]) - trace('sinle char', x_coord, y, px_coord) + + ins_x, ins_y = self.get_abs_position(x_coord + 1, y) + ins_x -= 1 + scroll_rect = SMALL_RECT() + scroll_rect.Top = scroll_rect.Bottom = ins_y + scroll_rect.Left = ins_x + scroll_rect.Right = self.getheightwidth()[1] - 1 + destination_origin = _COORD(X = scroll_rect.Left + 1, Y = scroll_rect.Top) + fill_info = CHAR_INFO() + fill_info.UnicodeChar = ' ' + + if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): + raise ctypes.WinError(ctypes.GetLastError()) self.__move(x_coord, y) -# self.__write_code(self.ich1) self.__write(newline[x_pos]) self.__posxy = x_coord + character_width, y @@ -418,13 +441,17 @@ def restore(self) -> None: ... def __move_relative(self, x, y): trace('move relative', x, y) + cur_x, cur_y = self.get_abs_position(x, y) + trace('move is', cur_x, cur_y) + self.__move_absolute(cur_x, cur_y) + + def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_x, cur_y = self.screen_xy dx = x - self.__posxy[0] dy = y - self.__posxy[1] cur_x += dx cur_y += dy - trace('move is', cur_x, cur_y) - self.__move_absolute(cur_x, cur_y) + return cur_x, cur_y def __move_absolute(self, x, y): assert 0 <= y - self.__offset < self.height, y - self.__offset From fa3815fa0b1c54bb0c983b4970d17fe0f0e14c01 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 15:18:40 -0700 Subject: [PATCH 06/66] Fix delete in middle --- Lib/_pyrepl/windows_console.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 04e18ccd7659dc..112b49fa872d6b 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -284,8 +284,8 @@ def refresh(self, screen, c_xy): self.screen = screen trace(f"Writing {self.screen} {cx} {cy}") - #self.move_cursor(cx, cy) self.flushoutput() + self.move_cursor(cx, cy) def __hide_cursor(self): info = CONSOLE_CURSOR_INFO() @@ -412,15 +412,15 @@ def __write_changed_line(self, y, oldline, newline, px_coord): size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): raise ctypes.WinError(ctypes.GetLastError()) - + #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) self.__write(newline[x_pos:]) self.__posxy = wlen(newline), y - if "\x1b" in newline: + #if "\x1b" in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. - self.move_cursor(0, y) + # self.move_cursor(0, y) def prepare(self) -> None: self.screen = [] @@ -440,9 +440,9 @@ def prepare(self) -> None: def restore(self) -> None: ... def __move_relative(self, x, y): - trace('move relative', x, y) + trace('move relative {} {}', x, y) cur_x, cur_y = self.get_abs_position(x, y) - trace('move is', cur_x, cur_y) + trace('move is {} {}', cur_x, cur_y) self.__move_absolute(cur_x, cur_y) def get_abs_position(self, x: int, y: int) -> tuple[int, int]: From b30b1055fed115700d2e0d1e63061067ca0920a6 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 15:22:26 -0700 Subject: [PATCH 07/66] Fix crash on invalid command key --- Lib/_pyrepl/windows_console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 112b49fa872d6b..c079af1f22429a 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -563,7 +563,7 @@ def forgetinput(self) -> None: def getpending(self) -> Event: """Return the characters that have been typed but not yet processed.""" - ... + return Event("key", "", b"") def wait(self) -> None: """Wait for an event.""" From df262c4b7f861a51ea9bc5382cb824a56fce9ad1 Mon Sep 17 00:00:00 2001 From: dino Date: Sun, 19 May 2024 19:18:39 -0700 Subject: [PATCH 08/66] More fixes --- Lib/_pyrepl/windows_console.py | 54 +++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index c079af1f22429a..fb120ee505b93f 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -205,7 +205,7 @@ def refresh(self, screen, c_xy): trace('!!Refresh {}', screen) if not self.__gone_tall: while len(self.screen) < min(len(screen), self.height): - trace('extend') + cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) self.__hide_cursor() self.__move(0, len(self.screen) - 1) self.__write("\n") @@ -237,7 +237,7 @@ def refresh(self, screen, c_xy): newscr = screen[offset : offset + height] # use hardware scrolling if we have it. - trace('offsets', old_offset, offset) + trace('offsets {} {}', old_offset, offset) if old_offset > offset: trace('old_offset > offset') elif old_offset < offset: @@ -260,7 +260,7 @@ def refresh(self, screen, c_xy): oldscr.pop(0) oldscr.append("") - trace('new offset', offset, px) + trace('new offset {} {}', offset, px) self.__offset = offset for ( @@ -277,7 +277,7 @@ def refresh(self, screen, c_xy): self.__hide_cursor() self.__move(0, y) self.__posxy = 0, y -# self.__write_code(self._el) + self.erase_to_end() y += 1 self.__show_cursor() @@ -306,7 +306,8 @@ def __show_cursor(self): raise ctypes.WinError(ctypes.GetLastError()) def __write(self, text): - self.__buffer.append((text, 0)) + os.write(self.output_fd, text.encode(self.encoding, "replace")) + #self.__buffer.append((text, 0)) def __move(self, x, y): info = CONSOLE_SCREEN_BUFFER_INFO() @@ -349,6 +350,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): # if we need to insert a single character right after the first detected change if oldline[x_pos:] == newline[x_pos + 1 :]: + trace('insert single') if ( y == self.__posxy[1] and x_coord > self.__posxy[0] @@ -376,6 +378,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): # if it's a single character change in the middle of the line elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]): + trace('char change') character_width = wlen(newline[x_pos]) self.__move(x_coord, y) self.__write(newline[x_pos]) @@ -389,6 +392,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): and x_coord < wlen(newline) - 2 and newline[x_pos + 1 : -1] == oldline[x_pos:-2] ): + trace('last char') self.__hide_cursor() self.__move(self.width - 2, y) self.__posxy = self.width - 2, y @@ -401,38 +405,42 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = character_width + 1, y else: - trace("Rewrite all", x_coord, len(oldline)) + trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() self.__move(x_coord, y) if wlen(oldline) > wlen(newline): - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - - size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X - if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) + self.erase_to_end() #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) self.__write(newline[x_pos:]) self.__posxy = wlen(newline), y + #if "\x1b" in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. # self.move_cursor(0, y) + def erase_to_end(self): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X + if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + def prepare(self) -> None: self.screen = [] self.height, self.width = self.getheightwidth() - self.__buffer = [] + #self.__buffer = [] info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - self.__posxy = 0, 0 #info.dwCursorPosition.X, info.dwCursorPosition.Y + self.__posxy = 0, 0 self.__gone_tall = 0 self.__move = self.__move_relative self.__offset = 0 @@ -440,7 +448,7 @@ def prepare(self) -> None: def restore(self) -> None: ... def __move_relative(self, x, y): - trace('move relative {} {}', x, y) + trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) trace('move is {} {}', cur_x, cur_y) self.__move_absolute(cur_x, cur_y) @@ -454,7 +462,6 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: return cur_x, cur_y def __move_absolute(self, x, y): - assert 0 <= y - self.__offset < self.height, y - self.__offset cord = _COORD() cord.X = x cord.Y = y @@ -549,12 +556,13 @@ def finish(self) -> None: def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some buffering going on somewhere).""" - for text, iscode in self.__buffer: - if iscode: - self.__tputs(text) - else: - os.write(self.output_fd, text.encode(self.encoding, "replace")) - del self.__buffer[:] + if False: + for text, iscode in self.__buffer: + if iscode: + self.__tputs(text) + else: + os.write(self.output_fd, text.encode(self.encoding, "replace")) + del self.__buffer[:] def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" From 6e5731683f8cd76dfa1b0f15a1ea49c54e1cad25 Mon Sep 17 00:00:00 2001 From: dino Date: Sun, 19 May 2024 19:43:09 -0700 Subject: [PATCH 09/66] Colorize --- Lib/_pyrepl/windows_console.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index fb120ee505b93f..6f06c580645088 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -86,7 +86,12 @@ class CHAR_INFO(Structure): ScrollConsoleScreenBuffer = windll.kernel32.ScrollConsoleScreenBufferW ScrollConsoleScreenBuffer.use_last_error = True ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] -ScrollConsoleScreenBuffer.rettype = BOOL +ScrollConsoleScreenBuffer.restype = BOOL + +SetConsoleMode = windll.kernel32.SetConsoleMode +SetConsoleMode.use_last_error = True +SetConsoleMode.argtypes = [HANDLE, DWORD] +SetConsoleMode.restype = BOOL class Char(Union): _fields_ = [ @@ -180,6 +185,7 @@ def __init__( term: str = "", encoding: str = "", ): + SetConsoleMode(OutHandle, 0x0004 | 0x0001) self.encoding = encoding or sys.getdefaultencoding() if isinstance(f_in, int): @@ -415,11 +421,13 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = wlen(newline), y - #if "\x1b" in newline: + if "\x1b" in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. - # self.move_cursor(0, y) + _, cur_y = self.get_abs_position(0, y) + self.__move_absolute(0, cur_y) + self.__posxy = 0, y def erase_to_end(self): info = CONSOLE_SCREEN_BUFFER_INFO() From 18ecc2e2c6c9fdb33d157aaa59c150b67d1f3473 Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 20 May 2024 06:55:12 -0700 Subject: [PATCH 10/66] Use constants --- Lib/_pyrepl/windows_console.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 6f06c580645088..c897b16dd0e68d 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -19,6 +19,9 @@ from __future__ import annotations +import os +import sys + from abc import ABC, abstractmethod from dataclasses import dataclass, field from _pyrepl.console import Event, Console @@ -27,7 +30,7 @@ from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR from ctypes import Structure, POINTER, Union from ctypes import windll -import os + if False: from typing import IO @@ -171,6 +174,9 @@ class INPUT_RECORD(Structure): 0x82: "f20", # VK_F20 } +ENABLE_PROCESSED_OUTPUT = 0x01 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 + class _error(Exception): pass @@ -185,7 +191,8 @@ def __init__( term: str = "", encoding: str = "", ): - SetConsoleMode(OutHandle, 0x0004 | 0x0001) + + SetConsoleMode(OutHandle, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) self.encoding = encoding or sys.getdefaultencoding() if isinstance(f_in, int): From 60525ebef6f35d31ea24600475c94c2b724b12df Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 20 May 2024 19:28:24 -0700 Subject: [PATCH 11/66] Use UnicodeChar --- Lib/_pyrepl/windows_console.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index c897b16dd0e68d..d102b73ae4701a 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -524,22 +524,20 @@ def get_event(self, block: bool = True) -> Event | None: return None if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: return False - key = chr(rec.Event.KeyEvent.uChar.Char[0]) - # self.push_char(key) - if rec.Event.KeyEvent.uChar.Char == b'\r': - return Event(evt="key", data="\n", raw="\n") - trace('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) + key = rec.Event.KeyEvent.uChar.UnicodeChar + if rec.Event.KeyEvent.uChar.UnicodeChar == '\r': + return Event(evt="key", data="\n", raw="\n") if rec.Event.KeyEvent.wVirtualKeyCode == 8: - return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) + return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.UnicodeChar) if rec.Event.KeyEvent.wVirtualKeyCode == 27: - return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.Char) - if rec.Event.KeyEvent.uChar.Char == b'\x00': + return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.UnicodeChar) + if rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: - return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.Char) + return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar) continue #print(key, rec.Event.KeyEvent.wVirtualKeyCode) - return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.Char) + return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar) def push_char(self, char: int | bytes) -> None: """ From af79a1787ab020e911778448b7805538b042242d Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 20 May 2024 19:57:10 -0700 Subject: [PATCH 12/66] Simplify --- Lib/_pyrepl/windows_console.py | 70 ++++++++++++---------------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index d102b73ae4701a..0e5a821bfe2475 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -389,34 +389,6 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__write(newline[x_pos]) self.__posxy = x_coord + character_width, y - # if it's a single character change in the middle of the line - elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]): - trace('char change') - character_width = wlen(newline[x_pos]) - self.__move(x_coord, y) - self.__write(newline[x_pos]) - self.__posxy = x_coord + character_width, y - - # if this is the last character to fit in the line and we edit in the middle of the line - elif ( -# self.dch1 -# and self.ich1 and - wlen(newline) == self.width - and x_coord < wlen(newline) - 2 - and newline[x_pos + 1 : -1] == oldline[x_pos:-2] - ): - trace('last char') - self.__hide_cursor() - self.__move(self.width - 2, y) - self.__posxy = self.width - 2, y -# self.__write_code(self.dch1) - - character_width = wlen(newline[x_pos]) - self.__move(x_coord, y) -# self.__write_code(self.ich1) - self.__write(newline[x_pos]) - self.__posxy = character_width + 1, y - else: trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() @@ -492,7 +464,6 @@ def move_cursor(self, x: int, y: int) -> None: self.__move(x, y) self.__posxy = x, y self.flushoutput() - def set_cursor_vis(self, visible: bool) -> None: if visible: @@ -513,30 +484,44 @@ def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false and there is no event pending, otherwise waits for the completion of an event.""" - rec = INPUT_RECORD() - read = DWORD() while True: + rec = INPUT_RECORD() + read = DWORD() if not ReadConsoleInput(InHandle, rec, 1, read): raise ctypes.WinError(ctypes.GetLastError()) + if read.value == 0: if block: continue return None + if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: - return False + # Only process keys and keydown events + if block: + continue + return None + key = rec.Event.KeyEvent.uChar.UnicodeChar + if rec.Event.KeyEvent.uChar.UnicodeChar == '\r': + # Make enter make unix-like return Event(evt="key", data="\n", raw="\n") - if rec.Event.KeyEvent.wVirtualKeyCode == 8: + elif rec.Event.KeyEvent.wVirtualKeyCode == 8: + # Turn backspace directly into the command return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.UnicodeChar) - if rec.Event.KeyEvent.wVirtualKeyCode == 27: + elif rec.Event.KeyEvent.wVirtualKeyCode == 27: + # Turn escape directly into the command return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.UnicodeChar) - if rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': + elif rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': + # Handle special keys like arrow keys and translate them into the appropriate command code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar) - continue - #print(key, rec.Event.KeyEvent.wVirtualKeyCode) + if block: + continue + + return None + return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar) def push_char(self, char: int | bytes) -> None: @@ -564,19 +549,12 @@ def finish(self) -> None: y -= 1 self.__move(0, min(y, self.height + self.__offset - 1)) self.__write("\r\n") - self.flushoutput() def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some buffering going on somewhere).""" - if False: - for text, iscode in self.__buffer: - if iscode: - self.__tputs(text) - else: - os.write(self.output_fd, text.encode(self.encoding, "replace")) - del self.__buffer[:] - + pass + def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" ... From 5f99256f4ab01482679a73b1c0de81068006cc2e Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 21 May 2024 11:07:41 -0700 Subject: [PATCH 13/66] Fix scrolling on input which is longer than screen/scrollback --- Lib/_pyrepl/windows_console.py | 132 +++++++++++++++++---------------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 0e5a821bfe2475..85aef5253dda67 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -27,7 +27,7 @@ from _pyrepl.console import Event, Console from .trace import trace import ctypes -from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR +from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR, SHORT from ctypes import Structure, POINTER, Union from ctypes import windll @@ -215,22 +215,17 @@ def refresh(self, screen, c_xy): - c_xy (tuple): Cursor position (x, y) on the screen. """ cx, cy = c_xy + trace('!!Refresh {}', screen) - if not self.__gone_tall: - while len(self.screen) < min(len(screen), self.height): - cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) - self.__hide_cursor() - self.__move(0, len(self.screen) - 1) - self.__write("\n") - self.__posxy = 0, len(self.screen) - self.screen.append("") - else: - while len(self.screen) < len(screen): - self.screen.append("") + while len(self.screen) < min(len(screen), self.height): + trace("...") + cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) + self.__hide_cursor() + self.__move_relative(0, len(self.screen) - 1) + self.__write("\n") + self.__posxy = 0, len(self.screen) + self.screen.append("") - if len(screen) > self.height: - self.__gone_tall = 1 - self.__move = self.__move_absolute px, py = self.__posxy old_offset = offset = self.__offset @@ -242,36 +237,35 @@ def refresh(self, screen, c_xy): offset = cy elif cy >= offset + height: offset = cy - height + 1 + scroll_lines = offset - old_offset + + trace(f'adj offset {cy} {height} {offset} {old_offset}') + # Scrolling the buffer as the current input is greater than the visible + # portion of the window. We need to scroll the visible portion and the + # entire history + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + bottom = info.srWindow.Bottom + + trace("Scrolling {} {} {} {} {}", scroll_lines, info.srWindow.Bottom, self.height, len(screen) - self.height, self.__posxy) + self.scroll(scroll_lines, bottom) + self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines + self.__offset += scroll_lines + + for i in range(scroll_lines): + self.screen.append("") + elif offset > 0 and len(screen) < offset + height: + trace("Adding extra line") offset = max(len(screen) - height, 0) - screen.append("") + #screen.append("") oldscr = self.screen[old_offset : old_offset + height] newscr = screen[offset : offset + height] - - # use hardware scrolling if we have it. - trace('offsets {} {}', old_offset, offset) - if old_offset > offset: - trace('old_offset > offset') - elif old_offset < offset: - trace('old_offset < offset') - if False: - if old_offset > offset and self._ri: - self.__hide_cursor() - self.__write_code(self._cup, 0, 0) - self.__posxy = 0, old_offset - for i in range(old_offset - offset): - self.__write_code(self._ri) - oldscr.pop(-1) - oldscr.insert(0, "") - elif old_offset < offset and self._ind: - self.__hide_cursor() - self.__write_code(self._cup, self.height - 1, 0) - self.__posxy = 0, old_offset + self.height - 1 - for i in range(offset - old_offset): - self.__write_code(self._ind) - oldscr.pop(0) - oldscr.append("") + trace('old screen {}', oldscr) + trace('new screen {}', newscr) trace('new offset {} {}', offset, px) self.__offset = offset @@ -286,9 +280,8 @@ def refresh(self, screen, c_xy): y = len(newscr) while y < len(oldscr): - trace('need to erase', y) self.__hide_cursor() - self.__move(0, y) + self.__move_relative(0, y) self.__posxy = 0, y self.erase_to_end() y += 1 @@ -296,10 +289,23 @@ def refresh(self, screen, c_xy): self.__show_cursor() self.screen = screen - trace(f"Writing {self.screen} {cx} {cy}") - self.flushoutput() + trace("Done and moving") self.move_cursor(cx, cy) + def scroll(self, top: int, bottom: int, left: int | None = None, right: int | None = None): + scroll_rect = SMALL_RECT() + scroll_rect.Top = SHORT(top) + scroll_rect.Bottom = SHORT(bottom) + scroll_rect.Left = SHORT(0 if left is None else left) + scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1 if right is None else right) + trace(f"Scrolling {scroll_rect.Top} {scroll_rect.Bottom}") + destination_origin = _COORD() + fill_info = CHAR_INFO() + fill_info.UnicodeChar = ' ' + + if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): + raise ctypes.WinError(ctypes.GetLastError()) + def __hide_cursor(self): info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): @@ -338,7 +344,7 @@ def screen_xy(self): raise ctypes.WinError(ctypes.GetLastError()) return info.dwCursorPosition.X, info.dwCursorPosition.Y - def __write_changed_line(self, y, oldline, newline, px_coord): + def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): # this is frustrating; there's no reason to test (say) # self.dch1 inside the loop -- but alternative ways of # structuring this function are equally painful (I'm trying to @@ -376,7 +382,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): ins_x, ins_y = self.get_abs_position(x_coord + 1, y) ins_x -= 1 scroll_rect = SMALL_RECT() - scroll_rect.Top = scroll_rect.Bottom = ins_y + scroll_rect.Top = scroll_rect.Bottom = SHORT(ins_y) scroll_rect.Left = ins_x scroll_rect.Right = self.getheightwidth()[1] - 1 destination_origin = _COORD(X = scroll_rect.Left + 1, Y = scroll_rect.Top) @@ -385,14 +391,15 @@ def __write_changed_line(self, y, oldline, newline, px_coord): if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): raise ctypes.WinError(ctypes.GetLastError()) - self.__move(x_coord, y) + + self.__move_relative(x_coord, y) self.__write(newline[x_pos]) self.__posxy = x_coord + character_width, y else: trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() - self.__move(x_coord, y) + self.__move_relative(x_coord, y) if wlen(oldline) > wlen(newline): self.erase_to_end() #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) @@ -421,25 +428,16 @@ def prepare(self) -> None: self.screen = [] self.height, self.width = self.getheightwidth() - #self.__buffer = [] - info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) self.__posxy = 0, 0 self.__gone_tall = 0 - self.__move = self.__move_relative self.__offset = 0 def restore(self) -> None: ... - def __move_relative(self, x, y): - trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) - cur_x, cur_y = self.get_abs_position(x, y) - trace('move is {} {}', cur_x, cur_y) - self.__move_absolute(cur_x, cur_y) - def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_x, cur_y = self.screen_xy dx = x - self.__posxy[0] @@ -448,7 +446,16 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_y += dy return cur_x, cur_y + def __move_relative(self, x, y): + trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) + cur_x, cur_y = self.get_abs_position(x, y) + trace('move is {} {}', cur_x, cur_y) + self.__move_absolute(cur_x, cur_y) + def __move_absolute(self, x, y): + """Moves to an absolute location in the screen buffer""" + if y < 0: + trace(f"Negative offset: {self.__posxy} {self.screen_xy}") cord = _COORD() cord.X = x cord.Y = y @@ -456,14 +463,13 @@ def __move_absolute(self, x, y): raise ctypes.WinError(ctypes.GetLastError()) def move_cursor(self, x: int, y: int) -> None: - trace(f'move to {x} {y}') + trace(f'move_cursor {x} {y}') if x < 0 or y < 0: raise ValueError(f"Bad cussor position {x}, {y}") - self.__move(x, y) + self.__move_relative(x, y) self.__posxy = x, y - self.flushoutput() def set_cursor_vis(self, visible: bool) -> None: if visible: @@ -547,12 +553,14 @@ def finish(self) -> None: y = len(self.screen) - 1 while y >= 0 and not self.screen[y]: y -= 1 - self.__move(0, min(y, self.height + self.__offset - 1)) + self.__move_relative(0, min(y, self.height + self.__offset - 1)) self.__write("\r\n") def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some - buffering going on somewhere).""" + buffering going on somewhere). + + All output on Windows is unbuffered so this is a nop""" pass def forgetinput(self) -> None: From bdff535c83117d1411497702d1dae90744b0af7b Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Tue, 21 May 2024 04:26:29 +1000 Subject: [PATCH 14/66] fix pager typo and refactor some unused branches (#41) --- Lib/_pyrepl/pager.py | 2 +- Lib/_pyrepl/reader.py | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Lib/_pyrepl/pager.py b/Lib/_pyrepl/pager.py index af0409c4523bc2..1c8c1bb13925e3 100644 --- a/Lib/_pyrepl/pager.py +++ b/Lib/_pyrepl/pager.py @@ -35,7 +35,7 @@ def get_pager() -> Pager: if os.environ.get('TERM') in ('dumb', 'emacs'): return plain_pager if sys.platform == 'win32': - return lambda text, title='': tempfilepager(plain(text), 'more <') + return lambda text, title='': tempfile_pager(plain(text), 'more <') if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: return lambda text, title='': pipe_pager(text, 'less', title) diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index d15a150180811d..792b88aa5c44fd 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -407,14 +407,13 @@ def get_arg(self, default: int = 1) -> int: """ if self.arg is None: return default - else: - return self.arg + return self.arg def get_prompt(self, lineno: int, cursor_on_line: bool) -> str: """Return what should be in the left-hand margin for line `lineno'.""" if self.arg is not None and cursor_on_line: - prompt = "(arg: %s) " % self.arg + prompt = f"(arg: {self.arg}) " elif self.paste_mode: prompt = "(paste) " elif "\n" in self.buffer: @@ -480,12 +479,12 @@ def pos2xy(self) -> tuple[int, int]: offset = l - 1 if in_wrapped_line else l # need to remove backslash if offset >= pos: break + + if p + sum(l2) >= self.console.width: + pos -= l - 1 # -1 cause backslash is not in buffer else: - if p + sum(l2) >= self.console.width: - pos -= l - 1 # -1 cause backslash is not in buffer - else: - pos -= l + 1 # +1 cause newline is in buffer - y += 1 + pos -= l + 1 # +1 cause newline is in buffer + y += 1 return p + sum(l2[:pos]), y def insert(self, text: str | list[str]) -> None: @@ -543,7 +542,6 @@ def suspend(self) -> SimpleContextManager: for arg in ("msg", "ps1", "ps2", "ps3", "ps4", "paste_mode"): setattr(self, arg, prev_state[arg]) self.prepare() - pass def finish(self) -> None: """Called when a command signals that we're finished.""" From 88d64f520eef8152fdaeb50c2d3f61f19282f408 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Tue, 21 May 2024 05:17:15 +1000 Subject: [PATCH 15/66] Implement screen clear for Windows (#42) --- Lib/_pyrepl/windows_console.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 85aef5253dda67..24cf536dd81735 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -86,6 +86,11 @@ class CHAR_INFO(Structure): FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] FillConsoleOutputCharacter.restype = BOOL +FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute +FillConsoleOutputAttribute.use_last_error = True +FillConsoleOutputAttribute.argtypes = [HANDLE, WORD, DWORD, _COORD, POINTER(DWORD)] +FillConsoleOutputAttribute.restype = BOOL + ScrollConsoleScreenBuffer = windll.kernel32.ScrollConsoleScreenBufferW ScrollConsoleScreenBuffer.use_last_error = True ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] @@ -99,7 +104,7 @@ class CHAR_INFO(Structure): class Char(Union): _fields_ = [ ("UnicodeChar",WCHAR), - ("Char", CHAR), + ("Char", CHAR), ] class KeyEvent(ctypes.Structure): @@ -191,7 +196,7 @@ def __init__( term: str = "", encoding: str = "", ): - + SetConsoleMode(OutHandle, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) self.encoding = encoding or sys.getdefaultencoding() @@ -466,12 +471,13 @@ def move_cursor(self, x: int, y: int) -> None: trace(f'move_cursor {x} {y}') if x < 0 or y < 0: - raise ValueError(f"Bad cussor position {x}, {y}") + raise ValueError(f"Bad cursor position {x}, {y}") self.__move_relative(x, y) self.__posxy = x, y - def set_cursor_vis(self, visible: bool) -> None: + + def set_cursor_vis(self, visible: bool) -> None: if visible: self.__show_cursor() else: @@ -483,9 +489,9 @@ def getheightwidth(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - return (info.srWindow.Bottom - info.srWindow.Top + 1, + return (info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1) - + def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false and there is no event pending, otherwise waits for the @@ -546,6 +552,13 @@ def clear(self) -> None: size = info.dwSize.X * info.dwSize.Y if not FillConsoleOutputCharacter(OutHandle, b' ', size, _COORD(), DWORD()): raise ctypes.WinError(ctypes.GetLastError()) + if not FillConsoleOutputAttribute(OutHandle, 0, size, _COORD(), DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + y = info.srWindow.Bottom - info.srWindow.Top + 1 + self.__move_absolute(0, y - info.dwSize.Y) + self.__posxy = 0, 0 + self.screen = [""] + def finish(self) -> None: """Move the cursor to the end of the display and otherwise get From 8a7430622fca3b4a7f50525d2ed7abf83ff70a1a Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 21 May 2024 12:10:28 -0700 Subject: [PATCH 16/66] Fix word wrap not being enabled in Windows Terminal Fix scrolling into input that has scrolled out of the buffer --- Lib/_pyrepl/windows_console.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 24cf536dd81735..deacedda475459 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -30,8 +30,9 @@ from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR, SHORT from ctypes import Structure, POINTER, Union from ctypes import windll +from typing import TYPE_CHECKING -if False: +if TYPE_CHECKING: from typing import IO class CONSOLE_SCREEN_BUFFER_INFO(Structure): @@ -180,6 +181,7 @@ class INPUT_RECORD(Structure): } ENABLE_PROCESSED_OUTPUT = 0x01 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 class _error(Exception): @@ -197,7 +199,7 @@ def __init__( encoding: str = "", ): - SetConsoleMode(OutHandle, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + SetConsoleMode(OutHandle, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) self.encoding = encoding or sys.getdefaultencoding() if isinstance(f_in, int): @@ -211,7 +213,7 @@ def __init__( self.output_fd = f_out.fileno() - def refresh(self, screen, c_xy): + def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ Refresh the console screen. @@ -224,14 +226,12 @@ def refresh(self, screen, c_xy): trace('!!Refresh {}', screen) while len(self.screen) < min(len(screen), self.height): trace("...") - cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) self.__write("\n") self.__posxy = 0, len(self.screen) self.screen.append("") - px, py = self.__posxy old_offset = offset = self.__offset height = self.height @@ -265,7 +265,7 @@ def refresh(self, screen, c_xy): elif offset > 0 and len(screen) < offset + height: trace("Adding extra line") offset = max(len(screen) - height, 0) - #screen.append("") + screen.append("") oldscr = self.screen[old_offset : old_offset + height] newscr = screen[offset : offset + height] @@ -303,7 +303,6 @@ def scroll(self, top: int, bottom: int, left: int | None = None, right: int | No scroll_rect.Bottom = SHORT(bottom) scroll_rect.Left = SHORT(0 if left is None else left) scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1 if right is None else right) - trace(f"Scrolling {scroll_rect.Top} {scroll_rect.Bottom}") destination_origin = _COORD() fill_info = CHAR_INFO() fill_info.UnicodeChar = ' ' @@ -331,7 +330,6 @@ def __show_cursor(self): def __write(self, text): os.write(self.output_fd, text.encode(self.encoding, "replace")) - #self.__buffer.append((text, 0)) def __move(self, x, y): info = CONSOLE_SCREEN_BUFFER_INFO() @@ -452,9 +450,15 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: return cur_x, cur_y def __move_relative(self, x, y): + """Moves relative to the start of the current input line""" trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) trace('move is {} {}', cur_x, cur_y) + if cur_y < 0: + # We're scrolling above the current buffer, we need to refresh + self.__posxy = self.__posxy[0], self.__posxy[1] + cur_y + self.refresh(self.screen, self.__posxy) + cur_y = 0 self.__move_absolute(cur_x, cur_y) def __move_absolute(self, x, y): From 38e9c5874e873c402fc5ff1eb8e05b8e829d19a8 Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 21 May 2024 16:54:21 -0700 Subject: [PATCH 17/66] Fix culmitive errors in wrapping as lines proceed --- Lib/_pyrepl/reader.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 792b88aa5c44fd..1133753f078626 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -279,7 +279,8 @@ def calc_screen(self) -> list[str]: screen.append(prompt + l) screeninfo.append((lp, l2)) else: - for i in range(wrapcount + 1): + i = 0 + while l: prelen = lp if i == 0 else 0 index_to_wrap_before = 0 column = 0 @@ -289,12 +290,17 @@ def calc_screen(self) -> list[str]: index_to_wrap_before += 1 column += character_width pre = prompt if i == 0 else "" - post = "\\" if i != wrapcount else "" - after = [1] if i != wrapcount else [] + if len(l) > index_to_wrap_before: + post = "\\" + after = [1] + else: + post = "" + after = [] screen.append(pre + l[:index_to_wrap_before] + post) screeninfo.append((prelen, l2[:index_to_wrap_before] + after)) l = l[index_to_wrap_before:] l2 = l2[index_to_wrap_before:] + i += 1 self.screeninfo = screeninfo self.cxy = self.pos2xy() if self.msg and self.msg_at_bottom: From 9cddace253f61986f1a8488688fdf4c8c43ef7d4 Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 21 May 2024 20:56:48 -0700 Subject: [PATCH 18/66] Fix issues with inputs longer than a single line --- Lib/_pyrepl/windows_console.py | 49 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index deacedda475459..504a0d4fdbc737 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -225,12 +225,12 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: trace('!!Refresh {}', screen) while len(self.screen) < min(len(screen), self.height): - trace("...") self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) self.__write("\n") self.__posxy = 0, len(self.screen) self.screen.append("") + trace(f"... {self.__posxy} {len(self.screen)} {self.height}") px, py = self.__posxy old_offset = offset = self.__offset @@ -328,18 +328,9 @@ def __show_cursor(self): if not SetConsoleCursorInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - def __write(self, text): + def __write(self, text: str): os.write(self.output_fd, text.encode(self.encoding, "replace")) - def __move(self, x, y): - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - x += info.dwCursorPosition.X - y += info.dwCursorPosition.Y - trace('..', x, y) - self.move_cursor(x, y) - @property def screen_xy(self): info = CONSOLE_SCREEN_BUFFER_INFO() @@ -372,7 +363,7 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): # if we need to insert a single character right after the first detected change if oldline[x_pos:] == newline[x_pos + 1 :]: - trace('insert single') + trace('insert single {} {}', y, self.__posxy) if ( y == self.__posxy[1] and x_coord > self.__posxy[0] @@ -380,14 +371,16 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): ): x_pos = px_pos x_coord = px_coord + character_width = wlen(newline[x_pos]) + # Scroll any text to the right if we're inserting ins_x, ins_y = self.get_abs_position(x_coord + 1, y) ins_x -= 1 scroll_rect = SMALL_RECT() scroll_rect.Top = scroll_rect.Bottom = SHORT(ins_y) - scroll_rect.Left = ins_x - scroll_rect.Right = self.getheightwidth()[1] - 1 + scroll_rect.Left = SHORT(ins_x) + scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1) destination_origin = _COORD(X = scroll_rect.Left + 1, Y = scroll_rect.Top) fill_info = CHAR_INFO() fill_info.UnicodeChar = ' ' @@ -397,20 +390,24 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): self.__move_relative(x_coord, y) self.__write(newline[x_pos]) - self.__posxy = x_coord + character_width, y - + if x_coord + character_width == self.width: + self.move_next_line(y) + else: + self.__posxy = x_coord + character_width, y else: - trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) + trace("Rewrite all {!r} {} {} y={} {} posxy={}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() self.__move_relative(x_coord, y) if wlen(oldline) > wlen(newline): self.erase_to_end() - #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) self.__write(newline[x_pos:]) - self.__posxy = wlen(newline), y + if len(newline[x_pos:]) == self.width: + self.move_next_line(y) + else: + self.__posxy = wlen(newline), y - if "\x1b" in newline: + if "\x1b" in newline or y != self.__posxy[1]: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. @@ -418,6 +415,12 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): self.__move_absolute(0, cur_y) self.__posxy = 0, y + def move_next_line(self, y: int): + self.__posxy = 0, y + _, cur_y = self.get_abs_position(0, y) + self.__move_absolute(0, cur_y) + self.__posxy = 0, y + def erase_to_end(self): info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): @@ -428,6 +431,7 @@ def erase_to_end(self): raise ctypes.WinError(ctypes.GetLastError()) def prepare(self) -> None: + trace("prepare") self.screen = [] self.height, self.width = self.getheightwidth() @@ -450,7 +454,7 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: return cur_x, cur_y def __move_relative(self, x, y): - """Moves relative to the start of the current input line""" + """Moves relative to the current __posxy""" trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) trace('move is {} {}', cur_x, cur_y) @@ -465,6 +469,9 @@ def __move_absolute(self, x, y): """Moves to an absolute location in the screen buffer""" if y < 0: trace(f"Negative offset: {self.__posxy} {self.screen_xy}") + if x < 0: + trace("Negative move {}", self.getheightwidth()) + # return cord = _COORD() cord.X = x cord.Y = y From fc4efeea2e673e79b8b1b0e195da5393240ea528 Mon Sep 17 00:00:00 2001 From: dino Date: Wed, 22 May 2024 23:43:24 -0700 Subject: [PATCH 19/66] Resize WIP --- Lib/_pyrepl/windows_console.py | 114 +++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 28 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 504a0d4fdbc737..bbdbf1c9f3bef9 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -31,6 +31,7 @@ from ctypes import Structure, POINTER, Union from ctypes import windll from typing import TYPE_CHECKING +from .utils import wlen if TYPE_CHECKING: from typing import IO @@ -118,11 +119,17 @@ class KeyEvent(ctypes.Structure): ("dwControlKeyState", DWORD), ] +class WindowsBufferSizeEvent(ctypes.Structure): + _fields_ = [ + ('dwSize', _COORD) + ] + + class ConsoleEvent(ctypes.Union): _fields_ = [ ("KeyEvent", KeyEvent), # ("MouseEvent", ), -# ("WindowsBufferSizeEvent", ), + ("WindowsBufferSizeEvent", WindowsBufferSizeEvent), # ("MenuEvent", ) # ("FocusEvent", ) ] @@ -187,9 +194,6 @@ class INPUT_RECORD(Structure): class _error(Exception): pass -def wlen(s: str) -> int: - return len(s) - class WindowsConsole(Console): def __init__( self, @@ -223,7 +227,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ cx, cy = c_xy - trace('!!Refresh {}', screen) + trace('!!Refresh {} {} {}', c_xy, self.__offset, screen) while len(self.screen) < min(len(screen), self.height): self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) @@ -275,6 +279,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: trace('new offset {} {}', offset, px) self.__offset = offset + self.__hide_cursor() for ( y, oldline, @@ -285,7 +290,6 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: y = len(newscr) while y < len(oldscr): - self.__hide_cursor() self.__move_relative(0, y) self.__posxy = 0, y self.erase_to_end() @@ -389,20 +393,34 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): raise ctypes.WinError(ctypes.GetLastError()) self.__move_relative(x_coord, y) + pos = self.__posxy + coord = self.screen_xy + self.__write(newline[x_pos]) if x_coord + character_width == self.width: - self.move_next_line(y) + + # If we wrapped we need to get back to a known good position, + # and the starting position was known good. + self.__move_absolute(*coord) + self.__posxy = pos else: self.__posxy = x_coord + character_width, y else: trace("Rewrite all {!r} {} {} y={} {} posxy={}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() self.__move_relative(x_coord, y) - if wlen(oldline) > wlen(newline): - self.erase_to_end() + pos = self.__posxy + coord = self.screen_xy + #if wlen(oldline) > wlen(newline): + self.erase_to_end() + trace(f"Writing {newline[x_pos:]}") self.__write(newline[x_pos:]) - if len(newline[x_pos:]) == self.width: - self.move_next_line(y) + + if wlen(newline[x_pos:]) == self.width: + # If we wrapped we need to get back to a known good position, + # and the starting position was known good. + self.__move_absolute(*coord) + self.__posxy = pos else: self.__posxy = wlen(newline), y @@ -411,16 +429,11 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. - _, cur_y = self.get_abs_position(0, y) - self.__move_absolute(0, cur_y) + #_, cur_y = self.get_abs_position(0, y) + #self.__move_absolute(0, cur_y) + self.__move_absolute(0, self.screen_xy[1]) self.__posxy = 0, y - def move_next_line(self, y: int): - self.__posxy = 0, y - _, cur_y = self.get_abs_position(0, y) - self.__move_absolute(0, cur_y) - self.__posxy = 0, y - def erase_to_end(self): info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): @@ -457,7 +470,6 @@ def __move_relative(self, x, y): """Moves relative to the current __posxy""" trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) - trace('move is {} {}', cur_x, cur_y) if cur_y < 0: # We're scrolling above the current buffer, we need to refresh self.__posxy = self.__posxy[0], self.__posxy[1] + cur_y @@ -467,6 +479,7 @@ def __move_relative(self, x, y): def __move_absolute(self, x, y): """Moves to an absolute location in the screen buffer""" + trace(f"move absolute {x} {y}") if y < 0: trace(f"Negative offset: {self.__posxy} {self.screen_xy}") if x < 0: @@ -487,7 +500,6 @@ def move_cursor(self, x: int, y: int) -> None: self.__move_relative(x, y) self.__posxy = x, y - def set_cursor_vis(self, visible: bool) -> None: if visible: self.__show_cursor() @@ -503,21 +515,67 @@ def getheightwidth(self) -> tuple[int, int]: return (info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1) + def __read_input(self) -> INPUT_RECORD | None: + rec = INPUT_RECORD() + read = DWORD() + if not ReadConsoleInput(InHandle, rec, 1, read): + raise ctypes.WinError(ctypes.GetLastError()) + + if read.value == 0: + return None + + return rec + def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false and there is no event pending, otherwise waits for the completion of an event.""" while True: - rec = INPUT_RECORD() - read = DWORD() - if not ReadConsoleInput(InHandle, rec, 1, read): - raise ctypes.WinError(ctypes.GetLastError()) - - if read.value == 0: + rec = self.__read_input() + if rec is None: if block: continue return None + if rec.EventType == WINDOW_BUFFER_SIZE_EVENT: + old_height, old_width = self.height, self.width + self.height, self.width = self.getheightwidth() + delta = self.width - old_width + # Windows will fix up the wrapping for us, but we + # need to sync __posxy with those changes. + + new_x, new_y = self.__posxy + y = self.__posxy[1] + trace("Cur screen {}", self.screen) + #last_len = -1 + new_lines = 0 + while y >= 0: + line = self.screen[y] + line_len = wlen(line) + trace(f"XX {wlen(line)} {self.width} {old_width} {wlen(line) <= self.width} {old_width > wlen(line)} {line}") + if (line_len >= self.width and line_len < old_width) and line[-1] != "\\": + # This line is turning into 2 lines + trace("Lines wrap") + new_y += 1 + new_lines += 1 + #elif line_len >= old_width and line_len < self.width and line[-1] == "\\" and last_len == 1: + # # This line is turning into 1 line + # trace("Lines join") + # #new_y -= 1 + #last_len = line_len + y -= 1 + + trace(f"RESIZE {self.screen_xy} {self.__posxy} ({new_x}, {new_y}) ({self.width}, {self.height})") + # Force redraw of current input, the wrapping can corrupt our state with \ + self.screen = [' ' * self.width] * (len(self.screen) + new_lines) + + # We could have "unwrapped" things which weren't really wrapped, shifting our x position, + # get back to something neutral. + self.__move_absolute(0, self.screen_xy[1]) + self.__posxy = 0, new_y + + return Event("resize", "") + if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: # Only process keys and keydown events if block: @@ -566,7 +624,7 @@ def clear(self) -> None: if not FillConsoleOutputAttribute(OutHandle, 0, size, _COORD(), DWORD()): raise ctypes.WinError(ctypes.GetLastError()) y = info.srWindow.Bottom - info.srWindow.Top + 1 - self.__move_absolute(0, y - info.dwSize.Y) + self.__move_absolute(0, 0) self.__posxy = 0, 0 self.screen = [""] From 564e6e16fed1cfab54766cd558828ab6405f7ea6 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Fri, 24 May 2024 09:58:17 -0700 Subject: [PATCH 20/66] set compat based on Windows build version --- Lib/_pyrepl/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/__main__.py b/Lib/_pyrepl/__main__.py index 784dc16ae00f79..b81dd2c4f84ff0 100644 --- a/Lib/_pyrepl/__main__.py +++ b/Lib/_pyrepl/__main__.py @@ -1,7 +1,11 @@ import os import sys -CAN_USE_PYREPL = True #sys.platform != "win32" +CAN_USE_PYREPL: bool +if sys.platform != "win32": + CAN_USE_PYREPL = True +else: + CAN_USE_PYREPL = sys.getwindowsversion().build >= 10586 # Windows 10 TH2 def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): From 3ae4316f94a537d1523e690cbfa57e73d7822bb2 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Fri, 24 May 2024 09:58:45 -0700 Subject: [PATCH 21/66] use escape sequence for clearing screen and setting cursor position --- Lib/_pyrepl/windows_console.py | 82 +++++++++++++--------------------- 1 file changed, 32 insertions(+), 50 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index bbdbf1c9f3bef9..08d629c79c5d15 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -22,8 +22,6 @@ import os import sys -from abc import ABC, abstractmethod -from dataclasses import dataclass, field from _pyrepl.console import Event, Console from .trace import trace import ctypes @@ -103,12 +101,14 @@ class CHAR_INFO(Structure): SetConsoleMode.argtypes = [HANDLE, DWORD] SetConsoleMode.restype = BOOL + class Char(Union): _fields_ = [ ("UnicodeChar",WCHAR), ("Char", CHAR), ] + class KeyEvent(ctypes.Structure): _fields_ = [ ("bKeyDown", BOOL), @@ -119,6 +119,7 @@ class KeyEvent(ctypes.Structure): ("dwControlKeyState", DWORD), ] + class WindowsBufferSizeEvent(ctypes.Structure): _fields_ = [ ('dwSize', _COORD) @@ -152,8 +153,6 @@ class INPUT_RECORD(Structure): ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] ReadConsoleInput.restype = BOOL - - OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) InHandle = GetStdHandle(STD_INPUT_HANDLE) @@ -191,9 +190,11 @@ class INPUT_RECORD(Structure): ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 + class _error(Exception): pass + class WindowsConsole(Console): def __init__( self, @@ -216,7 +217,6 @@ def __init__( else: self.output_fd = f_out.fileno() - def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ Refresh the console screen. @@ -255,17 +255,17 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - + bottom = info.srWindow.Bottom trace("Scrolling {} {} {} {} {}", scroll_lines, info.srWindow.Bottom, self.height, len(screen) - self.height, self.__posxy) self.scroll(scroll_lines, bottom) self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines self.__offset += scroll_lines - + for i in range(scroll_lines): self.screen.append("") - + elif offset > 0 and len(screen) < offset + height: trace("Adding extra line") offset = max(len(screen) - height, 0) @@ -315,22 +315,16 @@ def scroll(self, top: int, bottom: int, left: int | None = None, right: int | No raise ctypes.WinError(ctypes.GetLastError()) def __hide_cursor(self): - info = CONSOLE_CURSOR_INFO() - if not GetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - - info.bVisible = False - if not SetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + self.__write("\x1b[?25l") def __show_cursor(self): - info = CONSOLE_CURSOR_INFO() - if not GetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + self.__write("\x1b[?25h") - info.bVisible = True - if not SetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + def __enable_blinking(self): + self.__write("\x1b[?12h") + + def __disable_blinking(self): + self.__write("\x1b[?12l") def __write(self, text: str): os.write(self.output_fd, text.encode(self.encoding, "replace")) @@ -484,12 +478,7 @@ def __move_absolute(self, x, y): trace(f"Negative offset: {self.__posxy} {self.screen_xy}") if x < 0: trace("Negative move {}", self.getheightwidth()) - # return - cord = _COORD() - cord.X = x - cord.Y = y - if not SetConsoleCursorPosition(OutHandle, cord): - raise ctypes.WinError(ctypes.GetLastError()) + self.__write("\x1b[{};{}H".format(y + 1, x + 1)) def move_cursor(self, x: int, y: int) -> None: trace(f'move_cursor {x} {y}') @@ -520,10 +509,10 @@ def __read_input(self) -> INPUT_RECORD | None: read = DWORD() if not ReadConsoleInput(InHandle, rec, 1, read): raise ctypes.WinError(ctypes.GetLastError()) - + if read.value == 0: return None - + return rec def get_event(self, block: bool = True) -> Event | None: @@ -536,14 +525,14 @@ def get_event(self, block: bool = True) -> Event | None: if block: continue return None - + if rec.EventType == WINDOW_BUFFER_SIZE_EVENT: old_height, old_width = self.height, self.width self.height, self.width = self.getheightwidth() delta = self.width - old_width # Windows will fix up the wrapping for us, but we # need to sync __posxy with those changes. - + new_x, new_y = self.__posxy y = self.__posxy[1] trace("Cur screen {}", self.screen) @@ -581,9 +570,9 @@ def get_event(self, block: bool = True) -> Event | None: if block: continue return None - + key = rec.Event.KeyEvent.uChar.UnicodeChar - + if rec.Event.KeyEvent.uChar.UnicodeChar == '\r': # Make enter make unix-like return Event(evt="key", data="\n", raw="\n") @@ -597,7 +586,7 @@ def get_event(self, block: bool = True) -> Event | None: # Handle special keys like arrow keys and translate them into the appropriate command code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: - return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar) + return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar) if block: continue @@ -609,26 +598,19 @@ def push_char(self, char: int | bytes) -> None: """ Push a character to the console event queue. """ - trace(f'put_char {char}') + ... def beep(self) -> None: ... - def clear(self) -> None: + def clear(self, clear_scrollback=False) -> None: """Wipe the screen""" - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - size = info.dwSize.X * info.dwSize.Y - if not FillConsoleOutputCharacter(OutHandle, b' ', size, _COORD(), DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) - if not FillConsoleOutputAttribute(OutHandle, 0, size, _COORD(), DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) - y = info.srWindow.Bottom - info.srWindow.Top + 1 - self.__move_absolute(0, 0) + self.__write("\x1b[2J") + if clear_scrollback: + self.__write("\x1b[3J") + self.__write("\x1b[H") self.__posxy = 0, 0 self.screen = [""] - def finish(self) -> None: """Move the cursor to the end of the display and otherwise get ready for end. XXX could be merged with restore? Hmm.""" @@ -641,10 +623,10 @@ def finish(self) -> None: def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some buffering going on somewhere). - + All output on Windows is unbuffered so this is a nop""" - pass - + ... + def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" ... From 5bb02a23e9ca973ce0f2648423a1cf85db93d07f Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Fri, 24 May 2024 10:51:58 -0700 Subject: [PATCH 22/66] Remove unused imports --- Lib/_pyrepl/windows_console.py | 38 +++++++++++++--------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 08d629c79c5d15..78d5d38a2c5207 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -66,21 +66,6 @@ class CHAR_INFO(Structure): GetConsoleScreenBufferInfo.argtypes = [HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] GetConsoleScreenBufferInfo.restype = BOOL -SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo -SetConsoleCursorInfo.use_last_error = True -SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -SetConsoleCursorInfo.restype = BOOL - -GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo -GetConsoleCursorInfo.use_last_error = True -GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -GetConsoleCursorInfo.restype = BOOL - -SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition -SetConsoleCursorPosition.argtypes = [HANDLE, _COORD] -SetConsoleCursorPosition.restype = BOOL -SetConsoleCursorPosition.use_last_error = True - FillConsoleOutputCharacter = windll.kernel32.FillConsoleOutputCharacterW FillConsoleOutputCharacter.use_last_error = True FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] @@ -263,7 +248,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines self.__offset += scroll_lines - for i in range(scroll_lines): + for _ in range(scroll_lines): self.screen.append("") elif offset > 0 and len(screen) < offset + height: @@ -330,7 +315,7 @@ def __write(self, text: str): os.write(self.output_fd, text.encode(self.encoding, "replace")) @property - def screen_xy(self): + def screen_xy(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) @@ -438,16 +423,14 @@ def erase_to_end(self): raise ctypes.WinError(ctypes.GetLastError()) def prepare(self) -> None: + """ + Prepare the console for input/output operations. + """ trace("prepare") self.screen = [] self.height, self.width = self.getheightwidth() - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - self.__posxy = 0, 0 - self.__gone_tall = 0 self.__offset = 0 def restore(self) -> None: ... @@ -460,7 +443,7 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_y += dy return cur_x, cur_y - def __move_relative(self, x, y): + def __move_relative(self, x: int, y: int) -> None: """Moves relative to the current __posxy""" trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) @@ -471,7 +454,7 @@ def __move_relative(self, x, y): cur_y = 0 self.__move_absolute(cur_x, cur_y) - def __move_absolute(self, x, y): + def __move_absolute(self, x: int, y: int) -> None: """Moves to an absolute location in the screen buffer""" trace(f"move absolute {x} {y}") if y < 0: @@ -481,6 +464,13 @@ def __move_absolute(self, x, y): self.__write("\x1b[{};{}H".format(y + 1, x + 1)) def move_cursor(self, x: int, y: int) -> None: + """ + Move the cursor to the specified position on the screen. + + Parameters: + - x (int): X coordinate. + - y (int): Y coordinate. + """ trace(f'move_cursor {x} {y}') if x < 0 or y < 0: From 500761e2aba359f75729205223a61054fcfe0d8b Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 16 May 2024 18:58:54 -0700 Subject: [PATCH 23/66] Windows repl support --- Lib/_pyrepl/__main__.py | 4 +- Lib/_pyrepl/readline.py | 7 +- Lib/_pyrepl/simple_interact.py | 12 +- Lib/_pyrepl/windows_console.py | 501 +++++++++++++++++++++++++++++++++ 4 files changed, 518 insertions(+), 6 deletions(-) create mode 100644 Lib/_pyrepl/windows_console.py diff --git a/Lib/_pyrepl/__main__.py b/Lib/_pyrepl/__main__.py index c598019e7cd4ad..784dc16ae00f79 100644 --- a/Lib/_pyrepl/__main__.py +++ b/Lib/_pyrepl/__main__.py @@ -1,7 +1,7 @@ import os import sys -CAN_USE_PYREPL = sys.platform != "win32" +CAN_USE_PYREPL = True #sys.platform != "win32" def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): @@ -36,6 +36,8 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): except Exception as e: from .trace import trace msg = f"warning: can't use pyrepl: {e}" + import traceback + traceback.print_exc() trace(msg) print(msg, file=sys.stderr) CAN_USE_PYREPL = False diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index ffa14a9ce31a8f..c7430be443b9bf 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -38,7 +38,10 @@ from . import commands, historical_reader from .completing_reader import CompletingReader -from .unix_console import UnixConsole, _error +try: + from .unix_console import UnixConsole as Console, _error +except: + from .windows_console import WindowsConsole as Console, _error ENCODING = sys.getdefaultencoding() or "latin1" @@ -328,7 +331,7 @@ def __post_init__(self) -> None: def get_reader(self) -> ReadlineAlikeReader: if self.reader is None: - console = UnixConsole(self.f_in, self.f_out, encoding=ENCODING) + console = Console(self.f_in, self.f_out, encoding=ENCODING) self.reader = ReadlineAlikeReader(console=console, config=self.config) return self.reader diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 8ab4dab757685e..becdc119b6354e 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -34,8 +34,11 @@ from types import ModuleType from .readline import _get_reader, multiline_input -from .unix_console import _error +try: + from .unix_console import _error +except ModuleNotFoundError: + _error = OSError def check() -> str: """Returns the error message if there is a problem initializing the state.""" @@ -111,8 +114,11 @@ def run_multiline_interactive_console( mainmodule: ModuleType | None= None, future_flags: int = 0 ) -> None: import __main__ - from .readline import _setup - _setup() + try: + from .readline import _setup + _setup() + except ImportError: + pass mainmodule = mainmodule or __main__ console = InteractiveColoredConsole(mainmodule.__dict__, filename="") diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py new file mode 100644 index 00000000000000..a56f7dc544a244 --- /dev/null +++ b/Lib/_pyrepl/windows_console.py @@ -0,0 +1,501 @@ +# Copyright 2000-2004 Michael Hudson-Doyle +# +# All Rights Reserved +# +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose is hereby granted without fee, +# provided that the above copyright notice appear in all copies and +# that both that copyright notice and this permission notice appear in +# supporting documentation. +# +# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO +# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, +# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from _pyrepl.console import Event, Console +import ctypes +from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR +from ctypes import Structure, POINTER, Union +from ctypes import windll +import os +if False: + from typing import IO + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [ + ('dwSize', _COORD), + ('dwCursorPosition', _COORD), + ('wAttributes', WORD), + ('srWindow', SMALL_RECT), + ('dwMaximumWindowSize', _COORD), + ] + +class CONSOLE_CURSOR_INFO(Structure): + _fields_ = [ + ('dwSize', DWORD), + ('bVisible', BOOL), + ] + +STD_INPUT_HANDLE = -10 +STD_OUTPUT_HANDLE = -11 +GetStdHandle = windll.kernel32.GetStdHandle +GetStdHandle.argtypes = [ctypes.wintypes.DWORD] +GetStdHandle.restype = ctypes.wintypes.HANDLE + +GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo +GetConsoleScreenBufferInfo.use_last_error = True +GetConsoleScreenBufferInfo.argtypes = [HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] +GetConsoleScreenBufferInfo.restype = BOOL + +SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo +SetConsoleCursorInfo.use_last_error = True +SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +SetConsoleCursorInfo.restype = BOOL + +GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo +GetConsoleCursorInfo.use_last_error = True +GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +GetConsoleCursorInfo.restype = BOOL + +SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition +SetConsoleCursorPosition.argtypes = [HANDLE, _COORD] +SetConsoleCursorPosition.restype = BOOL +SetConsoleCursorPosition.use_last_error = True + +FillConsoleOutputCharacter = windll.kernel32.FillConsoleOutputCharacterW +FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] +FillConsoleOutputCharacter.restype = BOOL + +LOG = open('out.txt', 'w+') + +def log(*args): + LOG.write(" ".join((str(x) for x in args)) + "\n") + LOG.flush() + +class Char(Union): + _fields_ = [ + ("UnicodeChar",WCHAR), + ("Char", CHAR), + ] + +class KeyEvent(ctypes.Structure): + _fields_ = [ + ("bKeyDown", BOOL), + ("wRepeatCount", WORD), + ("wVirtualeyCode", WORD), + ("wVirtualScanCode", WORD), + ("uChar", Char), + ("dwControlKeyState", DWORD), + ] + +class ConsoleEvent(ctypes.Union): + _fields_ = [ + ("KeyEvent", KeyEvent), +# ("MouseEvent", ), +# ("WindowsBufferSizeEvent", ), +# ("MenuEvent", ) +# ("FocusEvent", ) + ] + + +KEY_EVENT = 0x01 +FOCUS_EVENT = 0x10 +MENU_EVENT = 0x08 +MOUSE_EVENT = 0x02 +WINDOW_BUFFER_SIZE_EVENT = 0x04 + +class INPUT_RECORD(Structure): + _fields_ = [ + ("EventType", WORD), + ("Event", ConsoleEvent) + ] + +ReadConsoleInput = windll.kernel32.ReadConsoleInputW +ReadConsoleInput.use_last__error = True +ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] +ReadConsoleInput.restype = BOOL + + + +OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) +InHandle = GetStdHandle(STD_INPUT_HANDLE) + +class _error(Exception): + pass + +def wlen(s: str) -> int: + return len(s) + +class WindowsConsole(Console): + def __init__( + self, + f_in: IO[bytes] | int = 0, + f_out: IO[bytes] | int = 1, + term: str = "", + encoding: str = "", + ): + self.encoding = encoding or sys.getdefaultencoding() + + if isinstance(f_in, int): + self.input_fd = f_in + else: + self.input_fd = f_in.fileno() + + if isinstance(f_out, int): + self.output_fd = f_out + else: + self.output_fd = f_out.fileno() + + + def refresh(self, screen, c_xy): + """ + Refresh the console screen. + + Parameters: + - screen (list): List of strings representing the screen contents. + - c_xy (tuple): Cursor position (x, y) on the screen. + """ + cx, cy = c_xy + log('Refresh', c_xy, self.screen_xy, self.__posxy) + if not self.__gone_tall: + while len(self.screen) < min(len(screen), self.height): + log('extend') + self.__hide_cursor() + self.__move(0, len(self.screen) - 1) + self.__write("\n") + self.__posxy = 0, len(self.screen) + self.screen.append("") + else: + while len(self.screen) < len(screen): + self.screen.append("") + + if len(screen) > self.height: + self.__gone_tall = 1 + self.__move = self.__move_absolute + + px, py = self.__posxy + old_offset = offset = self.__offset + height = self.height + + # we make sure the cursor is on the screen, and that we're + # using all of the screen if we can + if cy < offset: + offset = cy + elif cy >= offset + height: + offset = cy - height + 1 + elif offset > 0 and len(screen) < offset + height: + offset = max(len(screen) - height, 0) + screen.append("") + + oldscr = self.screen[old_offset : old_offset + height] + newscr = screen[offset : offset + height] + + # use hardware scrolling if we have it. + log('offsets', old_offset, offset) + if old_offset > offset: + log('old_offset > offset') + elif old_offset < offset: + log('old_offset < offset') + if False: + if old_offset > offset and self._ri: + self.__hide_cursor() + self.__write_code(self._cup, 0, 0) + self.__posxy = 0, old_offset + for i in range(old_offset - offset): + self.__write_code(self._ri) + oldscr.pop(-1) + oldscr.insert(0, "") + elif old_offset < offset and self._ind: + self.__hide_cursor() + self.__write_code(self._cup, self.height - 1, 0) + self.__posxy = 0, old_offset + self.height - 1 + for i in range(offset - old_offset): + self.__write_code(self._ind) + oldscr.pop(0) + oldscr.append("") + + log('new offset', offset, px) + self.__offset = offset + + for ( + y, + oldline, + newline, + ) in zip(range(offset, offset + height), oldscr, newscr): + if oldline != newline: + self.__write_changed_line(y, oldline, newline, px) + + y = len(newscr) + while y < len(oldscr): + log('need to erase', y) + self.__hide_cursor() + self.__move(0, y) + self.__posxy = 0, y +# self.__write_code(self._el) + y += 1 + + self.__show_cursor() + + self.screen = screen + log(f"Writing {self.screen} {cx} {cy}") + #self.move_cursor(cx, cy) + self.flushoutput() + + def __hide_cursor(self): + info = CONSOLE_CURSOR_INFO() + if not GetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + info.bVisible = False + if not SetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + def __show_cursor(self): + info = CONSOLE_CURSOR_INFO() + if not GetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + info.bVisible = True + if not SetConsoleCursorInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + def __write(self, text): + self.__buffer.append((text, 0)) + + def __move(self, x, y): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + x += info.dwCursorPosition.X + y += info.dwCursorPosition.Y + log('..', x, y) + self.move_cursor(x, y) + + @property + def screen_xy(self): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + return info.dwCursorPosition.X, info.dwCursorPosition.Y + + def __write_changed_line(self, y, oldline, newline, px_coord): + # this is frustrating; there's no reason to test (say) + # self.dch1 inside the loop -- but alternative ways of + # structuring this function are equally painful (I'm trying to + # avoid writing code generators these days...) + minlen = min(wlen(oldline), wlen(newline)) + x_pos = 0 + x_coord = 0 + + px_pos = 0 + j = 0 + for c in oldline: + if j >= px_coord: break + j += wlen(c) + px_pos += 1 + + # reuse the oldline as much as possible, but stop as soon as we + # encounter an ESCAPE, because it might be the start of an escape + # sequene + while x_coord < minlen and oldline[x_pos] == newline[x_pos] and newline[x_pos] != "\x1b": + x_coord += wlen(newline[x_pos]) + x_pos += 1 + + # if we need to insert a single character right after the first detected change + if oldline[x_pos:] == newline[x_pos + 1 :]: # and self.ich1: + if ( + y == self.__posxy[1] + and x_coord > self.__posxy[0] + and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1] + ): + x_pos = px_pos + x_coord = px_coord + character_width = wlen(newline[x_pos]) + log('sinle char', x_coord, y, px_coord) + self.__move(x_coord, y) +# self.__write_code(self.ich1) + self.__write(newline[x_pos]) + self.__posxy = x_coord + character_width, y + + # if it's a single character change in the middle of the line + elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]): + character_width = wlen(newline[x_pos]) + self.__move(x_coord, y) + self.__write(newline[x_pos]) + self.__posxy = x_coord + character_width, y + + # if this is the last character to fit in the line and we edit in the middle of the line + elif ( +# self.dch1 +# and self.ich1 and + wlen(newline) == self.width + and x_coord < wlen(newline) - 2 + and newline[x_pos + 1 : -1] == oldline[x_pos:-2] + ): + self.__hide_cursor() + self.__move(self.width - 2, y) + self.__posxy = self.width - 2, y +# self.__write_code(self.dch1) + + character_width = wlen(newline[x_pos]) + self.__move(x_coord, y) +# self.__write_code(self.ich1) + self.__write(newline[x_pos]) + self.__posxy = character_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 + + if "\x1b" in newline: + # ANSI escape characters are present, so we can't assume + # anything about the position of the cursor. Moving the cursor + # to the left margin should work to get to a known position. + self.move_cursor(0, y) + + def prepare(self) -> None: + self.screen = [] + self.height, self.width = self.getheightwidth() + + self.__buffer = [] + + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + self.__posxy = 0, 0 #info.dwCursorPosition.X, info.dwCursorPosition.Y + self.__gone_tall = 0 + self.__move = self.__move_relative + self.__offset = 0 + + def restore(self) -> None: ... + + def __move_relative(self, x, y): + log('move relative', x, y) + cur_x, cur_y = self.screen_xy + dx = x - self.__posxy[0] + dy = y - self.__posxy[1] + cur_x += dx + cur_y += dy + log('move is', cur_x, cur_y) + self.__move_absolute(cur_x, cur_y) + + def __move_absolute(self, x, y): + assert 0 <= y - self.__offset < self.height, y - self.__offset + cord = _COORD() + cord.X = x + cord.Y = y + if not SetConsoleCursorPosition(OutHandle, cord): + raise ctypes.WinError(ctypes.GetLastError()) + + def move_cursor(self, x: int, y: int) -> None: + log(f'move to {x} {y}') + + if x < 0 or y < 0: + raise ValueError(f"Bad cussor position {x}, {y}") + + self.__move(x, y) + self.__posxy = x, y + self.flushoutput() + + + def set_cursor_vis(self, visible: bool) -> None: + if visible: + self.__show_cursor() + else: + self.__hide_cursor() + + def getheightwidth(self) -> tuple[int, int]: + """Return (height, width) where height and width are the height + and width of the terminal window in characters.""" + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + return (info.srWindow.Bottom - info.srWindow.Top + 1, + info.srWindow.Right - info.srWindow.Left + 1) + + def get_event(self, block: bool = True) -> Event | None: + """Return an Event instance. Returns None if |block| is false + and there is no event pending, otherwise waits for the + completion of an event.""" + rec = INPUT_RECORD() + read = DWORD() + while True: + if not ReadConsoleInput(InHandle, rec, 1, read): + raise ctypes.WinError(ctypes.GetLastError()) + if read.value == 0 or rec.Event.KeyEvent.uChar.Char == b'\x00': + if block: + continue + return None + if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: + return False + key = chr(rec.Event.KeyEvent.uChar.Char[0]) + # self.push_char(key) + if rec.Event.KeyEvent.uChar.Char == b'\r': + return Event(evt="key", data="\n", raw="\n") + log('virtual key code', rec.Event.KeyEvent.wVirtualeyCode, rec.Event.KeyEvent.uChar.Char) + if rec.Event.KeyEvent.wVirtualeyCode == 8: + return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) + #print(key, rec.Event.KeyEvent.wVirtualeyCode) + return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.Char) + + def push_char(self, char: int | bytes) -> None: + """ + Push a character to the console event queue. + """ + log(f'put_char {char}') + + def beep(self) -> None: ... + + def clear(self) -> None: + """Wipe the screen""" + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + size = info.dwSize.X * info.dwSize.Y + if not FillConsoleOutputCharacter(OutHandle, b' ', size, _COORD(), DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + + def finish(self) -> None: + """Move the cursor to the end of the display and otherwise get + ready for end. XXX could be merged with restore? Hmm.""" + ... + + def flushoutput(self) -> None: + """Flush all output to the screen (assuming there's some + buffering going on somewhere).""" + for text, iscode in self.__buffer: + if iscode: + self.__tputs(text) + else: + os.write(self.output_fd, text.encode(self.encoding, "replace")) + del self.__buffer[:] + + def forgetinput(self) -> None: + """Forget all pending, but not yet processed input.""" + ... + + def getpending(self) -> Event: + """Return the characters that have been typed but not yet + processed.""" + ... + + def wait(self) -> None: + """Wait for an event.""" + ... + + def repaint(self) -> None: + log('repaint') From 98f16b9a8f70c3e57d00220b61aa7e390822bf7d Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 11:04:53 -0700 Subject: [PATCH 24/66] Arrow key support --- Lib/_pyrepl/windows_console.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index a56f7dc544a244..d7613b41d76b3c 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -91,7 +91,7 @@ class KeyEvent(ctypes.Structure): _fields_ = [ ("bKeyDown", BOOL), ("wRepeatCount", WORD), - ("wVirtualeyCode", WORD), + ("wVirtualKeyCode", WORD), ("wVirtualScanCode", WORD), ("uChar", Char), ("dwControlKeyState", DWORD), @@ -129,6 +129,15 @@ class INPUT_RECORD(Structure): OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) InHandle = GetStdHandle(STD_INPUT_HANDLE) +VK_MAP: dict[int, str] = { + 0x23: "end", # VK_END + 0x24: "home", # VK_HOME + 0x25: "left", # VK_LEFT + 0x26: "up", # VK_UP + 0x27: "right", # VK_RIGHT + 0x28: "down", # VK_DOWN +} + class _error(Exception): pass @@ -436,7 +445,7 @@ def get_event(self, block: bool = True) -> Event | None: while True: if not ReadConsoleInput(InHandle, rec, 1, read): raise ctypes.WinError(ctypes.GetLastError()) - if read.value == 0 or rec.Event.KeyEvent.uChar.Char == b'\x00': + if read.value == 0: if block: continue return None @@ -446,10 +455,15 @@ def get_event(self, block: bool = True) -> Event | None: # self.push_char(key) if rec.Event.KeyEvent.uChar.Char == b'\r': return Event(evt="key", data="\n", raw="\n") - log('virtual key code', rec.Event.KeyEvent.wVirtualeyCode, rec.Event.KeyEvent.uChar.Char) - if rec.Event.KeyEvent.wVirtualeyCode == 8: + log('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) + if rec.Event.KeyEvent.wVirtualKeyCode == 8: return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) - #print(key, rec.Event.KeyEvent.wVirtualeyCode) + if rec.Event.KeyEvent.uChar.Char == b'\x00': + code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) + if code: + return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.Char) + continue + #print(key, rec.Event.KeyEvent.wVirtualKeyCode) return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.Char) def push_char(self, char: int | bytes) -> None: From 911a76a5a64a17e9e8f825d9cb155cfe631c7393 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 12:10:43 -0700 Subject: [PATCH 25/66] Make backspace clear char --- Lib/_pyrepl/windows_console.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index d7613b41d76b3c..bfb307a7f08ab7 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -362,10 +362,18 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = character_width + 1, y else: + log("Rewrite all", x_coord, len(oldline)) self.__hide_cursor() self.__move(x_coord, y) -# if wlen(oldline) > wlen(newline): -# self.__write_code(self._el) + if wlen(oldline) > wlen(newline): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X + if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + self.__write(newline[x_pos:]) self.__posxy = wlen(newline), y From 77f1c42a930076aa7d349ddd3786f23dac1344d0 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 14:41:46 -0700 Subject: [PATCH 26/66] Fix missing newline after input --- Lib/_pyrepl/windows_console.py | 71 ++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index bfb307a7f08ab7..88c2ee944df485 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -22,6 +22,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field from _pyrepl.console import Event, Console +from .trace import trace import ctypes from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR from ctypes import Structure, POINTER, Union @@ -75,12 +76,6 @@ class CONSOLE_CURSOR_INFO(Structure): FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] FillConsoleOutputCharacter.restype = BOOL -LOG = open('out.txt', 'w+') - -def log(*args): - LOG.write(" ".join((str(x) for x in args)) + "\n") - LOG.flush() - class Char(Union): _fields_ = [ ("UnicodeChar",WCHAR), @@ -136,6 +131,27 @@ class INPUT_RECORD(Structure): 0x26: "up", # VK_UP 0x27: "right", # VK_RIGHT 0x28: "down", # VK_DOWN + 0x2E: "delete", # VK_DELETE + 0x70: "f1", # VK_F1 + 0x71: "f2", # VK_F2 + 0x72: "f3", # VK_F3 + 0x73: "f4", # VK_F4 + 0x74: "f5", # VK_F5 + 0x75: "f6", # VK_F6 + 0x76: "f7", # VK_F7 + 0x77: "f8", # VK_F8 + 0x78: "f9", # VK_F9 + 0x79: "f10", # VK_F10 + 0x7A: "f11", # VK_F11 + 0x7B: "f12", # VK_F12 + 0x7C: "f13", # VK_F13 + 0x7D: "f14", # VK_F14 + 0x7E: "f15", # VK_F15 + 0x7F: "f16", # VK_F16 + 0x79: "f17", # VK_F17 + 0x80: "f18", # VK_F18 + 0x81: "f19", # VK_F19 + 0x82: "f20", # VK_F20 } class _error(Exception): @@ -174,10 +190,10 @@ def refresh(self, screen, c_xy): - c_xy (tuple): Cursor position (x, y) on the screen. """ cx, cy = c_xy - log('Refresh', c_xy, self.screen_xy, self.__posxy) + trace('!!Refresh {}', screen) if not self.__gone_tall: while len(self.screen) < min(len(screen), self.height): - log('extend') + trace('extend') self.__hide_cursor() self.__move(0, len(self.screen) - 1) self.__write("\n") @@ -209,11 +225,11 @@ def refresh(self, screen, c_xy): newscr = screen[offset : offset + height] # use hardware scrolling if we have it. - log('offsets', old_offset, offset) + trace('offsets', old_offset, offset) if old_offset > offset: - log('old_offset > offset') + trace('old_offset > offset') elif old_offset < offset: - log('old_offset < offset') + trace('old_offset < offset') if False: if old_offset > offset and self._ri: self.__hide_cursor() @@ -232,7 +248,7 @@ def refresh(self, screen, c_xy): oldscr.pop(0) oldscr.append("") - log('new offset', offset, px) + trace('new offset', offset, px) self.__offset = offset for ( @@ -245,7 +261,7 @@ def refresh(self, screen, c_xy): y = len(newscr) while y < len(oldscr): - log('need to erase', y) + trace('need to erase', y) self.__hide_cursor() self.__move(0, y) self.__posxy = 0, y @@ -255,7 +271,7 @@ def refresh(self, screen, c_xy): self.__show_cursor() self.screen = screen - log(f"Writing {self.screen} {cx} {cy}") + trace(f"Writing {self.screen} {cx} {cy}") #self.move_cursor(cx, cy) self.flushoutput() @@ -286,7 +302,7 @@ def __move(self, x, y): raise ctypes.WinError(ctypes.GetLastError()) x += info.dwCursorPosition.X y += info.dwCursorPosition.Y - log('..', x, y) + trace('..', x, y) self.move_cursor(x, y) @property @@ -329,7 +345,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): x_pos = px_pos x_coord = px_coord character_width = wlen(newline[x_pos]) - log('sinle char', x_coord, y, px_coord) + trace('sinle char', x_coord, y, px_coord) self.__move(x_coord, y) # self.__write_code(self.ich1) self.__write(newline[x_pos]) @@ -362,7 +378,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = character_width + 1, y else: - log("Rewrite all", x_coord, len(oldline)) + trace("Rewrite all", x_coord, len(oldline)) self.__hide_cursor() self.__move(x_coord, y) if wlen(oldline) > wlen(newline): @@ -401,13 +417,13 @@ def prepare(self) -> None: def restore(self) -> None: ... def __move_relative(self, x, y): - log('move relative', x, y) + trace('move relative', x, y) cur_x, cur_y = self.screen_xy dx = x - self.__posxy[0] dy = y - self.__posxy[1] cur_x += dx cur_y += dy - log('move is', cur_x, cur_y) + trace('move is', cur_x, cur_y) self.__move_absolute(cur_x, cur_y) def __move_absolute(self, x, y): @@ -419,7 +435,7 @@ def __move_absolute(self, x, y): raise ctypes.WinError(ctypes.GetLastError()) def move_cursor(self, x: int, y: int) -> None: - log(f'move to {x} {y}') + trace(f'move to {x} {y}') if x < 0 or y < 0: raise ValueError(f"Bad cussor position {x}, {y}") @@ -463,9 +479,11 @@ def get_event(self, block: bool = True) -> Event | None: # self.push_char(key) if rec.Event.KeyEvent.uChar.Char == b'\r': return Event(evt="key", data="\n", raw="\n") - log('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) + trace('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) if rec.Event.KeyEvent.wVirtualKeyCode == 8: return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) + if rec.Event.KeyEvent.wVirtualKeyCode == 27: + return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.Char) if rec.Event.KeyEvent.uChar.Char == b'\x00': code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: @@ -478,7 +496,7 @@ def push_char(self, char: int | bytes) -> None: """ Push a character to the console event queue. """ - log(f'put_char {char}') + trace(f'put_char {char}') def beep(self) -> None: ... @@ -494,7 +512,12 @@ def clear(self) -> None: def finish(self) -> None: """Move the cursor to the end of the display and otherwise get ready for end. XXX could be merged with restore? Hmm.""" - ... + y = len(self.screen) - 1 + while y >= 0 and not self.screen[y]: + y -= 1 + self.__move(0, min(y, self.height + self.__offset - 1)) + self.__write("\r\n") + self.flushoutput() def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some @@ -520,4 +543,4 @@ def wait(self) -> None: ... def repaint(self) -> None: - log('repaint') + trace('repaint') From 243817d8925ebffdc868824c51fb1f8a88fea1ff Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 15:09:40 -0700 Subject: [PATCH 27/66] Make insert work --- Lib/_pyrepl/windows_console.py | 37 +++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 88c2ee944df485..04e18ccd7659dc 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -46,6 +46,12 @@ class CONSOLE_CURSOR_INFO(Structure): ('bVisible', BOOL), ] +class CHAR_INFO(Structure): + _fields_ = [ + ('UnicodeChar', WCHAR), + ('Attributes', WORD), + ] + STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 GetStdHandle = windll.kernel32.GetStdHandle @@ -73,9 +79,15 @@ class CONSOLE_CURSOR_INFO(Structure): SetConsoleCursorPosition.use_last_error = True FillConsoleOutputCharacter = windll.kernel32.FillConsoleOutputCharacterW +FillConsoleOutputCharacter.use_last_error = True FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] FillConsoleOutputCharacter.restype = BOOL +ScrollConsoleScreenBuffer = windll.kernel32.ScrollConsoleScreenBufferW +ScrollConsoleScreenBuffer.use_last_error = True +ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] +ScrollConsoleScreenBuffer.rettype = BOOL + class Char(Union): _fields_ = [ ("UnicodeChar",WCHAR), @@ -336,7 +348,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): x_pos += 1 # if we need to insert a single character right after the first detected change - if oldline[x_pos:] == newline[x_pos + 1 :]: # and self.ich1: + if oldline[x_pos:] == newline[x_pos + 1 :]: if ( y == self.__posxy[1] and x_coord > self.__posxy[0] @@ -345,9 +357,20 @@ def __write_changed_line(self, y, oldline, newline, px_coord): x_pos = px_pos x_coord = px_coord character_width = wlen(newline[x_pos]) - trace('sinle char', x_coord, y, px_coord) + + ins_x, ins_y = self.get_abs_position(x_coord + 1, y) + ins_x -= 1 + scroll_rect = SMALL_RECT() + scroll_rect.Top = scroll_rect.Bottom = ins_y + scroll_rect.Left = ins_x + scroll_rect.Right = self.getheightwidth()[1] - 1 + destination_origin = _COORD(X = scroll_rect.Left + 1, Y = scroll_rect.Top) + fill_info = CHAR_INFO() + fill_info.UnicodeChar = ' ' + + if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): + raise ctypes.WinError(ctypes.GetLastError()) self.__move(x_coord, y) -# self.__write_code(self.ich1) self.__write(newline[x_pos]) self.__posxy = x_coord + character_width, y @@ -418,13 +441,17 @@ def restore(self) -> None: ... def __move_relative(self, x, y): trace('move relative', x, y) + cur_x, cur_y = self.get_abs_position(x, y) + trace('move is', cur_x, cur_y) + self.__move_absolute(cur_x, cur_y) + + def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_x, cur_y = self.screen_xy dx = x - self.__posxy[0] dy = y - self.__posxy[1] cur_x += dx cur_y += dy - trace('move is', cur_x, cur_y) - self.__move_absolute(cur_x, cur_y) + return cur_x, cur_y def __move_absolute(self, x, y): assert 0 <= y - self.__offset < self.height, y - self.__offset From 25f51b4021d2c7eab30e1bd023d883303619a936 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 15:18:40 -0700 Subject: [PATCH 28/66] Fix delete in middle --- Lib/_pyrepl/windows_console.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 04e18ccd7659dc..112b49fa872d6b 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -284,8 +284,8 @@ def refresh(self, screen, c_xy): self.screen = screen trace(f"Writing {self.screen} {cx} {cy}") - #self.move_cursor(cx, cy) self.flushoutput() + self.move_cursor(cx, cy) def __hide_cursor(self): info = CONSOLE_CURSOR_INFO() @@ -412,15 +412,15 @@ def __write_changed_line(self, y, oldline, newline, px_coord): size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): raise ctypes.WinError(ctypes.GetLastError()) - + #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) self.__write(newline[x_pos:]) self.__posxy = wlen(newline), y - if "\x1b" in newline: + #if "\x1b" in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. - self.move_cursor(0, y) + # self.move_cursor(0, y) def prepare(self) -> None: self.screen = [] @@ -440,9 +440,9 @@ def prepare(self) -> None: def restore(self) -> None: ... def __move_relative(self, x, y): - trace('move relative', x, y) + trace('move relative {} {}', x, y) cur_x, cur_y = self.get_abs_position(x, y) - trace('move is', cur_x, cur_y) + trace('move is {} {}', cur_x, cur_y) self.__move_absolute(cur_x, cur_y) def get_abs_position(self, x: int, y: int) -> tuple[int, int]: From 5242239206d5496099dad2765ae679fa8943d554 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 18 May 2024 15:22:26 -0700 Subject: [PATCH 29/66] Fix crash on invalid command key --- Lib/_pyrepl/windows_console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 112b49fa872d6b..c079af1f22429a 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -563,7 +563,7 @@ def forgetinput(self) -> None: def getpending(self) -> Event: """Return the characters that have been typed but not yet processed.""" - ... + return Event("key", "", b"") def wait(self) -> None: """Wait for an event.""" From febe424f79833bb6aba8d678bcfe8fb06f0ddd40 Mon Sep 17 00:00:00 2001 From: dino Date: Sun, 19 May 2024 19:18:39 -0700 Subject: [PATCH 30/66] More fixes --- Lib/_pyrepl/windows_console.py | 54 +++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index c079af1f22429a..fb120ee505b93f 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -205,7 +205,7 @@ def refresh(self, screen, c_xy): trace('!!Refresh {}', screen) if not self.__gone_tall: while len(self.screen) < min(len(screen), self.height): - trace('extend') + cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) self.__hide_cursor() self.__move(0, len(self.screen) - 1) self.__write("\n") @@ -237,7 +237,7 @@ def refresh(self, screen, c_xy): newscr = screen[offset : offset + height] # use hardware scrolling if we have it. - trace('offsets', old_offset, offset) + trace('offsets {} {}', old_offset, offset) if old_offset > offset: trace('old_offset > offset') elif old_offset < offset: @@ -260,7 +260,7 @@ def refresh(self, screen, c_xy): oldscr.pop(0) oldscr.append("") - trace('new offset', offset, px) + trace('new offset {} {}', offset, px) self.__offset = offset for ( @@ -277,7 +277,7 @@ def refresh(self, screen, c_xy): self.__hide_cursor() self.__move(0, y) self.__posxy = 0, y -# self.__write_code(self._el) + self.erase_to_end() y += 1 self.__show_cursor() @@ -306,7 +306,8 @@ def __show_cursor(self): raise ctypes.WinError(ctypes.GetLastError()) def __write(self, text): - self.__buffer.append((text, 0)) + os.write(self.output_fd, text.encode(self.encoding, "replace")) + #self.__buffer.append((text, 0)) def __move(self, x, y): info = CONSOLE_SCREEN_BUFFER_INFO() @@ -349,6 +350,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): # if we need to insert a single character right after the first detected change if oldline[x_pos:] == newline[x_pos + 1 :]: + trace('insert single') if ( y == self.__posxy[1] and x_coord > self.__posxy[0] @@ -376,6 +378,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): # if it's a single character change in the middle of the line elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]): + trace('char change') character_width = wlen(newline[x_pos]) self.__move(x_coord, y) self.__write(newline[x_pos]) @@ -389,6 +392,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): and x_coord < wlen(newline) - 2 and newline[x_pos + 1 : -1] == oldline[x_pos:-2] ): + trace('last char') self.__hide_cursor() self.__move(self.width - 2, y) self.__posxy = self.width - 2, y @@ -401,38 +405,42 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = character_width + 1, y else: - trace("Rewrite all", x_coord, len(oldline)) + trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() self.__move(x_coord, y) if wlen(oldline) > wlen(newline): - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - - size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X - if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) + self.erase_to_end() #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) self.__write(newline[x_pos:]) self.__posxy = wlen(newline), y + #if "\x1b" in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. # self.move_cursor(0, y) + def erase_to_end(self): + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X + if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + def prepare(self) -> None: self.screen = [] self.height, self.width = self.getheightwidth() - self.__buffer = [] + #self.__buffer = [] info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - self.__posxy = 0, 0 #info.dwCursorPosition.X, info.dwCursorPosition.Y + self.__posxy = 0, 0 self.__gone_tall = 0 self.__move = self.__move_relative self.__offset = 0 @@ -440,7 +448,7 @@ def prepare(self) -> None: def restore(self) -> None: ... def __move_relative(self, x, y): - trace('move relative {} {}', x, y) + trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) trace('move is {} {}', cur_x, cur_y) self.__move_absolute(cur_x, cur_y) @@ -454,7 +462,6 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: return cur_x, cur_y def __move_absolute(self, x, y): - assert 0 <= y - self.__offset < self.height, y - self.__offset cord = _COORD() cord.X = x cord.Y = y @@ -549,12 +556,13 @@ def finish(self) -> None: def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some buffering going on somewhere).""" - for text, iscode in self.__buffer: - if iscode: - self.__tputs(text) - else: - os.write(self.output_fd, text.encode(self.encoding, "replace")) - del self.__buffer[:] + if False: + for text, iscode in self.__buffer: + if iscode: + self.__tputs(text) + else: + os.write(self.output_fd, text.encode(self.encoding, "replace")) + del self.__buffer[:] def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" From 756ac47100f814d7d0b7d410071d8440de5147c4 Mon Sep 17 00:00:00 2001 From: dino Date: Sun, 19 May 2024 19:43:09 -0700 Subject: [PATCH 31/66] Colorize --- Lib/_pyrepl/windows_console.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index fb120ee505b93f..6f06c580645088 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -86,7 +86,12 @@ class CHAR_INFO(Structure): ScrollConsoleScreenBuffer = windll.kernel32.ScrollConsoleScreenBufferW ScrollConsoleScreenBuffer.use_last_error = True ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] -ScrollConsoleScreenBuffer.rettype = BOOL +ScrollConsoleScreenBuffer.restype = BOOL + +SetConsoleMode = windll.kernel32.SetConsoleMode +SetConsoleMode.use_last_error = True +SetConsoleMode.argtypes = [HANDLE, DWORD] +SetConsoleMode.restype = BOOL class Char(Union): _fields_ = [ @@ -180,6 +185,7 @@ def __init__( term: str = "", encoding: str = "", ): + SetConsoleMode(OutHandle, 0x0004 | 0x0001) self.encoding = encoding or sys.getdefaultencoding() if isinstance(f_in, int): @@ -415,11 +421,13 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = wlen(newline), y - #if "\x1b" in newline: + if "\x1b" in newline: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. - # self.move_cursor(0, y) + _, cur_y = self.get_abs_position(0, y) + self.__move_absolute(0, cur_y) + self.__posxy = 0, y def erase_to_end(self): info = CONSOLE_SCREEN_BUFFER_INFO() From 50fd4c993c8ca2a9ca5638c2fbbc7caadc93da40 Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 20 May 2024 06:55:12 -0700 Subject: [PATCH 32/66] Use constants --- Lib/_pyrepl/windows_console.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 6f06c580645088..c897b16dd0e68d 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -19,6 +19,9 @@ from __future__ import annotations +import os +import sys + from abc import ABC, abstractmethod from dataclasses import dataclass, field from _pyrepl.console import Event, Console @@ -27,7 +30,7 @@ from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR from ctypes import Structure, POINTER, Union from ctypes import windll -import os + if False: from typing import IO @@ -171,6 +174,9 @@ class INPUT_RECORD(Structure): 0x82: "f20", # VK_F20 } +ENABLE_PROCESSED_OUTPUT = 0x01 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 + class _error(Exception): pass @@ -185,7 +191,8 @@ def __init__( term: str = "", encoding: str = "", ): - SetConsoleMode(OutHandle, 0x0004 | 0x0001) + + SetConsoleMode(OutHandle, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) self.encoding = encoding or sys.getdefaultencoding() if isinstance(f_in, int): From ff46d66ce342308aef19f1c556183808f39670d5 Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 20 May 2024 19:28:24 -0700 Subject: [PATCH 33/66] Use UnicodeChar --- Lib/_pyrepl/windows_console.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index c897b16dd0e68d..d102b73ae4701a 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -524,22 +524,20 @@ def get_event(self, block: bool = True) -> Event | None: return None if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: return False - key = chr(rec.Event.KeyEvent.uChar.Char[0]) - # self.push_char(key) - if rec.Event.KeyEvent.uChar.Char == b'\r': - return Event(evt="key", data="\n", raw="\n") - trace('virtual key code', rec.Event.KeyEvent.wVirtualKeyCode, rec.Event.KeyEvent.uChar.Char) + key = rec.Event.KeyEvent.uChar.UnicodeChar + if rec.Event.KeyEvent.uChar.UnicodeChar == '\r': + return Event(evt="key", data="\n", raw="\n") if rec.Event.KeyEvent.wVirtualKeyCode == 8: - return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.Char) + return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.UnicodeChar) if rec.Event.KeyEvent.wVirtualKeyCode == 27: - return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.Char) - if rec.Event.KeyEvent.uChar.Char == b'\x00': + return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.UnicodeChar) + if rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: - return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.Char) + return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar) continue #print(key, rec.Event.KeyEvent.wVirtualKeyCode) - return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.Char) + return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar) def push_char(self, char: int | bytes) -> None: """ From fbb9f840b0c4ccfec4187a95423d77fdff8b0da7 Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 20 May 2024 19:57:10 -0700 Subject: [PATCH 34/66] Simplify --- Lib/_pyrepl/windows_console.py | 70 ++++++++++++---------------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index d102b73ae4701a..0e5a821bfe2475 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -389,34 +389,6 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__write(newline[x_pos]) self.__posxy = x_coord + character_width, y - # if it's a single character change in the middle of the line - elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]): - trace('char change') - character_width = wlen(newline[x_pos]) - self.__move(x_coord, y) - self.__write(newline[x_pos]) - self.__posxy = x_coord + character_width, y - - # if this is the last character to fit in the line and we edit in the middle of the line - elif ( -# self.dch1 -# and self.ich1 and - wlen(newline) == self.width - and x_coord < wlen(newline) - 2 - and newline[x_pos + 1 : -1] == oldline[x_pos:-2] - ): - trace('last char') - self.__hide_cursor() - self.__move(self.width - 2, y) - self.__posxy = self.width - 2, y -# self.__write_code(self.dch1) - - character_width = wlen(newline[x_pos]) - self.__move(x_coord, y) -# self.__write_code(self.ich1) - self.__write(newline[x_pos]) - self.__posxy = character_width + 1, y - else: trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() @@ -492,7 +464,6 @@ def move_cursor(self, x: int, y: int) -> None: self.__move(x, y) self.__posxy = x, y self.flushoutput() - def set_cursor_vis(self, visible: bool) -> None: if visible: @@ -513,30 +484,44 @@ def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false and there is no event pending, otherwise waits for the completion of an event.""" - rec = INPUT_RECORD() - read = DWORD() while True: + rec = INPUT_RECORD() + read = DWORD() if not ReadConsoleInput(InHandle, rec, 1, read): raise ctypes.WinError(ctypes.GetLastError()) + if read.value == 0: if block: continue return None + if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: - return False + # Only process keys and keydown events + if block: + continue + return None + key = rec.Event.KeyEvent.uChar.UnicodeChar + if rec.Event.KeyEvent.uChar.UnicodeChar == '\r': + # Make enter make unix-like return Event(evt="key", data="\n", raw="\n") - if rec.Event.KeyEvent.wVirtualKeyCode == 8: + elif rec.Event.KeyEvent.wVirtualKeyCode == 8: + # Turn backspace directly into the command return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.UnicodeChar) - if rec.Event.KeyEvent.wVirtualKeyCode == 27: + elif rec.Event.KeyEvent.wVirtualKeyCode == 27: + # Turn escape directly into the command return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.UnicodeChar) - if rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': + elif rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': + # Handle special keys like arrow keys and translate them into the appropriate command code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar) - continue - #print(key, rec.Event.KeyEvent.wVirtualKeyCode) + if block: + continue + + return None + return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar) def push_char(self, char: int | bytes) -> None: @@ -564,19 +549,12 @@ def finish(self) -> None: y -= 1 self.__move(0, min(y, self.height + self.__offset - 1)) self.__write("\r\n") - self.flushoutput() def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some buffering going on somewhere).""" - if False: - for text, iscode in self.__buffer: - if iscode: - self.__tputs(text) - else: - os.write(self.output_fd, text.encode(self.encoding, "replace")) - del self.__buffer[:] - + pass + def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" ... From 683f5f6b879ede1dd11a55248d47f153c143291f Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 21 May 2024 11:07:41 -0700 Subject: [PATCH 35/66] Fix scrolling on input which is longer than screen/scrollback --- Lib/_pyrepl/windows_console.py | 132 +++++++++++++++++---------------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 0e5a821bfe2475..85aef5253dda67 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -27,7 +27,7 @@ from _pyrepl.console import Event, Console from .trace import trace import ctypes -from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR +from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR, SHORT from ctypes import Structure, POINTER, Union from ctypes import windll @@ -215,22 +215,17 @@ def refresh(self, screen, c_xy): - c_xy (tuple): Cursor position (x, y) on the screen. """ cx, cy = c_xy + trace('!!Refresh {}', screen) - if not self.__gone_tall: - while len(self.screen) < min(len(screen), self.height): - cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) - self.__hide_cursor() - self.__move(0, len(self.screen) - 1) - self.__write("\n") - self.__posxy = 0, len(self.screen) - self.screen.append("") - else: - while len(self.screen) < len(screen): - self.screen.append("") + while len(self.screen) < min(len(screen), self.height): + trace("...") + cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) + self.__hide_cursor() + self.__move_relative(0, len(self.screen) - 1) + self.__write("\n") + self.__posxy = 0, len(self.screen) + self.screen.append("") - if len(screen) > self.height: - self.__gone_tall = 1 - self.__move = self.__move_absolute px, py = self.__posxy old_offset = offset = self.__offset @@ -242,36 +237,35 @@ def refresh(self, screen, c_xy): offset = cy elif cy >= offset + height: offset = cy - height + 1 + scroll_lines = offset - old_offset + + trace(f'adj offset {cy} {height} {offset} {old_offset}') + # Scrolling the buffer as the current input is greater than the visible + # portion of the window. We need to scroll the visible portion and the + # entire history + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + bottom = info.srWindow.Bottom + + trace("Scrolling {} {} {} {} {}", scroll_lines, info.srWindow.Bottom, self.height, len(screen) - self.height, self.__posxy) + self.scroll(scroll_lines, bottom) + self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines + self.__offset += scroll_lines + + for i in range(scroll_lines): + self.screen.append("") + elif offset > 0 and len(screen) < offset + height: + trace("Adding extra line") offset = max(len(screen) - height, 0) - screen.append("") + #screen.append("") oldscr = self.screen[old_offset : old_offset + height] newscr = screen[offset : offset + height] - - # use hardware scrolling if we have it. - trace('offsets {} {}', old_offset, offset) - if old_offset > offset: - trace('old_offset > offset') - elif old_offset < offset: - trace('old_offset < offset') - if False: - if old_offset > offset and self._ri: - self.__hide_cursor() - self.__write_code(self._cup, 0, 0) - self.__posxy = 0, old_offset - for i in range(old_offset - offset): - self.__write_code(self._ri) - oldscr.pop(-1) - oldscr.insert(0, "") - elif old_offset < offset and self._ind: - self.__hide_cursor() - self.__write_code(self._cup, self.height - 1, 0) - self.__posxy = 0, old_offset + self.height - 1 - for i in range(offset - old_offset): - self.__write_code(self._ind) - oldscr.pop(0) - oldscr.append("") + trace('old screen {}', oldscr) + trace('new screen {}', newscr) trace('new offset {} {}', offset, px) self.__offset = offset @@ -286,9 +280,8 @@ def refresh(self, screen, c_xy): y = len(newscr) while y < len(oldscr): - trace('need to erase', y) self.__hide_cursor() - self.__move(0, y) + self.__move_relative(0, y) self.__posxy = 0, y self.erase_to_end() y += 1 @@ -296,10 +289,23 @@ def refresh(self, screen, c_xy): self.__show_cursor() self.screen = screen - trace(f"Writing {self.screen} {cx} {cy}") - self.flushoutput() + trace("Done and moving") self.move_cursor(cx, cy) + def scroll(self, top: int, bottom: int, left: int | None = None, right: int | None = None): + scroll_rect = SMALL_RECT() + scroll_rect.Top = SHORT(top) + scroll_rect.Bottom = SHORT(bottom) + scroll_rect.Left = SHORT(0 if left is None else left) + scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1 if right is None else right) + trace(f"Scrolling {scroll_rect.Top} {scroll_rect.Bottom}") + destination_origin = _COORD() + fill_info = CHAR_INFO() + fill_info.UnicodeChar = ' ' + + if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): + raise ctypes.WinError(ctypes.GetLastError()) + def __hide_cursor(self): info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): @@ -338,7 +344,7 @@ def screen_xy(self): raise ctypes.WinError(ctypes.GetLastError()) return info.dwCursorPosition.X, info.dwCursorPosition.Y - def __write_changed_line(self, y, oldline, newline, px_coord): + def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): # this is frustrating; there's no reason to test (say) # self.dch1 inside the loop -- but alternative ways of # structuring this function are equally painful (I'm trying to @@ -376,7 +382,7 @@ def __write_changed_line(self, y, oldline, newline, px_coord): ins_x, ins_y = self.get_abs_position(x_coord + 1, y) ins_x -= 1 scroll_rect = SMALL_RECT() - scroll_rect.Top = scroll_rect.Bottom = ins_y + scroll_rect.Top = scroll_rect.Bottom = SHORT(ins_y) scroll_rect.Left = ins_x scroll_rect.Right = self.getheightwidth()[1] - 1 destination_origin = _COORD(X = scroll_rect.Left + 1, Y = scroll_rect.Top) @@ -385,14 +391,15 @@ def __write_changed_line(self, y, oldline, newline, px_coord): if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): raise ctypes.WinError(ctypes.GetLastError()) - self.__move(x_coord, y) + + self.__move_relative(x_coord, y) self.__write(newline[x_pos]) self.__posxy = x_coord + character_width, y else: trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() - self.__move(x_coord, y) + self.__move_relative(x_coord, y) if wlen(oldline) > wlen(newline): self.erase_to_end() #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) @@ -421,25 +428,16 @@ def prepare(self) -> None: self.screen = [] self.height, self.width = self.getheightwidth() - #self.__buffer = [] - info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) self.__posxy = 0, 0 self.__gone_tall = 0 - self.__move = self.__move_relative self.__offset = 0 def restore(self) -> None: ... - def __move_relative(self, x, y): - trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) - cur_x, cur_y = self.get_abs_position(x, y) - trace('move is {} {}', cur_x, cur_y) - self.__move_absolute(cur_x, cur_y) - def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_x, cur_y = self.screen_xy dx = x - self.__posxy[0] @@ -448,7 +446,16 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_y += dy return cur_x, cur_y + def __move_relative(self, x, y): + trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) + cur_x, cur_y = self.get_abs_position(x, y) + trace('move is {} {}', cur_x, cur_y) + self.__move_absolute(cur_x, cur_y) + def __move_absolute(self, x, y): + """Moves to an absolute location in the screen buffer""" + if y < 0: + trace(f"Negative offset: {self.__posxy} {self.screen_xy}") cord = _COORD() cord.X = x cord.Y = y @@ -456,14 +463,13 @@ def __move_absolute(self, x, y): raise ctypes.WinError(ctypes.GetLastError()) def move_cursor(self, x: int, y: int) -> None: - trace(f'move to {x} {y}') + trace(f'move_cursor {x} {y}') if x < 0 or y < 0: raise ValueError(f"Bad cussor position {x}, {y}") - self.__move(x, y) + self.__move_relative(x, y) self.__posxy = x, y - self.flushoutput() def set_cursor_vis(self, visible: bool) -> None: if visible: @@ -547,12 +553,14 @@ def finish(self) -> None: y = len(self.screen) - 1 while y >= 0 and not self.screen[y]: y -= 1 - self.__move(0, min(y, self.height + self.__offset - 1)) + self.__move_relative(0, min(y, self.height + self.__offset - 1)) self.__write("\r\n") def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some - buffering going on somewhere).""" + buffering going on somewhere). + + All output on Windows is unbuffered so this is a nop""" pass def forgetinput(self) -> None: From 2c524b8d9b64219d4915b7338b138b6b95677a5c Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Tue, 21 May 2024 04:26:29 +1000 Subject: [PATCH 36/66] fix pager typo and refactor some unused branches (#41) --- Lib/_pyrepl/reader.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 81df0c925ee6cb..acd06de1bd3870 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -441,14 +441,13 @@ def get_arg(self, default: int = 1) -> int: """ if self.arg is None: return default - else: - return self.arg + return self.arg def get_prompt(self, lineno: int, cursor_on_line: bool) -> str: """Return what should be in the left-hand margin for line 'lineno'.""" if self.arg is not None and cursor_on_line: - prompt = "(arg: %s) " % self.arg + prompt = f"(arg: {self.arg}) " elif self.paste_mode: prompt = "(paste) " elif "\n" in self.buffer: @@ -514,12 +513,12 @@ def pos2xy(self) -> tuple[int, int]: offset = l - 1 if in_wrapped_line else l # need to remove backslash if offset >= pos: break + + if p + sum(l2) >= self.console.width: + pos -= l - 1 # -1 cause backslash is not in buffer else: - if p + sum(l2) >= self.console.width: - pos -= l - 1 # -1 cause backslash is not in buffer - else: - pos -= l + 1 # +1 cause newline is in buffer - y += 1 + pos -= l + 1 # +1 cause newline is in buffer + y += 1 return p + sum(l2[:pos]), y def insert(self, text: str | list[str]) -> None: @@ -577,7 +576,6 @@ def suspend(self) -> SimpleContextManager: for arg in ("msg", "ps1", "ps2", "ps3", "ps4", "paste_mode"): setattr(self, arg, prev_state[arg]) self.prepare() - pass def finish(self) -> None: """Called when a command signals that we're finished.""" From 44ce57bbe00ae7b855cae265a1b6bf4f62738739 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Tue, 21 May 2024 05:17:15 +1000 Subject: [PATCH 37/66] Implement screen clear for Windows (#42) --- Lib/_pyrepl/windows_console.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 85aef5253dda67..24cf536dd81735 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -86,6 +86,11 @@ class CHAR_INFO(Structure): FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] FillConsoleOutputCharacter.restype = BOOL +FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute +FillConsoleOutputAttribute.use_last_error = True +FillConsoleOutputAttribute.argtypes = [HANDLE, WORD, DWORD, _COORD, POINTER(DWORD)] +FillConsoleOutputAttribute.restype = BOOL + ScrollConsoleScreenBuffer = windll.kernel32.ScrollConsoleScreenBufferW ScrollConsoleScreenBuffer.use_last_error = True ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] @@ -99,7 +104,7 @@ class CHAR_INFO(Structure): class Char(Union): _fields_ = [ ("UnicodeChar",WCHAR), - ("Char", CHAR), + ("Char", CHAR), ] class KeyEvent(ctypes.Structure): @@ -191,7 +196,7 @@ def __init__( term: str = "", encoding: str = "", ): - + SetConsoleMode(OutHandle, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) self.encoding = encoding or sys.getdefaultencoding() @@ -466,12 +471,13 @@ def move_cursor(self, x: int, y: int) -> None: trace(f'move_cursor {x} {y}') if x < 0 or y < 0: - raise ValueError(f"Bad cussor position {x}, {y}") + raise ValueError(f"Bad cursor position {x}, {y}") self.__move_relative(x, y) self.__posxy = x, y - def set_cursor_vis(self, visible: bool) -> None: + + def set_cursor_vis(self, visible: bool) -> None: if visible: self.__show_cursor() else: @@ -483,9 +489,9 @@ def getheightwidth(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - return (info.srWindow.Bottom - info.srWindow.Top + 1, + return (info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1) - + def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false and there is no event pending, otherwise waits for the @@ -546,6 +552,13 @@ def clear(self) -> None: size = info.dwSize.X * info.dwSize.Y if not FillConsoleOutputCharacter(OutHandle, b' ', size, _COORD(), DWORD()): raise ctypes.WinError(ctypes.GetLastError()) + if not FillConsoleOutputAttribute(OutHandle, 0, size, _COORD(), DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + y = info.srWindow.Bottom - info.srWindow.Top + 1 + self.__move_absolute(0, y - info.dwSize.Y) + self.__posxy = 0, 0 + self.screen = [""] + def finish(self) -> None: """Move the cursor to the end of the display and otherwise get From 5ecc8cd2e6fa15a6548425959ce1fdd62b3e3f8a Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 21 May 2024 12:10:28 -0700 Subject: [PATCH 38/66] Fix word wrap not being enabled in Windows Terminal Fix scrolling into input that has scrolled out of the buffer --- Lib/_pyrepl/windows_console.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 24cf536dd81735..deacedda475459 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -30,8 +30,9 @@ from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR, SHORT from ctypes import Structure, POINTER, Union from ctypes import windll +from typing import TYPE_CHECKING -if False: +if TYPE_CHECKING: from typing import IO class CONSOLE_SCREEN_BUFFER_INFO(Structure): @@ -180,6 +181,7 @@ class INPUT_RECORD(Structure): } ENABLE_PROCESSED_OUTPUT = 0x01 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 class _error(Exception): @@ -197,7 +199,7 @@ def __init__( encoding: str = "", ): - SetConsoleMode(OutHandle, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + SetConsoleMode(OutHandle, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) self.encoding = encoding or sys.getdefaultencoding() if isinstance(f_in, int): @@ -211,7 +213,7 @@ def __init__( self.output_fd = f_out.fileno() - def refresh(self, screen, c_xy): + def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ Refresh the console screen. @@ -224,14 +226,12 @@ def refresh(self, screen, c_xy): trace('!!Refresh {}', screen) while len(self.screen) < min(len(screen), self.height): trace("...") - cur_x, cur_y = self.get_abs_position(0, len(self.screen) - 1) self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) self.__write("\n") self.__posxy = 0, len(self.screen) self.screen.append("") - px, py = self.__posxy old_offset = offset = self.__offset height = self.height @@ -265,7 +265,7 @@ def refresh(self, screen, c_xy): elif offset > 0 and len(screen) < offset + height: trace("Adding extra line") offset = max(len(screen) - height, 0) - #screen.append("") + screen.append("") oldscr = self.screen[old_offset : old_offset + height] newscr = screen[offset : offset + height] @@ -303,7 +303,6 @@ def scroll(self, top: int, bottom: int, left: int | None = None, right: int | No scroll_rect.Bottom = SHORT(bottom) scroll_rect.Left = SHORT(0 if left is None else left) scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1 if right is None else right) - trace(f"Scrolling {scroll_rect.Top} {scroll_rect.Bottom}") destination_origin = _COORD() fill_info = CHAR_INFO() fill_info.UnicodeChar = ' ' @@ -331,7 +330,6 @@ def __show_cursor(self): def __write(self, text): os.write(self.output_fd, text.encode(self.encoding, "replace")) - #self.__buffer.append((text, 0)) def __move(self, x, y): info = CONSOLE_SCREEN_BUFFER_INFO() @@ -452,9 +450,15 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: return cur_x, cur_y def __move_relative(self, x, y): + """Moves relative to the start of the current input line""" trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) trace('move is {} {}', cur_x, cur_y) + if cur_y < 0: + # We're scrolling above the current buffer, we need to refresh + self.__posxy = self.__posxy[0], self.__posxy[1] + cur_y + self.refresh(self.screen, self.__posxy) + cur_y = 0 self.__move_absolute(cur_x, cur_y) def __move_absolute(self, x, y): From 5bb90c7df771f3a3525f0d20415cd8d968093f22 Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 21 May 2024 20:56:48 -0700 Subject: [PATCH 39/66] Fix issues with inputs longer than a single line --- Lib/_pyrepl/windows_console.py | 49 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index deacedda475459..504a0d4fdbc737 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -225,12 +225,12 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: trace('!!Refresh {}', screen) while len(self.screen) < min(len(screen), self.height): - trace("...") self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) self.__write("\n") self.__posxy = 0, len(self.screen) self.screen.append("") + trace(f"... {self.__posxy} {len(self.screen)} {self.height}") px, py = self.__posxy old_offset = offset = self.__offset @@ -328,18 +328,9 @@ def __show_cursor(self): if not SetConsoleCursorInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - def __write(self, text): + def __write(self, text: str): os.write(self.output_fd, text.encode(self.encoding, "replace")) - def __move(self, x, y): - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - x += info.dwCursorPosition.X - y += info.dwCursorPosition.Y - trace('..', x, y) - self.move_cursor(x, y) - @property def screen_xy(self): info = CONSOLE_SCREEN_BUFFER_INFO() @@ -372,7 +363,7 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): # if we need to insert a single character right after the first detected change if oldline[x_pos:] == newline[x_pos + 1 :]: - trace('insert single') + trace('insert single {} {}', y, self.__posxy) if ( y == self.__posxy[1] and x_coord > self.__posxy[0] @@ -380,14 +371,16 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): ): x_pos = px_pos x_coord = px_coord + character_width = wlen(newline[x_pos]) + # Scroll any text to the right if we're inserting ins_x, ins_y = self.get_abs_position(x_coord + 1, y) ins_x -= 1 scroll_rect = SMALL_RECT() scroll_rect.Top = scroll_rect.Bottom = SHORT(ins_y) - scroll_rect.Left = ins_x - scroll_rect.Right = self.getheightwidth()[1] - 1 + scroll_rect.Left = SHORT(ins_x) + scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1) destination_origin = _COORD(X = scroll_rect.Left + 1, Y = scroll_rect.Top) fill_info = CHAR_INFO() fill_info.UnicodeChar = ' ' @@ -397,20 +390,24 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): self.__move_relative(x_coord, y) self.__write(newline[x_pos]) - self.__posxy = x_coord + character_width, y - + if x_coord + character_width == self.width: + self.move_next_line(y) + else: + self.__posxy = x_coord + character_width, y else: - trace("Rewrite all {!r} {} {} {} {} {}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) + trace("Rewrite all {!r} {} {} y={} {} posxy={}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() self.__move_relative(x_coord, y) if wlen(oldline) > wlen(newline): self.erase_to_end() - #os.write(self.output_fd, newline[x_pos:].encode(self.encoding, "replace")) self.__write(newline[x_pos:]) - self.__posxy = wlen(newline), y + if len(newline[x_pos:]) == self.width: + self.move_next_line(y) + else: + self.__posxy = wlen(newline), y - if "\x1b" in newline: + if "\x1b" in newline or y != self.__posxy[1]: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. @@ -418,6 +415,12 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): self.__move_absolute(0, cur_y) self.__posxy = 0, y + def move_next_line(self, y: int): + self.__posxy = 0, y + _, cur_y = self.get_abs_position(0, y) + self.__move_absolute(0, cur_y) + self.__posxy = 0, y + def erase_to_end(self): info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): @@ -428,6 +431,7 @@ def erase_to_end(self): raise ctypes.WinError(ctypes.GetLastError()) def prepare(self) -> None: + trace("prepare") self.screen = [] self.height, self.width = self.getheightwidth() @@ -450,7 +454,7 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: return cur_x, cur_y def __move_relative(self, x, y): - """Moves relative to the start of the current input line""" + """Moves relative to the current __posxy""" trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) trace('move is {} {}', cur_x, cur_y) @@ -465,6 +469,9 @@ def __move_absolute(self, x, y): """Moves to an absolute location in the screen buffer""" if y < 0: trace(f"Negative offset: {self.__posxy} {self.screen_xy}") + if x < 0: + trace("Negative move {}", self.getheightwidth()) + # return cord = _COORD() cord.X = x cord.Y = y From 35557dd4dab9dcc0604bef9a00300fbbbf7e5f42 Mon Sep 17 00:00:00 2001 From: dino Date: Wed, 22 May 2024 23:43:24 -0700 Subject: [PATCH 40/66] Resize WIP --- Lib/_pyrepl/windows_console.py | 114 +++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 28 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 504a0d4fdbc737..bbdbf1c9f3bef9 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -31,6 +31,7 @@ from ctypes import Structure, POINTER, Union from ctypes import windll from typing import TYPE_CHECKING +from .utils import wlen if TYPE_CHECKING: from typing import IO @@ -118,11 +119,17 @@ class KeyEvent(ctypes.Structure): ("dwControlKeyState", DWORD), ] +class WindowsBufferSizeEvent(ctypes.Structure): + _fields_ = [ + ('dwSize', _COORD) + ] + + class ConsoleEvent(ctypes.Union): _fields_ = [ ("KeyEvent", KeyEvent), # ("MouseEvent", ), -# ("WindowsBufferSizeEvent", ), + ("WindowsBufferSizeEvent", WindowsBufferSizeEvent), # ("MenuEvent", ) # ("FocusEvent", ) ] @@ -187,9 +194,6 @@ class INPUT_RECORD(Structure): class _error(Exception): pass -def wlen(s: str) -> int: - return len(s) - class WindowsConsole(Console): def __init__( self, @@ -223,7 +227,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ cx, cy = c_xy - trace('!!Refresh {}', screen) + trace('!!Refresh {} {} {}', c_xy, self.__offset, screen) while len(self.screen) < min(len(screen), self.height): self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) @@ -275,6 +279,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: trace('new offset {} {}', offset, px) self.__offset = offset + self.__hide_cursor() for ( y, oldline, @@ -285,7 +290,6 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: y = len(newscr) while y < len(oldscr): - self.__hide_cursor() self.__move_relative(0, y) self.__posxy = 0, y self.erase_to_end() @@ -389,20 +393,34 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): raise ctypes.WinError(ctypes.GetLastError()) self.__move_relative(x_coord, y) + pos = self.__posxy + coord = self.screen_xy + self.__write(newline[x_pos]) if x_coord + character_width == self.width: - self.move_next_line(y) + + # If we wrapped we need to get back to a known good position, + # and the starting position was known good. + self.__move_absolute(*coord) + self.__posxy = pos else: self.__posxy = x_coord + character_width, y else: trace("Rewrite all {!r} {} {} y={} {} posxy={}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) self.__hide_cursor() self.__move_relative(x_coord, y) - if wlen(oldline) > wlen(newline): - self.erase_to_end() + pos = self.__posxy + coord = self.screen_xy + #if wlen(oldline) > wlen(newline): + self.erase_to_end() + trace(f"Writing {newline[x_pos:]}") self.__write(newline[x_pos:]) - if len(newline[x_pos:]) == self.width: - self.move_next_line(y) + + if wlen(newline[x_pos:]) == self.width: + # If we wrapped we need to get back to a known good position, + # and the starting position was known good. + self.__move_absolute(*coord) + self.__posxy = pos else: self.__posxy = wlen(newline), y @@ -411,16 +429,11 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. - _, cur_y = self.get_abs_position(0, y) - self.__move_absolute(0, cur_y) + #_, cur_y = self.get_abs_position(0, y) + #self.__move_absolute(0, cur_y) + self.__move_absolute(0, self.screen_xy[1]) self.__posxy = 0, y - def move_next_line(self, y: int): - self.__posxy = 0, y - _, cur_y = self.get_abs_position(0, y) - self.__move_absolute(0, cur_y) - self.__posxy = 0, y - def erase_to_end(self): info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): @@ -457,7 +470,6 @@ def __move_relative(self, x, y): """Moves relative to the current __posxy""" trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) cur_x, cur_y = self.get_abs_position(x, y) - trace('move is {} {}', cur_x, cur_y) if cur_y < 0: # We're scrolling above the current buffer, we need to refresh self.__posxy = self.__posxy[0], self.__posxy[1] + cur_y @@ -467,6 +479,7 @@ def __move_relative(self, x, y): def __move_absolute(self, x, y): """Moves to an absolute location in the screen buffer""" + trace(f"move absolute {x} {y}") if y < 0: trace(f"Negative offset: {self.__posxy} {self.screen_xy}") if x < 0: @@ -487,7 +500,6 @@ def move_cursor(self, x: int, y: int) -> None: self.__move_relative(x, y) self.__posxy = x, y - def set_cursor_vis(self, visible: bool) -> None: if visible: self.__show_cursor() @@ -503,21 +515,67 @@ def getheightwidth(self) -> tuple[int, int]: return (info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1) + def __read_input(self) -> INPUT_RECORD | None: + rec = INPUT_RECORD() + read = DWORD() + if not ReadConsoleInput(InHandle, rec, 1, read): + raise ctypes.WinError(ctypes.GetLastError()) + + if read.value == 0: + return None + + return rec + def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false and there is no event pending, otherwise waits for the completion of an event.""" while True: - rec = INPUT_RECORD() - read = DWORD() - if not ReadConsoleInput(InHandle, rec, 1, read): - raise ctypes.WinError(ctypes.GetLastError()) - - if read.value == 0: + rec = self.__read_input() + if rec is None: if block: continue return None + if rec.EventType == WINDOW_BUFFER_SIZE_EVENT: + old_height, old_width = self.height, self.width + self.height, self.width = self.getheightwidth() + delta = self.width - old_width + # Windows will fix up the wrapping for us, but we + # need to sync __posxy with those changes. + + new_x, new_y = self.__posxy + y = self.__posxy[1] + trace("Cur screen {}", self.screen) + #last_len = -1 + new_lines = 0 + while y >= 0: + line = self.screen[y] + line_len = wlen(line) + trace(f"XX {wlen(line)} {self.width} {old_width} {wlen(line) <= self.width} {old_width > wlen(line)} {line}") + if (line_len >= self.width and line_len < old_width) and line[-1] != "\\": + # This line is turning into 2 lines + trace("Lines wrap") + new_y += 1 + new_lines += 1 + #elif line_len >= old_width and line_len < self.width and line[-1] == "\\" and last_len == 1: + # # This line is turning into 1 line + # trace("Lines join") + # #new_y -= 1 + #last_len = line_len + y -= 1 + + trace(f"RESIZE {self.screen_xy} {self.__posxy} ({new_x}, {new_y}) ({self.width}, {self.height})") + # Force redraw of current input, the wrapping can corrupt our state with \ + self.screen = [' ' * self.width] * (len(self.screen) + new_lines) + + # We could have "unwrapped" things which weren't really wrapped, shifting our x position, + # get back to something neutral. + self.__move_absolute(0, self.screen_xy[1]) + self.__posxy = 0, new_y + + return Event("resize", "") + if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: # Only process keys and keydown events if block: @@ -566,7 +624,7 @@ def clear(self) -> None: if not FillConsoleOutputAttribute(OutHandle, 0, size, _COORD(), DWORD()): raise ctypes.WinError(ctypes.GetLastError()) y = info.srWindow.Bottom - info.srWindow.Top + 1 - self.__move_absolute(0, y - info.dwSize.Y) + self.__move_absolute(0, 0) self.__posxy = 0, 0 self.screen = [""] From e04699f69005f880fc65e3b714d6f24f98715523 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 14:10:33 -0700 Subject: [PATCH 41/66] Use vt100 scrolling to avoid race conditions on resize --- Lib/_pyrepl/windows_console.py | 145 ++++++++++----------------------- 1 file changed, 42 insertions(+), 103 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index bbdbf1c9f3bef9..02cefe168ba25b 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -78,11 +78,6 @@ class CHAR_INFO(Structure): GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] GetConsoleCursorInfo.restype = BOOL -SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition -SetConsoleCursorPosition.argtypes = [HANDLE, _COORD] -SetConsoleCursorPosition.restype = BOOL -SetConsoleCursorPosition.use_last_error = True - FillConsoleOutputCharacter = windll.kernel32.FillConsoleOutputCharacterW FillConsoleOutputCharacter.use_last_error = True FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] @@ -216,6 +211,8 @@ def __init__( else: self.output_fd = f_out.fileno() + self.event_queue: deque[Event] = [] + def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ @@ -227,7 +224,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ cx, cy = c_xy - trace('!!Refresh {} {} {}', c_xy, self.__offset, screen) + trace('!!Refresh c_xy={} offset={} screen={} posxy={} screen_xy={}', c_xy, self.__offset, screen, self.__posxy, self.screen_xy) while len(self.screen) < min(len(screen), self.height): self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) @@ -262,7 +259,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.scroll(scroll_lines, bottom) self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines self.__offset += scroll_lines - + for i in range(scroll_lines): self.screen.append("") @@ -273,6 +270,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: oldscr = self.screen[old_offset : old_offset + height] newscr = screen[offset : offset + height] + trace('old screen {}', oldscr) trace('new screen {}', newscr) @@ -298,7 +296,6 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.__show_cursor() self.screen = screen - trace("Done and moving") self.move_cursor(cx, cy) def scroll(self, top: int, bottom: int, left: int | None = None, right: int | None = None): @@ -366,6 +363,7 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): x_pos += 1 # if we need to insert a single character right after the first detected change + screen_cord = self.screen_xy if oldline[x_pos:] == newline[x_pos + 1 :]: trace('insert single {} {}', y, self.__posxy) if ( @@ -397,42 +395,25 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): coord = self.screen_xy self.__write(newline[x_pos]) - if x_coord + character_width == self.width: - - # If we wrapped we need to get back to a known good position, - # and the starting position was known good. - self.__move_absolute(*coord) - self.__posxy = pos - else: - self.__posxy = x_coord + character_width, y + self.__posxy = x_coord + character_width, y else: - trace("Rewrite all {!r} {} {} y={} {} posxy={}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy) + trace("Rewrite all {!r} x_coord={} {} y={} {} posxy={} screen_xy={}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy, self.screen_xy) self.__hide_cursor() self.__move_relative(x_coord, y) pos = self.__posxy coord = self.screen_xy - #if wlen(oldline) > wlen(newline): - self.erase_to_end() - trace(f"Writing {newline[x_pos:]}") - self.__write(newline[x_pos:]) + if wlen(oldline) > wlen(newline): + self.erase_to_end() - if wlen(newline[x_pos:]) == self.width: - # If we wrapped we need to get back to a known good position, - # and the starting position was known good. - self.__move_absolute(*coord) - self.__posxy = pos - else: - self.__posxy = wlen(newline), y + self.__write(newline[x_pos:]) + self.__posxy = wlen(newline), y if "\x1b" in newline or y != self.__posxy[1]: # ANSI escape characters are present, so we can't assume # anything about the position of the cursor. Moving the cursor # to the left margin should work to get to a known position. - #_, cur_y = self.get_abs_position(0, y) - #self.__move_absolute(0, cur_y) - self.__move_absolute(0, self.screen_xy[1]) - self.__posxy = 0, y + self.move_cursor(0, y) def erase_to_end(self): info = CONSOLE_SCREEN_BUFFER_INFO() @@ -468,28 +449,18 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: def __move_relative(self, x, y): """Moves relative to the current __posxy""" - trace('move relative {} {} {} {}', x, y, self.__posxy, self.screen_xy) - cur_x, cur_y = self.get_abs_position(x, y) - if cur_y < 0: - # We're scrolling above the current buffer, we need to refresh - self.__posxy = self.__posxy[0], self.__posxy[1] + cur_y - self.refresh(self.screen, self.__posxy) - cur_y = 0 - self.__move_absolute(cur_x, cur_y) - - def __move_absolute(self, x, y): - """Moves to an absolute location in the screen buffer""" - trace(f"move absolute {x} {y}") - if y < 0: - trace(f"Negative offset: {self.__posxy} {self.screen_xy}") - if x < 0: - trace("Negative move {}", self.getheightwidth()) - # return - cord = _COORD() - cord.X = x - cord.Y = y - if not SetConsoleCursorPosition(OutHandle, cord): - raise ctypes.WinError(ctypes.GetLastError()) + dx = x - self.__posxy[0] + dy = y - self.__posxy[1] + if dx < 0: + os.write(self.output_fd, f"\x1b[{-dx}D".encode(self.encoding)) + elif dx > 0: + trace(f"Move console right {dx}") + os.write(self.output_fd, f"\x1b[{dx}C".encode(self.encoding)) + + if dy < 0: + os.write(self.output_fd, f"\x1b[{-dy}A".encode(self.encoding)) + elif dy > 0: + os.write(self.output_fd, f"\x1b[{dy}B".encode(self.encoding)) def move_cursor(self, x: int, y: int) -> None: trace(f'move_cursor {x} {y}') @@ -497,8 +468,11 @@ def move_cursor(self, x: int, y: int) -> None: if x < 0 or y < 0: raise ValueError(f"Bad cursor position {x}, {y}") - self.__move_relative(x, y) - self.__posxy = x, y + if y < self.__offset or y >= self.__offset + self.height: + self.event_queue.insert(0, Event("scroll", "")) + else: + self.__move_relative(x, y) + self.__posxy = x, y def set_cursor_vis(self, visible: bool) -> None: if visible: @@ -530,6 +504,9 @@ def get_event(self, block: bool = True) -> Event | None: """Return an Event instance. Returns None if |block| is false and there is no event pending, otherwise waits for the completion of an event.""" + if self.event_queue: + return self.event_queue.pop() + while True: rec = self.__read_input() if rec is None: @@ -538,42 +515,6 @@ def get_event(self, block: bool = True) -> Event | None: return None if rec.EventType == WINDOW_BUFFER_SIZE_EVENT: - old_height, old_width = self.height, self.width - self.height, self.width = self.getheightwidth() - delta = self.width - old_width - # Windows will fix up the wrapping for us, but we - # need to sync __posxy with those changes. - - new_x, new_y = self.__posxy - y = self.__posxy[1] - trace("Cur screen {}", self.screen) - #last_len = -1 - new_lines = 0 - while y >= 0: - line = self.screen[y] - line_len = wlen(line) - trace(f"XX {wlen(line)} {self.width} {old_width} {wlen(line) <= self.width} {old_width > wlen(line)} {line}") - if (line_len >= self.width and line_len < old_width) and line[-1] != "\\": - # This line is turning into 2 lines - trace("Lines wrap") - new_y += 1 - new_lines += 1 - #elif line_len >= old_width and line_len < self.width and line[-1] == "\\" and last_len == 1: - # # This line is turning into 1 line - # trace("Lines join") - # #new_y -= 1 - #last_len = line_len - y -= 1 - - trace(f"RESIZE {self.screen_xy} {self.__posxy} ({new_x}, {new_y}) ({self.width}, {self.height})") - # Force redraw of current input, the wrapping can corrupt our state with \ - self.screen = [' ' * self.width] * (len(self.screen) + new_lines) - - # We could have "unwrapped" things which weren't really wrapped, shifting our x position, - # get back to something neutral. - self.__move_absolute(0, self.screen_xy[1]) - self.__posxy = 0, new_y - return Event("resize", "") if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown: @@ -615,20 +556,18 @@ def beep(self) -> None: ... def clear(self) -> None: """Wipe the screen""" - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - size = info.dwSize.X * info.dwSize.Y - if not FillConsoleOutputCharacter(OutHandle, b' ', size, _COORD(), DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) - if not FillConsoleOutputAttribute(OutHandle, 0, size, _COORD(), DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) - y = info.srWindow.Bottom - info.srWindow.Top + 1 - self.__move_absolute(0, 0) + os.write(self.output_fd, f"\x1b[H\x1b[J".encode(self.encoding)) self.__posxy = 0, 0 self.screen = [""] - + def clear_range(self, x: int, y: int, width: int, height: int) -> None: + size = width * height + start = _COORD(X = x, Y = y) + if not FillConsoleOutputCharacter(OutHandle, b' ', size, start, DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + if not FillConsoleOutputAttribute(OutHandle, 0, size, start, DWORD()): + raise ctypes.WinError(ctypes.GetLastError()) + def finish(self) -> None: """Move the cursor to the end of the display and otherwise get ready for end. XXX could be merged with restore? Hmm.""" From 623040047db9973ff27c527077b11775e20f1d05 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 14:26:16 -0700 Subject: [PATCH 42/66] More win api cleanup --- Lib/_pyrepl/windows_console.py | 80 ++++++++-------------------------- 1 file changed, 17 insertions(+), 63 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 02cefe168ba25b..a166370574cebc 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -23,6 +23,7 @@ import sys from abc import ABC, abstractmethod +from collections import deque from dataclasses import dataclass, field from _pyrepl.console import Event, Console from .trace import trace @@ -211,7 +212,7 @@ def __init__( else: self.output_fd = f_out.fileno() - self.event_queue: deque[Event] = [] + self.event_queue: deque[Event] = deque() def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: @@ -231,7 +232,6 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.__write("\n") self.__posxy = 0, len(self.screen) self.screen.append("") - trace(f"... {self.__posxy} {len(self.screen)} {self.height}") px, py = self.__posxy old_offset = offset = self.__offset @@ -245,36 +245,22 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: offset = cy - height + 1 scroll_lines = offset - old_offset - trace(f'adj offset {cy} {height} {offset} {old_offset}') # Scrolling the buffer as the current input is greater than the visible # portion of the window. We need to scroll the visible portion and the # entire history - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - - bottom = info.srWindow.Bottom - - trace("Scrolling {} {} {} {} {}", scroll_lines, info.srWindow.Bottom, self.height, len(screen) - self.height, self.__posxy) - self.scroll(scroll_lines, bottom) + self.scroll(scroll_lines, self.getscrollbacksize()) self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines self.__offset += scroll_lines for i in range(scroll_lines): - self.screen.append("") - + self.screen.append("") elif offset > 0 and len(screen) < offset + height: - trace("Adding extra line") offset = max(len(screen) - height, 0) screen.append("") oldscr = self.screen[old_offset : old_offset + height] newscr = screen[offset : offset + height] - trace('old screen {}', oldscr) - trace('new screen {}', newscr) - - trace('new offset {} {}', offset, px) self.__offset = offset self.__hide_cursor() @@ -362,52 +348,13 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): x_coord += wlen(newline[x_pos]) x_pos += 1 - # if we need to insert a single character right after the first detected change - screen_cord = self.screen_xy - if oldline[x_pos:] == newline[x_pos + 1 :]: - trace('insert single {} {}', y, self.__posxy) - if ( - y == self.__posxy[1] - and x_coord > self.__posxy[0] - and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1] - ): - x_pos = px_pos - x_coord = px_coord - - character_width = wlen(newline[x_pos]) - - # Scroll any text to the right if we're inserting - ins_x, ins_y = self.get_abs_position(x_coord + 1, y) - ins_x -= 1 - scroll_rect = SMALL_RECT() - scroll_rect.Top = scroll_rect.Bottom = SHORT(ins_y) - scroll_rect.Left = SHORT(ins_x) - scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1) - destination_origin = _COORD(X = scroll_rect.Left + 1, Y = scroll_rect.Top) - fill_info = CHAR_INFO() - fill_info.UnicodeChar = ' ' - - if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): - raise ctypes.WinError(ctypes.GetLastError()) - - self.__move_relative(x_coord, y) - pos = self.__posxy - coord = self.screen_xy - - self.__write(newline[x_pos]) - self.__posxy = x_coord + character_width, y - else: - trace("Rewrite all {!r} x_coord={} {} y={} {} posxy={} screen_xy={}", newline, x_coord, len(oldline), y, wlen(newline), self.__posxy, self.screen_xy) - self.__hide_cursor() - self.__move_relative(x_coord, y) - pos = self.__posxy - coord = self.screen_xy - if wlen(oldline) > wlen(newline): - self.erase_to_end() - - self.__write(newline[x_pos:]) - self.__posxy = wlen(newline), y + self.__hide_cursor() + self.__move_relative(x_coord, y) + if wlen(oldline) > wlen(newline): + self.erase_to_end() + self.__write(newline[x_pos:]) + self.__posxy = wlen(newline), y if "\x1b" in newline or y != self.__posxy[1]: # ANSI escape characters are present, so we can't assume @@ -489,6 +436,13 @@ def getheightwidth(self) -> tuple[int, int]: return (info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1) + def getscrollbacksize(self) -> int: + info = CONSOLE_SCREEN_BUFFER_INFO() + if not GetConsoleScreenBufferInfo(OutHandle, info): + raise ctypes.WinError(ctypes.GetLastError()) + + return info.srWindow.Bottom + def __read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() read = DWORD() From 70a9a31a9712eea2257cc2e4c7cc1fff35493d24 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 15:02:23 -0700 Subject: [PATCH 43/66] Code cleanup --- Lib/_pyrepl/windows_console.py | 352 ++++++++++++++++----------------- 1 file changed, 165 insertions(+), 187 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index a166370574cebc..21cf58ba10a5b2 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -29,7 +29,7 @@ from .trace import trace import ctypes from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR, SHORT -from ctypes import Structure, POINTER, Union +from ctypes import Structure, POINTER, Union, WinDLL from ctypes import windll from typing import TYPE_CHECKING from .utils import wlen @@ -37,122 +37,6 @@ if TYPE_CHECKING: from typing import IO -class CONSOLE_SCREEN_BUFFER_INFO(Structure): - _fields_ = [ - ('dwSize', _COORD), - ('dwCursorPosition', _COORD), - ('wAttributes', WORD), - ('srWindow', SMALL_RECT), - ('dwMaximumWindowSize', _COORD), - ] - -class CONSOLE_CURSOR_INFO(Structure): - _fields_ = [ - ('dwSize', DWORD), - ('bVisible', BOOL), - ] - -class CHAR_INFO(Structure): - _fields_ = [ - ('UnicodeChar', WCHAR), - ('Attributes', WORD), - ] - -STD_INPUT_HANDLE = -10 -STD_OUTPUT_HANDLE = -11 -GetStdHandle = windll.kernel32.GetStdHandle -GetStdHandle.argtypes = [ctypes.wintypes.DWORD] -GetStdHandle.restype = ctypes.wintypes.HANDLE - -GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo -GetConsoleScreenBufferInfo.use_last_error = True -GetConsoleScreenBufferInfo.argtypes = [HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] -GetConsoleScreenBufferInfo.restype = BOOL - -SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo -SetConsoleCursorInfo.use_last_error = True -SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -SetConsoleCursorInfo.restype = BOOL - -GetConsoleCursorInfo = windll.kernel32.GetConsoleCursorInfo -GetConsoleCursorInfo.use_last_error = True -GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -GetConsoleCursorInfo.restype = BOOL - -FillConsoleOutputCharacter = windll.kernel32.FillConsoleOutputCharacterW -FillConsoleOutputCharacter.use_last_error = True -FillConsoleOutputCharacter.argtypes = [HANDLE, CHAR, DWORD, _COORD, POINTER(DWORD)] -FillConsoleOutputCharacter.restype = BOOL - -FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute -FillConsoleOutputAttribute.use_last_error = True -FillConsoleOutputAttribute.argtypes = [HANDLE, WORD, DWORD, _COORD, POINTER(DWORD)] -FillConsoleOutputAttribute.restype = BOOL - -ScrollConsoleScreenBuffer = windll.kernel32.ScrollConsoleScreenBufferW -ScrollConsoleScreenBuffer.use_last_error = True -ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] -ScrollConsoleScreenBuffer.restype = BOOL - -SetConsoleMode = windll.kernel32.SetConsoleMode -SetConsoleMode.use_last_error = True -SetConsoleMode.argtypes = [HANDLE, DWORD] -SetConsoleMode.restype = BOOL - -class Char(Union): - _fields_ = [ - ("UnicodeChar",WCHAR), - ("Char", CHAR), - ] - -class KeyEvent(ctypes.Structure): - _fields_ = [ - ("bKeyDown", BOOL), - ("wRepeatCount", WORD), - ("wVirtualKeyCode", WORD), - ("wVirtualScanCode", WORD), - ("uChar", Char), - ("dwControlKeyState", DWORD), - ] - -class WindowsBufferSizeEvent(ctypes.Structure): - _fields_ = [ - ('dwSize', _COORD) - ] - - -class ConsoleEvent(ctypes.Union): - _fields_ = [ - ("KeyEvent", KeyEvent), -# ("MouseEvent", ), - ("WindowsBufferSizeEvent", WindowsBufferSizeEvent), -# ("MenuEvent", ) -# ("FocusEvent", ) - ] - - -KEY_EVENT = 0x01 -FOCUS_EVENT = 0x10 -MENU_EVENT = 0x08 -MOUSE_EVENT = 0x02 -WINDOW_BUFFER_SIZE_EVENT = 0x04 - -class INPUT_RECORD(Structure): - _fields_ = [ - ("EventType", WORD), - ("Event", ConsoleEvent) - ] - -ReadConsoleInput = windll.kernel32.ReadConsoleInputW -ReadConsoleInput.use_last__error = True -ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] -ReadConsoleInput.restype = BOOL - - - -OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) -InHandle = GetStdHandle(STD_INPUT_HANDLE) - VK_MAP: dict[int, str] = { 0x23: "end", # VK_END 0x24: "home", # VK_HOME @@ -183,9 +67,13 @@ class INPUT_RECORD(Structure): 0x82: "f20", # VK_F20 } -ENABLE_PROCESSED_OUTPUT = 0x01 -ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 -ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 +# Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences +ERASE_IN_LINE = "\x1b[K" +MOVE_LEFT = "\x1b{}D" +MOVE_RIGHT = "\x1b{}C" +MOVE_UP = "\x1b{}A" +MOVE_DOWN = "\x1b{}B" +CLEAR = "\x1b[H\x1b[J" class _error(Exception): pass @@ -284,6 +172,43 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.screen = screen self.move_cursor(cx, cy) + def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): + # this is frustrating; there's no reason to test (say) + # self.dch1 inside the loop -- but alternative ways of + # structuring this function are equally painful (I'm trying to + # avoid writing code generators these days...) + minlen = min(wlen(oldline), wlen(newline)) + x_pos = 0 + x_coord = 0 + + px_pos = 0 + j = 0 + for c in oldline: + if j >= px_coord: break + j += wlen(c) + px_pos += 1 + + # reuse the oldline as much as possible, but stop as soon as we + # encounter an ESCAPE, because it might be the start of an escape + # sequene + while x_coord < minlen and oldline[x_pos] == newline[x_pos] and newline[x_pos] != "\x1b": + x_coord += wlen(newline[x_pos]) + x_pos += 1 + + self.__hide_cursor() + self.__move_relative(x_coord, y) + if wlen(oldline) > wlen(newline): + self.erase_to_end() + + self.__write(newline[x_pos:]) + self.__posxy = wlen(newline), y + + if "\x1b" in newline or y != self.__posxy[1]: + # ANSI escape characters are present, so we can't assume + # anything about the position of the cursor. Moving the cursor + # to the left margin should work to get to a known position. + self.move_cursor(0, y) + def scroll(self, top: int, bottom: int, left: int | None = None, right: int | None = None): scroll_rect = SMALL_RECT() scroll_rect.Top = SHORT(top) @@ -325,51 +250,8 @@ def screen_xy(self): raise ctypes.WinError(ctypes.GetLastError()) return info.dwCursorPosition.X, info.dwCursorPosition.Y - def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): - # this is frustrating; there's no reason to test (say) - # self.dch1 inside the loop -- but alternative ways of - # structuring this function are equally painful (I'm trying to - # avoid writing code generators these days...) - minlen = min(wlen(oldline), wlen(newline)) - x_pos = 0 - x_coord = 0 - - px_pos = 0 - j = 0 - for c in oldline: - if j >= px_coord: break - j += wlen(c) - px_pos += 1 - - # reuse the oldline as much as possible, but stop as soon as we - # encounter an ESCAPE, because it might be the start of an escape - # sequene - while x_coord < minlen and oldline[x_pos] == newline[x_pos] and newline[x_pos] != "\x1b": - x_coord += wlen(newline[x_pos]) - x_pos += 1 - - self.__hide_cursor() - self.__move_relative(x_coord, y) - if wlen(oldline) > wlen(newline): - self.erase_to_end() - - self.__write(newline[x_pos:]) - self.__posxy = wlen(newline), y - - if "\x1b" in newline or y != self.__posxy[1]: - # ANSI escape characters are present, so we can't assume - # anything about the position of the cursor. Moving the cursor - # to the left margin should work to get to a known position. - self.move_cursor(0, y) - def erase_to_end(self): - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - - size = info.srWindow.Right - info.srWindow.Left + 1 - info.dwCursorPosition.X - if not FillConsoleOutputCharacter(OutHandle, b' ', size, info.dwCursorPosition, DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) + self.__write(ERASE_IN_LINE) def prepare(self) -> None: trace("prepare") @@ -399,15 +281,14 @@ def __move_relative(self, x, y): dx = x - self.__posxy[0] dy = y - self.__posxy[1] if dx < 0: - os.write(self.output_fd, f"\x1b[{-dx}D".encode(self.encoding)) + self.__write(MOVE_LEFT.format(-dx)) elif dx > 0: - trace(f"Move console right {dx}") - os.write(self.output_fd, f"\x1b[{dx}C".encode(self.encoding)) + self.__write(MOVE_RIGHT.fomrat(dx)) if dy < 0: - os.write(self.output_fd, f"\x1b[{-dy}A".encode(self.encoding)) + self.__write(MOVE_UP.format(-dy)) elif dy > 0: - os.write(self.output_fd, f"\x1b[{dy}B".encode(self.encoding)) + self.__write(MOVE_DOWN.format(dy)) def move_cursor(self, x: int, y: int) -> None: trace(f'move_cursor {x} {y}') @@ -481,13 +362,10 @@ def get_event(self, block: bool = True) -> Event | None: if rec.Event.KeyEvent.uChar.UnicodeChar == '\r': # Make enter make unix-like - return Event(evt="key", data="\n", raw="\n") + return Event(evt="key", data="\n", raw=b"\n") elif rec.Event.KeyEvent.wVirtualKeyCode == 8: # Turn backspace directly into the command return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.UnicodeChar) - elif rec.Event.KeyEvent.wVirtualKeyCode == 27: - # Turn escape directly into the command - return Event(evt="key", data="escape", raw=rec.Event.KeyEvent.uChar.UnicodeChar) elif rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': # Handle special keys like arrow keys and translate them into the appropriate command code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) @@ -504,24 +382,17 @@ def push_char(self, char: int | bytes) -> None: """ Push a character to the console event queue. """ - trace(f'put_char {char}') + raise NotImplementedError("push_char not supported on Windows") - def beep(self) -> None: ... + def beep(self) -> None: + self.__write("\x07") def clear(self) -> None: """Wipe the screen""" - os.write(self.output_fd, f"\x1b[H\x1b[J".encode(self.encoding)) + self.__write(CLEAR) self.__posxy = 0, 0 self.screen = [""] - def clear_range(self, x: int, y: int, width: int, height: int) -> None: - size = width * height - start = _COORD(X = x, Y = y) - if not FillConsoleOutputCharacter(OutHandle, b' ', size, start, DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) - if not FillConsoleOutputAttribute(OutHandle, 0, size, start, DWORD()): - raise ctypes.WinError(ctypes.GetLastError()) - def finish(self) -> None: """Move the cursor to the end of the display and otherwise get ready for end. XXX could be merged with restore? Hmm.""" @@ -540,7 +411,8 @@ def flushoutput(self) -> None: def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" - ... + while self.__read_input() is not None: + pass def getpending(self) -> Event: """Return the characters that have been typed but not yet @@ -549,7 +421,113 @@ def getpending(self) -> Event: def wait(self) -> None: """Wait for an event.""" - ... + raise NotImplementedError("No wait support") def repaint(self) -> None: - trace('repaint') + raise NotImplementedError("No repaint support") + + + +# Windows interop +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [ + ('dwSize', _COORD), + ('dwCursorPosition', _COORD), + ('wAttributes', WORD), + ('srWindow', SMALL_RECT), + ('dwMaximumWindowSize', _COORD), + ] + +class CONSOLE_CURSOR_INFO(Structure): + _fields_ = [ + ('dwSize', DWORD), + ('bVisible', BOOL), + ] + +class CHAR_INFO(Structure): + _fields_ = [ + ('UnicodeChar', WCHAR), + ('Attributes', WORD), + ] + + +class Char(Union): + _fields_ = [ + ("UnicodeChar",WCHAR), + ("Char", CHAR), + ] + +class KeyEvent(ctypes.Structure): + _fields_ = [ + ("bKeyDown", BOOL), + ("wRepeatCount", WORD), + ("wVirtualKeyCode", WORD), + ("wVirtualScanCode", WORD), + ("uChar", Char), + ("dwControlKeyState", DWORD), + ] + +class WindowsBufferSizeEvent(ctypes.Structure): + _fields_ = [ + ('dwSize', _COORD) + ] + + +class ConsoleEvent(ctypes.Union): + _fields_ = [ + ("KeyEvent", KeyEvent), + ("WindowsBufferSizeEvent", WindowsBufferSizeEvent), + ] + + +KEY_EVENT = 0x01 +FOCUS_EVENT = 0x10 +MENU_EVENT = 0x08 +MOUSE_EVENT = 0x02 +WINDOW_BUFFER_SIZE_EVENT = 0x04 + +class INPUT_RECORD(Structure): + _fields_ = [ + ("EventType", WORD), + ("Event", ConsoleEvent) + ] + +STD_INPUT_HANDLE = -10 +STD_OUTPUT_HANDLE = -11 + +_KERNEL32 = WinDLL("kernel32", use_last_error=True) + +GetStdHandle = windll.kernel32.GetStdHandle +GetStdHandle.argtypes = [DWORD] +GetStdHandle.restype = HANDLE + +GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo +GetConsoleScreenBufferInfo.argtypes = [HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] +GetConsoleScreenBufferInfo.restype = BOOL + +SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo +SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +SetConsoleCursorInfo.restype = BOOL + +GetConsoleCursorInfo = _KERNEL32.GetConsoleCursorInfo +GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +GetConsoleCursorInfo.restype = BOOL + +ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW +ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] +ScrollConsoleScreenBuffer.restype = BOOL + +SetConsoleMode = _KERNEL32.SetConsoleMode +SetConsoleMode.argtypes = [HANDLE, DWORD] +SetConsoleMode.restype = BOOL + +ReadConsoleInput = _KERNEL32.ReadConsoleInputW +ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] +ReadConsoleInput.restype = BOOL + +OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) +InHandle = GetStdHandle(STD_INPUT_HANDLE) + +ENABLE_PROCESSED_OUTPUT = 0x01 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 \ No newline at end of file From 4adfe771e8419f25f02c893fbe6e7d4f2181fa1f Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 15:04:14 -0700 Subject: [PATCH 44/66] Reformat --- Lib/_pyrepl/windows_console.py | 215 ++++++++++++++++++++------------- 1 file changed, 133 insertions(+), 82 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 21cf58ba10a5b2..8ee4874f2d7ad5 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -28,7 +28,17 @@ from _pyrepl.console import Event, Console from .trace import trace import ctypes -from ctypes.wintypes import _COORD, WORD, SMALL_RECT, BOOL, HANDLE, CHAR, DWORD, WCHAR, SHORT +from ctypes.wintypes import ( + _COORD, + WORD, + SMALL_RECT, + BOOL, + HANDLE, + CHAR, + DWORD, + WCHAR, + SHORT, +) from ctypes import Structure, POINTER, Union, WinDLL from ctypes import windll from typing import TYPE_CHECKING @@ -38,33 +48,33 @@ from typing import IO VK_MAP: dict[int, str] = { - 0x23: "end", # VK_END - 0x24: "home", # VK_HOME - 0x25: "left", # VK_LEFT - 0x26: "up", # VK_UP - 0x27: "right", # VK_RIGHT - 0x28: "down", # VK_DOWN - 0x2E: "delete", # VK_DELETE - 0x70: "f1", # VK_F1 - 0x71: "f2", # VK_F2 - 0x72: "f3", # VK_F3 - 0x73: "f4", # VK_F4 - 0x74: "f5", # VK_F5 - 0x75: "f6", # VK_F6 - 0x76: "f7", # VK_F7 - 0x77: "f8", # VK_F8 - 0x78: "f9", # VK_F9 - 0x79: "f10", # VK_F10 - 0x7A: "f11", # VK_F11 - 0x7B: "f12", # VK_F12 - 0x7C: "f13", # VK_F13 - 0x7D: "f14", # VK_F14 - 0x7E: "f15", # VK_F15 - 0x7F: "f16", # VK_F16 - 0x79: "f17", # VK_F17 - 0x80: "f18", # VK_F18 - 0x81: "f19", # VK_F19 - 0x82: "f20", # VK_F20 + 0x23: "end", # VK_END + 0x24: "home", # VK_HOME + 0x25: "left", # VK_LEFT + 0x26: "up", # VK_UP + 0x27: "right", # VK_RIGHT + 0x28: "down", # VK_DOWN + 0x2E: "delete", # VK_DELETE + 0x70: "f1", # VK_F1 + 0x71: "f2", # VK_F2 + 0x72: "f3", # VK_F3 + 0x73: "f4", # VK_F4 + 0x74: "f5", # VK_F5 + 0x75: "f6", # VK_F6 + 0x76: "f7", # VK_F7 + 0x77: "f8", # VK_F8 + 0x78: "f9", # VK_F9 + 0x79: "f10", # VK_F10 + 0x7A: "f11", # VK_F11 + 0x7B: "f12", # VK_F12 + 0x7C: "f13", # VK_F13 + 0x7D: "f14", # VK_F14 + 0x7E: "f15", # VK_F15 + 0x7F: "f16", # VK_F16 + 0x79: "f17", # VK_F17 + 0x80: "f18", # VK_F18 + 0x81: "f19", # VK_F19 + 0x82: "f20", # VK_F20 } # Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences @@ -75,9 +85,11 @@ MOVE_DOWN = "\x1b{}B" CLEAR = "\x1b[H\x1b[J" + class _error(Exception): pass + class WindowsConsole(Console): def __init__( self, @@ -87,7 +99,12 @@ def __init__( encoding: str = "", ): - SetConsoleMode(OutHandle, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + SetConsoleMode( + OutHandle, + ENABLE_WRAP_AT_EOL_OUTPUT + | ENABLE_PROCESSED_OUTPUT + | ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) self.encoding = encoding or sys.getdefaultencoding() if isinstance(f_in, int): @@ -102,7 +119,6 @@ def __init__( self.event_queue: deque[Event] = deque() - def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ Refresh the console screen. @@ -113,7 +129,14 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ cx, cy = c_xy - trace('!!Refresh c_xy={} offset={} screen={} posxy={} screen_xy={}', c_xy, self.__offset, screen, self.__posxy, self.screen_xy) + trace( + "!!Refresh c_xy={} offset={} screen={} posxy={} screen_xy={}", + c_xy, + self.__offset, + screen, + self.__posxy, + self.screen_xy, + ) while len(self.screen) < min(len(screen), self.height): self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) @@ -139,9 +162,9 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.scroll(scroll_lines, self.getscrollbacksize()) self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines self.__offset += scroll_lines - + for i in range(scroll_lines): - self.screen.append("") + self.screen.append("") elif offset > 0 and len(screen) < offset + height: offset = max(len(screen) - height, 0) screen.append("") @@ -184,14 +207,19 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): px_pos = 0 j = 0 for c in oldline: - if j >= px_coord: break + if j >= px_coord: + break j += wlen(c) px_pos += 1 # reuse the oldline as much as possible, but stop as soon as we # encounter an ESCAPE, because it might be the start of an escape # sequene - while x_coord < minlen and oldline[x_pos] == newline[x_pos] and newline[x_pos] != "\x1b": + while ( + x_coord < minlen + and oldline[x_pos] == newline[x_pos] + and newline[x_pos] != "\x1b" + ): x_coord += wlen(newline[x_pos]) x_pos += 1 @@ -209,36 +237,42 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): # to the left margin should work to get to a known position. self.move_cursor(0, y) - def scroll(self, top: int, bottom: int, left: int | None = None, right: int | None = None): + def scroll( + self, top: int, bottom: int, left: int | None = None, right: int | None = None + ): scroll_rect = SMALL_RECT() - scroll_rect.Top = SHORT(top) + scroll_rect.Top = SHORT(top) scroll_rect.Bottom = SHORT(bottom) scroll_rect.Left = SHORT(0 if left is None else left) - scroll_rect.Right = SHORT(self.getheightwidth()[1] - 1 if right is None else right) + scroll_rect.Right = SHORT( + self.getheightwidth()[1] - 1 if right is None else right + ) destination_origin = _COORD() fill_info = CHAR_INFO() - fill_info.UnicodeChar = ' ' + fill_info.UnicodeChar = " " - if not ScrollConsoleScreenBuffer(OutHandle, scroll_rect, None, destination_origin, fill_info): + if not ScrollConsoleScreenBuffer( + OutHandle, scroll_rect, None, destination_origin, fill_info + ): raise ctypes.WinError(ctypes.GetLastError()) def __hide_cursor(self): info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise ctypes.WinError(ctypes.GetLastError()) info.bVisible = False if not SetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise ctypes.WinError(ctypes.GetLastError()) def __show_cursor(self): info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise ctypes.WinError(ctypes.GetLastError()) info.bVisible = True if not SetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise ctypes.WinError(ctypes.GetLastError()) def __write(self, text: str): os.write(self.output_fd, text.encode(self.encoding, "replace")) @@ -291,7 +325,7 @@ def __move_relative(self, x, y): self.__write(MOVE_DOWN.format(dy)) def move_cursor(self, x: int, y: int) -> None: - trace(f'move_cursor {x} {y}') + trace(f"move_cursor {x} {y}") if x < 0 or y < 0: raise ValueError(f"Bad cursor position {x}, {y}") @@ -314,14 +348,16 @@ def getheightwidth(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - return (info.srWindow.Bottom - info.srWindow.Top + 1, - info.srWindow.Right - info.srWindow.Left + 1) + return ( + info.srWindow.Bottom - info.srWindow.Top + 1, + info.srWindow.Right - info.srWindow.Left + 1, + ) def getscrollbacksize(self) -> int: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - + return info.srWindow.Bottom def __read_input(self) -> INPUT_RECORD | None: @@ -329,10 +365,10 @@ def __read_input(self) -> INPUT_RECORD | None: read = DWORD() if not ReadConsoleInput(InHandle, rec, 1, read): raise ctypes.WinError(ctypes.GetLastError()) - + if read.value == 0: return None - + return rec def get_event(self, block: bool = True) -> Event | None: @@ -341,14 +377,14 @@ def get_event(self, block: bool = True) -> Event | None: completion of an event.""" if self.event_queue: return self.event_queue.pop() - + while True: rec = self.__read_input() if rec is None: if block: continue return None - + if rec.EventType == WINDOW_BUFFER_SIZE_EVENT: return Event("resize", "") @@ -357,20 +393,26 @@ def get_event(self, block: bool = True) -> Event | None: if block: continue return None - + key = rec.Event.KeyEvent.uChar.UnicodeChar - - if rec.Event.KeyEvent.uChar.UnicodeChar == '\r': + + if rec.Event.KeyEvent.uChar.UnicodeChar == "\r": # Make enter make unix-like return Event(evt="key", data="\n", raw=b"\n") elif rec.Event.KeyEvent.wVirtualKeyCode == 8: # Turn backspace directly into the command - return Event(evt="key", data="backspace", raw=rec.Event.KeyEvent.uChar.UnicodeChar) - elif rec.Event.KeyEvent.uChar.UnicodeChar == '\x00': + return Event( + evt="key", + data="backspace", + raw=rec.Event.KeyEvent.uChar.UnicodeChar, + ) + elif rec.Event.KeyEvent.uChar.UnicodeChar == "\x00": # Handle special keys like arrow keys and translate them into the appropriate command code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode) if code: - return Event(evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar) + return Event( + evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar + ) if block: continue @@ -405,10 +447,10 @@ def finish(self) -> None: def flushoutput(self) -> None: """Flush all output to the screen (assuming there's some buffering going on somewhere). - + All output on Windows is unbuffered so this is a nop""" pass - + def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" while self.__read_input() is not None: @@ -427,36 +469,38 @@ def repaint(self) -> None: raise NotImplementedError("No repaint support") - # Windows interop class CONSOLE_SCREEN_BUFFER_INFO(Structure): _fields_ = [ - ('dwSize', _COORD), - ('dwCursorPosition', _COORD), - ('wAttributes', WORD), - ('srWindow', SMALL_RECT), - ('dwMaximumWindowSize', _COORD), + ("dwSize", _COORD), + ("dwCursorPosition", _COORD), + ("wAttributes", WORD), + ("srWindow", SMALL_RECT), + ("dwMaximumWindowSize", _COORD), ] + class CONSOLE_CURSOR_INFO(Structure): _fields_ = [ - ('dwSize', DWORD), - ('bVisible', BOOL), + ("dwSize", DWORD), + ("bVisible", BOOL), ] + class CHAR_INFO(Structure): _fields_ = [ - ('UnicodeChar', WCHAR), - ('Attributes', WORD), + ("UnicodeChar", WCHAR), + ("Attributes", WORD), ] class Char(Union): _fields_ = [ - ("UnicodeChar",WCHAR), + ("UnicodeChar", WCHAR), ("Char", CHAR), ] + class KeyEvent(ctypes.Structure): _fields_ = [ ("bKeyDown", BOOL), @@ -467,10 +511,9 @@ class KeyEvent(ctypes.Structure): ("dwControlKeyState", DWORD), ] + class WindowsBufferSizeEvent(ctypes.Structure): - _fields_ = [ - ('dwSize', _COORD) - ] + _fields_ = [("dwSize", _COORD)] class ConsoleEvent(ctypes.Union): @@ -486,11 +529,10 @@ class ConsoleEvent(ctypes.Union): MOUSE_EVENT = 0x02 WINDOW_BUFFER_SIZE_EVENT = 0x04 + class INPUT_RECORD(Structure): - _fields_ = [ - ("EventType", WORD), - ("Event", ConsoleEvent) - ] + _fields_ = [("EventType", WORD), ("Event", ConsoleEvent)] + STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 @@ -502,7 +544,10 @@ class INPUT_RECORD(Structure): GetStdHandle.restype = HANDLE GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo -GetConsoleScreenBufferInfo.argtypes = [HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] +GetConsoleScreenBufferInfo.argtypes = [ + HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), +] GetConsoleScreenBufferInfo.restype = BOOL SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo @@ -514,7 +559,13 @@ class INPUT_RECORD(Structure): GetConsoleCursorInfo.restype = BOOL ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW -ScrollConsoleScreenBuffer.argtypes = [HANDLE, POINTER(SMALL_RECT), POINTER(SMALL_RECT), _COORD, POINTER(CHAR_INFO)] +ScrollConsoleScreenBuffer.argtypes = [ + HANDLE, + POINTER(SMALL_RECT), + POINTER(SMALL_RECT), + _COORD, + POINTER(CHAR_INFO), +] ScrollConsoleScreenBuffer.restype = BOOL SetConsoleMode = _KERNEL32.SetConsoleMode @@ -529,5 +580,5 @@ class INPUT_RECORD(Structure): InHandle = GetStdHandle(STD_INPUT_HANDLE) ENABLE_PROCESSED_OUTPUT = 0x01 -ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 -ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 \ No newline at end of file +ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 From 9c00bd1667ccbaffcce829c364a42989c01ed743 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 15:15:31 -0700 Subject: [PATCH 45/66] Annotations --- Lib/_pyrepl/windows_console.py | 39 +++++++++++++--------------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 8ee4874f2d7ad5..2fcfcb1c702eb2 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -79,10 +79,10 @@ # Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences ERASE_IN_LINE = "\x1b[K" -MOVE_LEFT = "\x1b{}D" -MOVE_RIGHT = "\x1b{}C" -MOVE_UP = "\x1b{}A" -MOVE_DOWN = "\x1b{}B" +MOVE_LEFT = "\x1b[{}D" +MOVE_RIGHT = "\x1b[{}C" +MOVE_UP = "\x1b[{}A" +MOVE_DOWN = "\x1b[{}B" CLEAR = "\x1b[H\x1b[J" @@ -129,14 +129,6 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ cx, cy = c_xy - trace( - "!!Refresh c_xy={} offset={} screen={} posxy={} screen_xy={}", - c_xy, - self.__offset, - screen, - self.__posxy, - self.screen_xy, - ) while len(self.screen) < min(len(screen), self.height): self.__hide_cursor() self.__move_relative(0, len(self.screen) - 1) @@ -195,7 +187,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.screen = screen self.move_cursor(cx, cy) - def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): + def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord: int) -> None: # this is frustrating; there's no reason to test (say) # self.dch1 inside the loop -- but alternative ways of # structuring this function are equally painful (I'm trying to @@ -239,7 +231,7 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord): def scroll( self, top: int, bottom: int, left: int | None = None, right: int | None = None - ): + ) -> None: scroll_rect = SMALL_RECT() scroll_rect.Top = SHORT(top) scroll_rect.Bottom = SHORT(bottom) @@ -256,7 +248,7 @@ def scroll( ): raise ctypes.WinError(ctypes.GetLastError()) - def __hide_cursor(self): + def __hide_cursor(self) -> None: info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) @@ -265,7 +257,7 @@ def __hide_cursor(self): if not SetConsoleCursorInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - def __show_cursor(self): + def __show_cursor(self) -> None: info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) @@ -274,17 +266,17 @@ def __show_cursor(self): if not SetConsoleCursorInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) - def __write(self, text: str): + def __write(self, text: str) -> None: os.write(self.output_fd, text.encode(self.encoding, "replace")) @property - def screen_xy(self): + def screen_xy(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise ctypes.WinError(ctypes.GetLastError()) return info.dwCursorPosition.X, info.dwCursorPosition.Y - def erase_to_end(self): + def erase_to_end(self) -> None: self.__write(ERASE_IN_LINE) def prepare(self) -> None: @@ -300,7 +292,8 @@ def prepare(self) -> None: self.__gone_tall = 0 self.__offset = 0 - def restore(self) -> None: ... + def restore(self) -> None: + pass def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_x, cur_y = self.screen_xy @@ -310,14 +303,14 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_y += dy return cur_x, cur_y - def __move_relative(self, x, y): + def __move_relative(self, x, y) -> None: """Moves relative to the current __posxy""" dx = x - self.__posxy[0] dy = y - self.__posxy[1] if dx < 0: self.__write(MOVE_LEFT.format(-dx)) elif dx > 0: - self.__write(MOVE_RIGHT.fomrat(dx)) + self.__write(MOVE_RIGHT.format(dx)) if dy < 0: self.__write(MOVE_UP.format(-dy)) @@ -325,8 +318,6 @@ def __move_relative(self, x, y): self.__write(MOVE_DOWN.format(dy)) def move_cursor(self, x: int, y: int) -> None: - trace(f"move_cursor {x} {y}") - if x < 0 or y < 0: raise ValueError(f"Bad cursor position {x}, {y}") From dc7ec86136e3a512276e3f2a6b43fb072b480a01 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 15:18:03 -0700 Subject: [PATCH 46/66] Update news --- Doc/whatsnew/3.13.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 7edfdd4f8167a0..693d10de9a6862 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -177,7 +177,8 @@ the :envvar:`PYTHON_BASIC_REPL` environment variable. For more on interactive mode, see :ref:`tut-interac`. (Contributed by Pablo Galindo Salgado, Łukasz Langa, and -Lysandros Nikolaou in :gh:`111201` based on code from the PyPy project.) +Lysandros Nikolaou in :gh:`111201` based on code from the PyPy project. +Windows support contributed by Dino Viehland and Anthony Shaw.) .. _whatsnew313-improved-error-messages: From 79a3fb9332b1e4fe1babefc3d7055bdb64175764 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 15:24:06 -0700 Subject: [PATCH 47/66] Update history --- Doc/whatsnew/3.13.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 693d10de9a6862..9b2b4b4697b6a1 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -154,10 +154,10 @@ New Features A Better Interactive Interpreter -------------------------------- -On Unix-like systems like Linux or macOS, Python now uses a new -:term:`interactive` shell. When the user starts the :term:`REPL` from an -interactive terminal, and both :mod:`curses` and :mod:`readline` are -available, the interactive shell now supports the following new features: +On Unix-like systems like Linux or macOS as well as Windows, Python now +uses a new :term:`interactive` shell. When the user starts the +:term:`REPL` from an interactive terminal the interactive shell now +supports the following new features: * Colorized prompts. * Multiline editing with history preservation. @@ -174,6 +174,8 @@ available, the interactive shell now supports the following new features: If the new interactive shell is not desired, it can be disabled via the :envvar:`PYTHON_BASIC_REPL` environment variable. +The new shell requires :mod:`curses` on Unix-like systems. + For more on interactive mode, see :ref:`tut-interac`. (Contributed by Pablo Galindo Salgado, Łukasz Langa, and From 3e4fe557c723919471a9a881fca15c0e9bdf228c Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 24 May 2024 23:58:52 -0700 Subject: [PATCH 48/66] Initial test cases --- Lib/_pyrepl/windows_console.py | 22 +- Lib/test/test_pyrepl/__init__.py | 12 +- Lib/test/test_pyrepl/support.py | 2 +- Lib/test/test_pyrepl/test_pyrepl.py | 5 +- Lib/test/test_pyrepl/test_unix_console.py | 11 +- Lib/test/test_pyrepl/test_unix_eventqueue.py | 10 +- Lib/test/test_pyrepl/test_windows_console.py | 332 +++++++++++++++++++ 7 files changed, 370 insertions(+), 24 deletions(-) create mode 100644 Lib/test/test_pyrepl/test_windows_console.py diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 2fcfcb1c702eb2..9ed8e90bad11a0 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -221,13 +221,18 @@ def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord: int self.erase_to_end() self.__write(newline[x_pos:]) - self.__posxy = wlen(newline), y + if wlen(newline) == self.width: + # If we wrapped we want to start at the next line + self.__move_relative(0, y + 1) + self.__posxy = 0, y + 1 + else: + self.__posxy = wlen(newline), y - if "\x1b" in newline or y != self.__posxy[1]: - # ANSI escape characters are present, so we can't assume - # anything about the position of the cursor. Moving the cursor - # to the left margin should work to get to a known position. - self.move_cursor(0, y) + if "\x1b" in newline or y != self.__posxy[1]: + # ANSI escape characters are present, so we can't assume + # anything about the position of the cursor. Moving the cursor + # to the left margin should work to get to a known position. + self.move_cursor(0, y) def scroll( self, top: int, bottom: int, left: int | None = None, right: int | None = None @@ -267,6 +272,7 @@ def __show_cursor(self) -> None: raise ctypes.WinError(ctypes.GetLastError()) def __write(self, text: str) -> None: + print(repr(text)) os.write(self.output_fd, text.encode(self.encoding, "replace")) @property @@ -284,10 +290,6 @@ def prepare(self) -> None: self.screen = [] self.height, self.width = self.getheightwidth() - info = CONSOLE_SCREEN_BUFFER_INFO() - if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) - self.__posxy = 0, 0 self.__gone_tall = 0 self.__offset = 0 diff --git a/Lib/test/test_pyrepl/__init__.py b/Lib/test/test_pyrepl/__init__.py index fa38b86b847dd9..1b33819eb180b2 100644 --- a/Lib/test/test_pyrepl/__init__.py +++ b/Lib/test/test_pyrepl/__init__.py @@ -1,12 +1,14 @@ import os +import sys from test.support import requires, load_package_tests from test.support.import_helper import import_module -# Optionally test pyrepl. This currently requires that the -# 'curses' resource be given on the regrtest command line using the -u -# option. Additionally, we need to attempt to import curses and readline. -requires("curses") -curses = import_module("curses") +if sys.platform != "win32": + # Optionally test pyrepl. This currently requires that the + # 'curses' resource be given on the regrtest command line using the -u + # option. Additionally, we need to attempt to import curses and readline. + requires("curses") + curses = import_module("curses") def load_tests(*args): diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index 75539049d43c2a..d2f5429aea7a11 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -55,7 +55,7 @@ def get_prompt(lineno, cursor_on_line) -> str: return reader -def prepare_console(events: Iterable[Event], **kwargs): +def prepare_console(events: Iterable[Event], **kwargs) -> MagicMock | Console: console = MagicMock() console.get_event.side_effect = events console.height = 100 diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index bdcabf9be05b9e..aa2722095794c9 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -508,14 +508,15 @@ def prepare_reader(self, events, namespace): reader = ReadlineAlikeReader(console=console, config=config) return reader + @patch("rlcompleter._readline_available", False) def test_simple_completion(self): - events = code_to_events("os.geten\t\n") + events = code_to_events("os.getpid\t\n") namespace = {"os": os} reader = self.prepare_reader(events, namespace) output = multiline_input(reader, namespace) - self.assertEqual(output, "os.getenv") + self.assertEqual(output, "os.getpid()") def test_completion_with_many_options(self): # Test with something that initially displays many options diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index e1faa00caafc27..d0b98f17ade094 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -1,12 +1,16 @@ import itertools +import sys +import unittest from functools import partial from unittest import TestCase from unittest.mock import MagicMock, call, patch, ANY from .support import handle_all_events, code_to_events -from _pyrepl.console import Event -from _pyrepl.unix_console import UnixConsole - +try: + from _pyrepl.console import Event + from _pyrepl.unix_console import UnixConsole +except ImportError: + pass def unix_console(events, **kwargs): console = UnixConsole() @@ -67,6 +71,7 @@ def unix_console(events, **kwargs): } +@unittest.skipIf(sys.platform == "win32", "No Unix event queue on Windows") @patch("_pyrepl.curses.tigetstr", lambda s: TERM_CAPABILITIES.get(s)) @patch( "_pyrepl.curses.tparm", diff --git a/Lib/test/test_pyrepl/test_unix_eventqueue.py b/Lib/test/test_pyrepl/test_unix_eventqueue.py index c06536b4a86a04..301f79927a741f 100644 --- a/Lib/test/test_pyrepl/test_unix_eventqueue.py +++ b/Lib/test/test_pyrepl/test_unix_eventqueue.py @@ -1,11 +1,15 @@ import tempfile import unittest +import sys from unittest.mock import patch -from _pyrepl.console import Event -from _pyrepl.unix_eventqueue import EventQueue - +try: + from _pyrepl.console import Event + from _pyrepl.unix_eventqueue import EventQueue +except ImportError: + pass +@unittest.skipIf(sys.platform == "win32", "No Unix event queue on Windows") @patch("_pyrepl.curses.tigetstr", lambda x: b"") class TestUnixEventQueue(unittest.TestCase): def setUp(self): diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py new file mode 100644 index 00000000000000..24cc4017763a83 --- /dev/null +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -0,0 +1,332 @@ +import itertools +import sys +import unittest +from Lib._pyrepl.windows_console import ERASE_IN_LINE +from _pyrepl.console import Event, Console +from _pyrepl.windows_console import ( + MOVE_LEFT, + MOVE_RIGHT, + MOVE_UP, + MOVE_DOWN, + ERASE_IN_LINE, +) +from functools import partial +from typing import Iterable +from unittest import TestCase, main +from unittest.mock import MagicMock, call, patch, ANY + +from .support import handle_all_events, code_to_events + +try: + from _pyrepl.console import Event + from _pyrepl.windows_console import WindowsConsole +except ImportError: + pass + + +@patch("os.write") +@unittest.skipIf(sys.platform != "win32", "No Unix event queue on Windows") +class WindowsConsoleTests(TestCase): + def console(self, events, **kwargs) -> Console: + console = WindowsConsole() + console.get_event = MagicMock(side_effect=events) + console.scroll = MagicMock() + console.__hide_cursor = MagicMock() + console.__show_cursor = MagicMock() + console.getscrollbacksize = MagicMock(42) + + height = kwargs.get("height", 25) + width = kwargs.get("width", 80) + console.getheightwidth = MagicMock(side_effect=lambda: (height, width)) + + console.prepare() + for key, val in kwargs.items(): + setattr(console, key, val) + return console + + def handle_events(self, events: Iterable[Event], **kwargs): + return handle_all_events(events, partial(self.console, **kwargs)) + + def handle_events_narrow(self, events): + return self.handle_events(events, width=5) + + def handle_events_short(self, events): + return self.handle_events(events, height=1) + + def handle_events_height_3(self, events): + return self.handle_events(events, height=3) + + def test_simple_addition(self, _os_write): + code = "12+34" + events = code_to_events(code) + _, con = self.handle_events(events) + _os_write.assert_any_call(ANY, b"1") + _os_write.assert_any_call(ANY, b"2") + _os_write.assert_any_call(ANY, b"+") + _os_write.assert_any_call(ANY, b"3") + _os_write.assert_any_call(ANY, b"4") + con.restore() + + def test_wrap(self, _os_write): + code = "12+34" + events = code_to_events(code) + _, con = self.handle_events_narrow(events) + _os_write.assert_any_call(ANY, b"1") + _os_write.assert_any_call(ANY, b"2") + _os_write.assert_any_call(ANY, b"+") + _os_write.assert_any_call(ANY, b"3") + _os_write.assert_any_call(ANY, b"\\") + _os_write.assert_any_call(ANY, b"\n") + _os_write.assert_any_call(ANY, b"4") + con.restore() + + def test_resize_wider(self, _os_write): + code = "1234567890" + events = code_to_events(code) + reader, console = self.handle_events_narrow(events) + + console.height = 20 + console.width = 80 + console.getheightwidth = MagicMock(lambda _: (20, 80)) + + def same_reader(_): + return reader + + def same_console(events): + console.get_event = MagicMock(side_effect=events) + return console + + _, con = handle_all_events( + [Event(evt="resize", data=None)], + prepare_reader=same_reader, + prepare_console=same_console, + ) + + _os_write.assert_any_call(ANY, self.move_right(2)) + _os_write.assert_any_call(ANY, self.move_up(2)) + _os_write.assert_any_call(ANY, b"567890") + + con.restore() + + def test_resize_narrower(self, _os_write): + code = "1234567890" + events = code_to_events(code) + reader, console = self.handle_events(events) + + console.height = 20 + console.width = 4 + console.getheightwidth = MagicMock(lambda _: (20, 4)) + + def same_reader(_): + return reader + + def same_console(events): + console.get_event = MagicMock(side_effect=events) + return console + + _, con = handle_all_events( + [Event(evt="resize", data=None)], + prepare_reader=same_reader, + prepare_console=same_console, + ) + + _os_write.assert_any_call(ANY, b"456\\") + _os_write.assert_any_call(ANY, b"789\\") + + con.restore() + + def test_cursor_left(self, _os_write): + code = "1" + events = itertools.chain( + code_to_events(code), + [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))], + ) + _, con = self.handle_events(events) + _os_write.assert_any_call(ANY, self.move_left()) + con.restore() + + def test_cursor_left_right(self, _os_write): + code = "1" + events = itertools.chain( + code_to_events(code), + [ + Event(evt="key", data="left", raw=bytearray(b"\x1bOD")), + Event(evt="key", data="right", raw=bytearray(b"\x1bOC")), + ], + ) + _, con = self.handle_events(events) + _os_write.assert_any_call(ANY, self.move_left()) + _os_write.assert_any_call(ANY, self.move_right()) + con.restore() + + def test_cursor_up(self, _os_write): + code = "1\n2+3" + events = itertools.chain( + code_to_events(code), + [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))], + ) + _, con = self.handle_events(events) + _os_write.assert_any_call(ANY, self.move_up()) + con.restore() + + def test_cursor_up_down(self, _os_write): + code = "1\n2+3" + events = itertools.chain( + code_to_events(code), + [ + Event(evt="key", data="up", raw=bytearray(b"\x1bOA")), + Event(evt="key", data="down", raw=bytearray(b"\x1bOB")), + ], + ) + _, con = self.handle_events(events) + _os_write.assert_any_call(ANY, self.move_up()) + _os_write.assert_any_call(ANY, self.move_down()) + con.restore() + + def test_cursor_back_write(self, _os_write): + events = itertools.chain( + code_to_events("1"), + [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))], + code_to_events("2"), + ) + _, con = self.handle_events(events) + _os_write.assert_any_call(ANY, b"1") + _os_write.assert_any_call(ANY, self.move_left()) + _os_write.assert_any_call(ANY, b"21") + con.restore() + + def test_multiline_function_move_up_short_terminal(self, _os_write): + # fmt: off + code = ( + "def f():\n" + " foo" + ) + # fmt: on + + events = itertools.chain( + code_to_events(code), + [ + Event(evt="key", data="up", raw=bytearray(b"\x1bOA")), + Event(evt="scroll", data=None), + ], + ) + _, con = self.handle_events_short(events) + _os_write.assert_any_call(ANY, self.move_left(5)) + _os_write.assert_any_call(ANY, self.move_up()) + con.restore() + + def test_multiline_function_move_up_down_short_terminal(self, _os_write): + # fmt: off + code = ( + "def f():\n" + " foo" + ) + # fmt: on + + events = itertools.chain( + code_to_events(code), + [ + Event(evt="key", data="up", raw=bytearray(b"\x1bOA")), + Event(evt="scroll", data=None), + Event(evt="key", data="down", raw=bytearray(b"\x1bOB")), + Event(evt="scroll", data=None), + ], + ) + _, con = self.handle_events_short(events) + _os_write.assert_any_call(ANY, self.move_left(8)) + _os_write.assert_any_call(ANY, self.erase_in_line()) + con.restore() + + def test_resize_bigger_on_multiline_function(self, _os_write): + # fmt: off + code = ( + "def f():\n" + " foo" + ) + # fmt: on + + events = itertools.chain(code_to_events(code)) + reader, console = self.handle_events_short(events) + + console.height = 2 + console.getheightwidth = MagicMock(lambda _: (2, 80)) + + def same_reader(_): + return reader + + def same_console(events): + console.get_event = MagicMock(side_effect=events) + return console + + _, con = handle_all_events( + [Event(evt="resize", data=None)], + prepare_reader=same_reader, + prepare_console=same_console, + ) + _os_write.assert_has_calls( + [ + call(ANY, self.move_left(5)), + call(ANY, self.move_up()), + call(ANY, b"def f():"), + call(ANY, self.move_left(3)), + call(ANY, self.move_down()), + ] + ) + console.restore() + con.restore() + + def test_resize_smaller_on_multiline_function(self, _os_write): + # fmt: off + code = ( + "def f():\n" + " foo" + ) + # fmt: on + + events = itertools.chain(code_to_events(code)) + reader, console = self.handle_events_height_3(events) + + console.height = 1 + console.getheightwidth = MagicMock(lambda _: (1, 80)) + + def same_reader(_): + return reader + + def same_console(events): + console.get_event = MagicMock(side_effect=events) + return console + + _, con = handle_all_events( + [Event(evt="resize", data=None)], + prepare_reader=same_reader, + prepare_console=same_console, + ) + _os_write.assert_has_calls( + [ + call(ANY, self.move_left(5)), + call(ANY, self.move_up()), + call(ANY, self.erase_in_line()), + call(ANY, b" foo"), + ] + ) + console.restore() + con.restore() + + def move_up(self, lines=1): + return MOVE_UP.format(lines).encode("utf8") + + def move_down(self, lines=1): + return MOVE_DOWN.format(lines).encode("utf8") + + def move_left(self, cols=1): + return MOVE_LEFT.format(cols).encode("utf8") + + def move_right(self, cols=1): + return MOVE_RIGHT.format(cols).encode("utf8") + + def erase_in_line(self): + return ERASE_IN_LINE.encode("utf8") + + +if __name__ == "__main__": + unittest.main() From 38800e72ccc542d1f24c570c27135864ccfef686 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 25 May 2024 18:43:13 +0000 Subject: [PATCH 49/66] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst diff --git a/Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst b/Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst new file mode 100644 index 00000000000000..f3918ed633d78c --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst @@ -0,0 +1 @@ +Add support for new pyrepl on Windows From b3f7092525cb4c68858b98a8d32ab6d6154d5c44 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 11:59:11 -0700 Subject: [PATCH 50/66] Fix mypy and formatting issues --- Doc/whatsnew/3.13.rst | 6 +-- Lib/_pyrepl/windows_console.py | 39 +++++++++++++++----- Lib/test/test_pyrepl/test_windows_console.py | 27 +++++--------- 3 files changed, 41 insertions(+), 31 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 9b2b4b4697b6a1..3fecc08b5fa8b3 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -154,9 +154,9 @@ New Features A Better Interactive Interpreter -------------------------------- -On Unix-like systems like Linux or macOS as well as Windows, Python now -uses a new :term:`interactive` shell. When the user starts the -:term:`REPL` from an interactive terminal the interactive shell now +On Unix-like systems like Linux or macOS as well as Windows, Python now +uses a new :term:`interactive` shell. When the user starts the +:term:`REPL` from an interactive terminal the interactive shell now supports the following new features: * Colorized prompts. diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 9ed8e90bad11a0..a77776d200b55d 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -39,11 +39,26 @@ WCHAR, SHORT, ) -from ctypes import Structure, POINTER, Union, WinDLL +from ctypes import Structure, POINTER, Union from ctypes import windll from typing import TYPE_CHECKING from .utils import wlen +try: + from ctypes import GetLastError, WinError, WinDLL, windll +except: + # Keep MyPy happy off Windows + from ctypes import CDLL as WinDLL, cdll as windll + + def GetLastError() -> int: + return 42 + + class WinError(OSError): + def __init__(self, err: int|None, descr: str|None = None) -> None: + self.err = err + self.descr = descr + + if TYPE_CHECKING: from typing import IO @@ -117,6 +132,10 @@ def __init__( else: self.output_fd = f_out.fileno() + self.screen: List[str] = [] + self.width = 80 + self.height = 25 + self.__offset = 0 self.event_queue: deque[Event] = deque() def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: @@ -251,25 +270,25 @@ def scroll( if not ScrollConsoleScreenBuffer( OutHandle, scroll_rect, None, destination_origin, fill_info ): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) def __hide_cursor(self) -> None: info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) info.bVisible = False if not SetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) def __show_cursor(self) -> None: info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) info.bVisible = True if not SetConsoleCursorInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) def __write(self, text: str) -> None: print(repr(text)) @@ -279,7 +298,7 @@ def __write(self, text: str) -> None: def screen_xy(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) return info.dwCursorPosition.X, info.dwCursorPosition.Y def erase_to_end(self) -> None: @@ -340,7 +359,7 @@ def getheightwidth(self) -> tuple[int, int]: and width of the terminal window in characters.""" info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) return ( info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1, @@ -349,7 +368,7 @@ def getheightwidth(self) -> tuple[int, int]: def getscrollbacksize(self) -> int: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) return info.srWindow.Bottom @@ -357,7 +376,7 @@ def __read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() read = DWORD() if not ReadConsoleInput(InHandle, rec, 1, read): - raise ctypes.WinError(ctypes.GetLastError()) + raise WinError(GetLastError()) if read.value == 0: return None diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 24cc4017763a83..6b8c5e3551aa6c 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -1,15 +1,8 @@ import itertools import sys import unittest -from Lib._pyrepl.windows_console import ERASE_IN_LINE from _pyrepl.console import Event, Console -from _pyrepl.windows_console import ( - MOVE_LEFT, - MOVE_RIGHT, - MOVE_UP, - MOVE_DOWN, - ERASE_IN_LINE, -) +from _pyrepl.windows_console import MOVE_LEFT, MOVE_RIGHT, MOVE_UP, MOVE_DOWN, ERASE_IN_LINE from functools import partial from typing import Iterable from unittest import TestCase, main @@ -23,7 +16,6 @@ except ImportError: pass - @patch("os.write") @unittest.skipIf(sys.platform != "win32", "No Unix event queue on Windows") class WindowsConsoleTests(TestCase): @@ -46,16 +38,16 @@ def console(self, events, **kwargs) -> Console: def handle_events(self, events: Iterable[Event], **kwargs): return handle_all_events(events, partial(self.console, **kwargs)) - + def handle_events_narrow(self, events): return self.handle_events(events, width=5) - + def handle_events_short(self, events): return self.handle_events(events, height=1) - + def handle_events_height_3(self, events): return self.handle_events(events, height=3) - + def test_simple_addition(self, _os_write): code = "12+34" events = code_to_events(code) @@ -130,8 +122,8 @@ def same_console(events): prepare_console=same_console, ) - _os_write.assert_any_call(ANY, b"456\\") - _os_write.assert_any_call(ANY, b"789\\") + _os_write.assert_any_call(ANY, b'456\\') + _os_write.assert_any_call(ANY, b'789\\') con.restore() @@ -323,10 +315,9 @@ def move_left(self, cols=1): def move_right(self, cols=1): return MOVE_RIGHT.format(cols).encode("utf8") - + def erase_in_line(self): return ERASE_IN_LINE.encode("utf8") - if __name__ == "__main__": - unittest.main() + unittest.main() \ No newline at end of file From a5893219e61071ac0d404a8d65510d107d75c8f7 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 12:07:36 -0700 Subject: [PATCH 51/66] More mypy fixes --- Lib/_pyrepl/windows_console.py | 20 ++++++++------- Lib/test/test_pyrepl/test_windows_console.py | 26 +++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index a77776d200b55d..fb918f7524b9d1 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -25,8 +25,6 @@ from abc import ABC, abstractmethod from collections import deque from dataclasses import dataclass, field -from _pyrepl.console import Event, Console -from .trace import trace import ctypes from ctypes.wintypes import ( _COORD, @@ -42,23 +40,25 @@ from ctypes import Structure, POINTER, Union from ctypes import windll from typing import TYPE_CHECKING +from .console import Event, Console +from .trace import trace from .utils import wlen try: - from ctypes import GetLastError, WinError, WinDLL, windll + from ctypes import GetLastError, WinDLL, windll except: # Keep MyPy happy off Windows from ctypes import CDLL as WinDLL, cdll as windll def GetLastError() -> int: return 42 - + class WinError(OSError): - def __init__(self, err: int|None, descr: str|None = None) -> None: + def __init__(self, err: int | None, descr: str | None = None) -> None: self.err = err self.descr = descr - - + + if TYPE_CHECKING: from typing import IO @@ -132,7 +132,7 @@ def __init__( else: self.output_fd = f_out.fileno() - self.screen: List[str] = [] + self.screen: list[str] = [] self.width = 80 self.height = 25 self.__offset = 0 @@ -206,7 +206,9 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.screen = screen self.move_cursor(cx, cy) - def __write_changed_line(self, y: int, oldline: str, newline: str, px_coord: int) -> None: + def __write_changed_line( + self, y: int, oldline: str, newline: str, px_coord: int + ) -> None: # this is frustrating; there's no reason to test (say) # self.dch1 inside the loop -- but alternative ways of # structuring this function are equally painful (I'm trying to diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 6b8c5e3551aa6c..e29ed0eab346c2 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -2,7 +2,13 @@ import sys import unittest from _pyrepl.console import Event, Console -from _pyrepl.windows_console import MOVE_LEFT, MOVE_RIGHT, MOVE_UP, MOVE_DOWN, ERASE_IN_LINE +from _pyrepl.windows_console import ( + MOVE_LEFT, + MOVE_RIGHT, + MOVE_UP, + MOVE_DOWN, + ERASE_IN_LINE, +) from functools import partial from typing import Iterable from unittest import TestCase, main @@ -16,6 +22,7 @@ except ImportError: pass + @patch("os.write") @unittest.skipIf(sys.platform != "win32", "No Unix event queue on Windows") class WindowsConsoleTests(TestCase): @@ -38,16 +45,16 @@ def console(self, events, **kwargs) -> Console: def handle_events(self, events: Iterable[Event], **kwargs): return handle_all_events(events, partial(self.console, **kwargs)) - + def handle_events_narrow(self, events): return self.handle_events(events, width=5) - + def handle_events_short(self, events): return self.handle_events(events, height=1) - + def handle_events_height_3(self, events): return self.handle_events(events, height=3) - + def test_simple_addition(self, _os_write): code = "12+34" events = code_to_events(code) @@ -122,8 +129,8 @@ def same_console(events): prepare_console=same_console, ) - _os_write.assert_any_call(ANY, b'456\\') - _os_write.assert_any_call(ANY, b'789\\') + _os_write.assert_any_call(ANY, b"456\\") + _os_write.assert_any_call(ANY, b"789\\") con.restore() @@ -315,9 +322,10 @@ def move_left(self, cols=1): def move_right(self, cols=1): return MOVE_RIGHT.format(cols).encode("utf8") - + def erase_in_line(self): return ERASE_IN_LINE.encode("utf8") + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 9a34d8a61a6a230868654e542b96da274bbe7cf0 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 13:38:40 -0700 Subject: [PATCH 52/66] More MyPy --- Lib/_pyrepl/readline.py | 4 ++ Lib/_pyrepl/simple_interact.py | 3 +- Lib/_pyrepl/windows_console.py | 81 +++++++++++++++++----------------- 3 files changed, 47 insertions(+), 41 deletions(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index c7430be443b9bf..eceafb95ddde2b 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -38,6 +38,10 @@ from . import commands, historical_reader from .completing_reader import CompletingReader +from .console import Console as ConsoleType + +Console: type[ConsoleType] +error: tuple[type[Exception], ...] | type[Exception] try: from .unix_console import UnixConsole as Console, _error except: diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index becdc119b6354e..fdc7c2863dcb5b 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -35,10 +35,11 @@ from .readline import _get_reader, multiline_input +error: tuple[type[Exception], ...] | type[Exception] try: from .unix_console import _error except ModuleNotFoundError: - _error = OSError + from .windows_console import _error def check() -> str: """Returns the error message if there is a problem initializing the state.""" diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index fb918f7524b9d1..8dfd9397309de9 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -38,13 +38,13 @@ SHORT, ) from ctypes import Structure, POINTER, Union -from ctypes import windll -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from .console import Event, Console from .trace import trace from .utils import wlen try: + # type: ignore from ctypes import GetLastError, WinDLL, windll except: # Keep MyPy happy off Windows @@ -326,7 +326,7 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_y += dy return cur_x, cur_y - def __move_relative(self, x, y) -> None: + def __move_relative(self, x: int, y: int) -> None: """Moves relative to the current __posxy""" dx = x - self.__posxy[0] dy = y - self.__posxy[1] @@ -372,7 +372,7 @@ def getscrollbacksize(self) -> int: if not GetConsoleScreenBufferInfo(OutHandle, info): raise WinError(GetLastError()) - return info.srWindow.Bottom + return cast(int, info.srWindow.Bottom) def __read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() @@ -551,47 +551,48 @@ class INPUT_RECORD(Structure): STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 -_KERNEL32 = WinDLL("kernel32", use_last_error=True) +if sys.platform == "win32": + _KERNEL32 = WinDLL("kernel32", use_last_error=True) -GetStdHandle = windll.kernel32.GetStdHandle -GetStdHandle.argtypes = [DWORD] -GetStdHandle.restype = HANDLE + GetStdHandle = windll.kernel32.GetStdHandle + GetStdHandle.argtypes = [DWORD] + GetStdHandle.restype = HANDLE -GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo -GetConsoleScreenBufferInfo.argtypes = [ - HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), -] -GetConsoleScreenBufferInfo.restype = BOOL - -SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo -SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -SetConsoleCursorInfo.restype = BOOL - -GetConsoleCursorInfo = _KERNEL32.GetConsoleCursorInfo -GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -GetConsoleCursorInfo.restype = BOOL - -ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW -ScrollConsoleScreenBuffer.argtypes = [ - HANDLE, - POINTER(SMALL_RECT), - POINTER(SMALL_RECT), - _COORD, - POINTER(CHAR_INFO), -] -ScrollConsoleScreenBuffer.restype = BOOL + GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo + GetConsoleScreenBufferInfo.argtypes = [ + HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), + ] + GetConsoleScreenBufferInfo.restype = BOOL + + SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo + SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] + SetConsoleCursorInfo.restype = BOOL + + GetConsoleCursorInfo = _KERNEL32.GetConsoleCursorInfo + GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] + GetConsoleCursorInfo.restype = BOOL + + ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW + ScrollConsoleScreenBuffer.argtypes = [ + HANDLE, + POINTER(SMALL_RECT), + POINTER(SMALL_RECT), + _COORD, + POINTER(CHAR_INFO), + ] + ScrollConsoleScreenBuffer.restype = BOOL -SetConsoleMode = _KERNEL32.SetConsoleMode -SetConsoleMode.argtypes = [HANDLE, DWORD] -SetConsoleMode.restype = BOOL + SetConsoleMode = _KERNEL32.SetConsoleMode + SetConsoleMode.argtypes = [HANDLE, DWORD] + SetConsoleMode.restype = BOOL -ReadConsoleInput = _KERNEL32.ReadConsoleInputW -ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] -ReadConsoleInput.restype = BOOL + ReadConsoleInput = _KERNEL32.ReadConsoleInputW + ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] + ReadConsoleInput.restype = BOOL -OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) -InHandle = GetStdHandle(STD_INPUT_HANDLE) + OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) + InHandle = GetStdHandle(STD_INPUT_HANDLE) ENABLE_PROCESSED_OUTPUT = 0x01 ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 From f8a9176650837bd4775968522355058527db21bb Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 14:39:24 -0700 Subject: [PATCH 53/66] More MyPy fixes --- Lib/_pyrepl/console.py | 25 ++++++++++ Lib/_pyrepl/readline.py | 2 +- Lib/_pyrepl/simple_interact.py | 2 +- Lib/_pyrepl/unix_console.py | 15 +----- Lib/_pyrepl/windows_console.py | 89 +++++++++++++++------------------- 5 files changed, 67 insertions(+), 66 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index d7e86e768671dc..5ce462afeb5ab1 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -19,9 +19,14 @@ from __future__ import annotations +import sys + from abc import ABC, abstractmethod from dataclasses import dataclass, field +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from typing import IO @dataclass class Event: @@ -36,6 +41,26 @@ class Console(ABC): height: int = 25 width: int = 80 + def __init__( + self, + f_in: IO[bytes] | int = 0, + f_out: IO[bytes] | int = 1, + term: str = "", + encoding: str = "", + ): + self.encoding = encoding or sys.getdefaultencoding() + + if isinstance(f_in, int): + self.input_fd = f_in + else: + self.input_fd = f_in.fileno() + + if isinstance(f_out, int): + self.output_fd = f_out + else: + self.output_fd = f_out.fileno() + + @abstractmethod def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index eceafb95ddde2b..9a1ddf14da6809 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -41,7 +41,7 @@ from .console import Console as ConsoleType Console: type[ConsoleType] -error: tuple[type[Exception], ...] | type[Exception] +_error: tuple[type[Exception], ...] | type[Exception] try: from .unix_console import UnixConsole as Console, _error except: diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index fdc7c2863dcb5b..63f76033317f1b 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -35,7 +35,7 @@ from .readline import _get_reader, multiline_input -error: tuple[type[Exception], ...] | type[Exception] +_error: tuple[type[Exception], ...] | type[Exception] try: from .unix_console import _error except ModuleNotFoundError: diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index ec7d0636b9aeb3..8c7c5f3cb5ce38 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -143,19 +143,8 @@ def __init__( - term (str): Terminal name. - encoding (str): Encoding to use for I/O operations. """ - - self.encoding = encoding or sys.getdefaultencoding() - - if isinstance(f_in, int): - self.input_fd = f_in - else: - self.input_fd = f_in.fileno() - - if isinstance(f_out, int): - self.output_fd = f_out - else: - self.output_fd = f_out.fileno() - + super().__init__(f_in, f_out, term, encoding) + self.pollob = poll() self.pollob.register(self.input_fd, select.POLLIN) curses.setupterm(term or None, self.output_fd) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 8dfd9397309de9..115991f9f9f6d7 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -45,7 +45,7 @@ try: # type: ignore - from ctypes import GetLastError, WinDLL, windll + from ctypes import GetLastError, WinDLL, windll, WinError except: # Keep MyPy happy off Windows from ctypes import CDLL as WinDLL, cdll as windll @@ -113,6 +113,7 @@ def __init__( term: str = "", encoding: str = "", ): + super().__init__(f_in, f_out, term, encoding) SetConsoleMode( OutHandle, @@ -120,18 +121,6 @@ def __init__( | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING, ) - self.encoding = encoding or sys.getdefaultencoding() - - if isinstance(f_in, int): - self.input_fd = f_in - else: - self.input_fd = f_in.fileno() - - if isinstance(f_out, int): - self.output_fd = f_out - else: - self.output_fd = f_out.fileno() - self.screen: list[str] = [] self.width = 80 self.height = 25 @@ -293,7 +282,6 @@ def __show_cursor(self) -> None: raise WinError(GetLastError()) def __write(self, text: str) -> None: - print(repr(text)) os.write(self.output_fd, text.encode(self.encoding, "replace")) @property @@ -551,48 +539,47 @@ class INPUT_RECORD(Structure): STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 -if sys.platform == "win32": - _KERNEL32 = WinDLL("kernel32", use_last_error=True) +_KERNEL32 = WinDLL("kernel32", use_last_error=True) - GetStdHandle = windll.kernel32.GetStdHandle - GetStdHandle.argtypes = [DWORD] - GetStdHandle.restype = HANDLE +GetStdHandle = windll.kernel32.GetStdHandle +GetStdHandle.argtypes = [DWORD] +GetStdHandle.restype = HANDLE - GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo - GetConsoleScreenBufferInfo.argtypes = [ - HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), - ] - GetConsoleScreenBufferInfo.restype = BOOL - - SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo - SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] - SetConsoleCursorInfo.restype = BOOL - - GetConsoleCursorInfo = _KERNEL32.GetConsoleCursorInfo - GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] - GetConsoleCursorInfo.restype = BOOL - - ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW - ScrollConsoleScreenBuffer.argtypes = [ - HANDLE, - POINTER(SMALL_RECT), - POINTER(SMALL_RECT), - _COORD, - POINTER(CHAR_INFO), - ] - ScrollConsoleScreenBuffer.restype = BOOL +GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo +GetConsoleScreenBufferInfo.argtypes = [ + HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), +] +GetConsoleScreenBufferInfo.restype = BOOL + +SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo +SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +SetConsoleCursorInfo.restype = BOOL + +GetConsoleCursorInfo = _KERNEL32.GetConsoleCursorInfo +GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] +GetConsoleCursorInfo.restype = BOOL + +ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW +ScrollConsoleScreenBuffer.argtypes = [ + HANDLE, + POINTER(SMALL_RECT), + POINTER(SMALL_RECT), + _COORD, + POINTER(CHAR_INFO), +] +ScrollConsoleScreenBuffer.restype = BOOL - SetConsoleMode = _KERNEL32.SetConsoleMode - SetConsoleMode.argtypes = [HANDLE, DWORD] - SetConsoleMode.restype = BOOL +SetConsoleMode = _KERNEL32.SetConsoleMode +SetConsoleMode.argtypes = [HANDLE, DWORD] +SetConsoleMode.restype = BOOL - ReadConsoleInput = _KERNEL32.ReadConsoleInputW - ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] - ReadConsoleInput.restype = BOOL +ReadConsoleInput = _KERNEL32.ReadConsoleInputW +ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] +ReadConsoleInput.restype = BOOL - OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) - InHandle = GetStdHandle(STD_INPUT_HANDLE) +OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) +InHandle = GetStdHandle(STD_INPUT_HANDLE) ENABLE_PROCESSED_OUTPUT = 0x01 ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 From ce77202112b0036a391bb9fda4f4fec881d89841 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 17:44:04 -0700 Subject: [PATCH 54/66] More mypy fixes, name changes in WindowsConsole --- Lib/_pyrepl/console.py | 7 ++- Lib/_pyrepl/unix_console.py | 17 +++++-- Lib/_pyrepl/windows_console.py | 53 ++++++++++---------- Lib/test/test_pyrepl/test_windows_console.py | 8 +-- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 5ce462afeb5ab1..42f23c49da5c48 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from typing import IO + @dataclass class Event: evt: str @@ -49,7 +50,7 @@ def __init__( encoding: str = "", ): self.encoding = encoding or sys.getdefaultencoding() - + if isinstance(f_in, int): self.input_fd = f_in else: @@ -60,7 +61,6 @@ def __init__( else: self.output_fd = f_out.fileno() - @abstractmethod def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... @@ -133,5 +133,4 @@ def wait(self) -> None: ... @abstractmethod - def repaint(self) -> None: - ... + def repaint(self) -> None: ... diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 8c7c5f3cb5ce38..4bdb02261982c3 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -144,7 +144,7 @@ def __init__( - encoding (str): Encoding to use for I/O operations. """ super().__init__(f_in, f_out, term, encoding) - + self.pollob = poll() self.pollob.register(self.input_fd, select.POLLIN) curses.setupterm(term or None, self.output_fd) @@ -581,14 +581,19 @@ def __write_changed_line(self, y, oldline, newline, px_coord): px_pos = 0 j = 0 for c in oldline: - if j >= px_coord: break + if j >= px_coord: + break j += wlen(c) px_pos += 1 # reuse the oldline as much as possible, but stop as soon as we # encounter an ESCAPE, because it might be the start of an escape # sequene - while x_coord < minlen and oldline[x_pos] == newline[x_pos] and newline[x_pos] != "\x1b": + while ( + x_coord < minlen + and oldline[x_pos] == newline[x_pos] + and newline[x_pos] != "\x1b" + ): x_coord += wlen(newline[x_pos]) x_pos += 1 @@ -608,7 +613,11 @@ def __write_changed_line(self, y, oldline, newline, px_coord): self.__posxy = x_coord + character_width, y # if it's a single character change in the middle of the line - elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]): + elif ( + x_coord < minlen + and oldline[x_pos + 1 :] == newline[x_pos + 1 :] + and wlen(oldline[x_pos]) == wlen(newline[x_pos]) + ): character_width = wlen(newline[x_pos]) self.__move(x_coord, y) self.__write(newline[x_pos]) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 115991f9f9f6d7..7da527e1bedc0b 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -44,8 +44,7 @@ from .utils import wlen try: - # type: ignore - from ctypes import GetLastError, WinDLL, windll, WinError + from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore except: # Keep MyPy happy off Windows from ctypes import CDLL as WinDLL, cdll as windll @@ -53,7 +52,7 @@ def GetLastError() -> int: return 42 - class WinError(OSError): + class WinError(OSError): # type: ignore def __init__(self, err: int | None, descr: str | None = None) -> None: self.err = err self.descr = descr @@ -138,8 +137,8 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: cx, cy = c_xy while len(self.screen) < min(len(screen), self.height): - self.__hide_cursor() - self.__move_relative(0, len(self.screen) - 1) + self._hide_cursor() + self._move_relative(0, len(self.screen) - 1) self.__write("\n") self.__posxy = 0, len(self.screen) self.screen.append("") @@ -159,7 +158,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: # Scrolling the buffer as the current input is greater than the visible # portion of the window. We need to scroll the visible portion and the # entire history - self.scroll(scroll_lines, self.getscrollbacksize()) + self._scroll(scroll_lines, self._getscrollbacksize()) self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines self.__offset += scroll_lines @@ -174,7 +173,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: self.__offset = offset - self.__hide_cursor() + self._hide_cursor() for ( y, oldline, @@ -185,12 +184,12 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: y = len(newscr) while y < len(oldscr): - self.__move_relative(0, y) + self._move_relative(0, y) self.__posxy = 0, y - self.erase_to_end() + self._erase_to_end() y += 1 - self.__show_cursor() + self._show_cursor() self.screen = screen self.move_cursor(cx, cy) @@ -225,15 +224,15 @@ def __write_changed_line( x_coord += wlen(newline[x_pos]) x_pos += 1 - self.__hide_cursor() - self.__move_relative(x_coord, y) + self._hide_cursor() + self._move_relative(x_coord, y) if wlen(oldline) > wlen(newline): - self.erase_to_end() + self._erase_to_end() self.__write(newline[x_pos:]) if wlen(newline) == self.width: # If we wrapped we want to start at the next line - self.__move_relative(0, y + 1) + self._move_relative(0, y + 1) self.__posxy = 0, y + 1 else: self.__posxy = wlen(newline), y @@ -244,7 +243,7 @@ def __write_changed_line( # to the left margin should work to get to a known position. self.move_cursor(0, y) - def scroll( + def _scroll( self, top: int, bottom: int, left: int | None = None, right: int | None = None ) -> None: scroll_rect = SMALL_RECT() @@ -263,7 +262,7 @@ def scroll( ): raise WinError(GetLastError()) - def __hide_cursor(self) -> None: + def _hide_cursor(self) -> None: info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): raise WinError(GetLastError()) @@ -272,7 +271,7 @@ def __hide_cursor(self) -> None: if not SetConsoleCursorInfo(OutHandle, info): raise WinError(GetLastError()) - def __show_cursor(self) -> None: + def _show_cursor(self) -> None: info = CONSOLE_CURSOR_INFO() if not GetConsoleCursorInfo(OutHandle, info): raise WinError(GetLastError()) @@ -291,7 +290,7 @@ def screen_xy(self) -> tuple[int, int]: raise WinError(GetLastError()) return info.dwCursorPosition.X, info.dwCursorPosition.Y - def erase_to_end(self) -> None: + def _erase_to_end(self) -> None: self.__write(ERASE_IN_LINE) def prepare(self) -> None: @@ -314,7 +313,7 @@ def get_abs_position(self, x: int, y: int) -> tuple[int, int]: cur_y += dy return cur_x, cur_y - def __move_relative(self, x: int, y: int) -> None: + def _move_relative(self, x: int, y: int) -> None: """Moves relative to the current __posxy""" dx = x - self.__posxy[0] dy = y - self.__posxy[1] @@ -335,14 +334,14 @@ def move_cursor(self, x: int, y: int) -> None: if y < self.__offset or y >= self.__offset + self.height: self.event_queue.insert(0, Event("scroll", "")) else: - self.__move_relative(x, y) + self._move_relative(x, y) self.__posxy = x, y def set_cursor_vis(self, visible: bool) -> None: if visible: - self.__show_cursor() + self._show_cursor() else: - self.__hide_cursor() + self._hide_cursor() def getheightwidth(self) -> tuple[int, int]: """Return (height, width) where height and width are the height @@ -355,14 +354,14 @@ def getheightwidth(self) -> tuple[int, int]: info.srWindow.Right - info.srWindow.Left + 1, ) - def getscrollbacksize(self) -> int: + def _getscrollbacksize(self) -> int: info = CONSOLE_SCREEN_BUFFER_INFO() if not GetConsoleScreenBufferInfo(OutHandle, info): raise WinError(GetLastError()) return cast(int, info.srWindow.Bottom) - def __read_input(self) -> INPUT_RECORD | None: + def _read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() read = DWORD() if not ReadConsoleInput(InHandle, rec, 1, read): @@ -381,7 +380,7 @@ def get_event(self, block: bool = True) -> Event | None: return self.event_queue.pop() while True: - rec = self.__read_input() + rec = self._read_input() if rec is None: if block: continue @@ -443,7 +442,7 @@ def finish(self) -> None: y = len(self.screen) - 1 while y >= 0 and not self.screen[y]: y -= 1 - self.__move_relative(0, min(y, self.height + self.__offset - 1)) + self._move_relative(0, min(y, self.height + self.__offset - 1)) self.__write("\r\n") def flushoutput(self) -> None: @@ -455,7 +454,7 @@ def flushoutput(self) -> None: def forgetinput(self) -> None: """Forget all pending, but not yet processed input.""" - while self.__read_input() is not None: + while self._read_input() is not None: pass def getpending(self) -> Event: diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index e29ed0eab346c2..8a74f576ba5876 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -29,10 +29,10 @@ class WindowsConsoleTests(TestCase): def console(self, events, **kwargs) -> Console: console = WindowsConsole() console.get_event = MagicMock(side_effect=events) - console.scroll = MagicMock() - console.__hide_cursor = MagicMock() - console.__show_cursor = MagicMock() - console.getscrollbacksize = MagicMock(42) + console._scroll = MagicMock() + console._hide_cursor = MagicMock() + console._show_cursor = MagicMock() + console._getscrollbacksize = MagicMock(42) height = kwargs.get("height", 25) width = kwargs.get("width", 80) From 6d8e7f5cd58eecbc83f195975675af9700ffe0c8 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 17:47:57 -0700 Subject: [PATCH 55/66] Fix up ignores --- Lib/_pyrepl/windows_console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 7da527e1bedc0b..3f5b9f52d374bf 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -44,7 +44,7 @@ from .utils import wlen try: - from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore + from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined] except: # Keep MyPy happy off Windows from ctypes import CDLL as WinDLL, cdll as windll @@ -52,7 +52,7 @@ def GetLastError() -> int: return 42 - class WinError(OSError): # type: ignore + class WinError(OSError): # type: ignore[no-redef] def __init__(self, err: int | None, descr: str | None = None) -> None: self.err = err self.descr = descr From bbbb2969a727cd350efcc69d8bd6d93a7b207387 Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 18:27:05 -0700 Subject: [PATCH 56/66] Avoid kernel32 on non-Windows platforms --- Lib/_pyrepl/windows_console.py | 111 ++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 49 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 3f5b9f52d374bf..878eb0c1b576ae 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -44,7 +44,7 @@ from .utils import wlen try: - from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined] + from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined] except: # Keep MyPy happy off Windows from ctypes import CDLL as WinDLL, cdll as windll @@ -52,7 +52,7 @@ def GetLastError() -> int: return 42 - class WinError(OSError): # type: ignore[no-redef] + class WinError(OSError): # type: ignore[no-redef] def __init__(self, err: int | None, descr: str | None = None) -> None: self.err = err self.descr = descr @@ -524,62 +524,75 @@ class ConsoleEvent(ctypes.Union): ] +class INPUT_RECORD(Structure): + _fields_ = [("EventType", WORD), ("Event", ConsoleEvent)] + + KEY_EVENT = 0x01 FOCUS_EVENT = 0x10 MENU_EVENT = 0x08 MOUSE_EVENT = 0x02 WINDOW_BUFFER_SIZE_EVENT = 0x04 - -class INPUT_RECORD(Structure): - _fields_ = [("EventType", WORD), ("Event", ConsoleEvent)] - +ENABLE_PROCESSED_OUTPUT = 0x01 +ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 -_KERNEL32 = WinDLL("kernel32", use_last_error=True) - -GetStdHandle = windll.kernel32.GetStdHandle -GetStdHandle.argtypes = [DWORD] -GetStdHandle.restype = HANDLE - -GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo -GetConsoleScreenBufferInfo.argtypes = [ - HANDLE, - ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), -] -GetConsoleScreenBufferInfo.restype = BOOL - -SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo -SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -SetConsoleCursorInfo.restype = BOOL - -GetConsoleCursorInfo = _KERNEL32.GetConsoleCursorInfo -GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] -GetConsoleCursorInfo.restype = BOOL - -ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW -ScrollConsoleScreenBuffer.argtypes = [ - HANDLE, - POINTER(SMALL_RECT), - POINTER(SMALL_RECT), - _COORD, - POINTER(CHAR_INFO), -] -ScrollConsoleScreenBuffer.restype = BOOL - -SetConsoleMode = _KERNEL32.SetConsoleMode -SetConsoleMode.argtypes = [HANDLE, DWORD] -SetConsoleMode.restype = BOOL +if sys.platform == "win32": + _KERNEL32 = WinDLL("kernel32", use_last_error=True) -ReadConsoleInput = _KERNEL32.ReadConsoleInputW -ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] -ReadConsoleInput.restype = BOOL + GetStdHandle = windll.kernel32.GetStdHandle + GetStdHandle.argtypes = [DWORD] + GetStdHandle.restype = HANDLE -OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) -InHandle = GetStdHandle(STD_INPUT_HANDLE) - -ENABLE_PROCESSED_OUTPUT = 0x01 -ENABLE_WRAP_AT_EOL_OUTPUT = 0x02 -ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04 + GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo + GetConsoleScreenBufferInfo.argtypes = [ + HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), + ] + GetConsoleScreenBufferInfo.restype = BOOL + + SetConsoleCursorInfo = _KERNEL32.SetConsoleCursorInfo + SetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] + SetConsoleCursorInfo.restype = BOOL + + GetConsoleCursorInfo = _KERNEL32.GetConsoleCursorInfo + GetConsoleCursorInfo.argtypes = [HANDLE, POINTER(CONSOLE_CURSOR_INFO)] + GetConsoleCursorInfo.restype = BOOL + + ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW + ScrollConsoleScreenBuffer.argtypes = [ + HANDLE, + POINTER(SMALL_RECT), + POINTER(SMALL_RECT), + _COORD, + POINTER(CHAR_INFO), + ] + ScrollConsoleScreenBuffer.restype = BOOL + + SetConsoleMode = _KERNEL32.SetConsoleMode + SetConsoleMode.argtypes = [HANDLE, DWORD] + SetConsoleMode.restype = BOOL + + ReadConsoleInput = _KERNEL32.ReadConsoleInputW + ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)] + ReadConsoleInput.restype = BOOL + + OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) + InHandle = GetStdHandle(STD_INPUT_HANDLE) +else: + def _win_only(*args, **kwargs): + raise NotImplementedError("Windows only") + + GetStdHandle = _win_only + GetConsoleScreenBufferInfo = _win_only + SetConsoleCursorInfo = _win_only + GetConsoleCursorInfo = _win_only + ScrollConsoleScreenBuffer = _win_only + SetConsoleMode = _win_only + ReadConsoleInput = _win_only + OutHandle = 0 + InHandle = 0 \ No newline at end of file From d064124d34a9fe6e1d744a42bef56314df613a1a Mon Sep 17 00:00:00 2001 From: dino Date: Sat, 25 May 2024 18:34:08 -0700 Subject: [PATCH 57/66] Formatting --- Lib/_pyrepl/simple_interact.py | 7 ++----- Lib/_pyrepl/windows_console.py | 5 +++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 63f76033317f1b..e5bc0abe674351 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -115,11 +115,8 @@ def run_multiline_interactive_console( mainmodule: ModuleType | None= None, future_flags: int = 0 ) -> None: import __main__ - try: - from .readline import _setup - _setup() - except ImportError: - pass + from .readline import _setup + _setup() mainmodule = mainmodule or __main__ console = InteractiveColoredConsole(mainmodule.__dict__, filename="") diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 878eb0c1b576ae..72a89b5dd5e3d2 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -584,9 +584,10 @@ class INPUT_RECORD(Structure): OutHandle = GetStdHandle(STD_OUTPUT_HANDLE) InHandle = GetStdHandle(STD_INPUT_HANDLE) else: + def _win_only(*args, **kwargs): raise NotImplementedError("Windows only") - + GetStdHandle = _win_only GetConsoleScreenBufferInfo = _win_only SetConsoleCursorInfo = _win_only @@ -595,4 +596,4 @@ def _win_only(*args, **kwargs): SetConsoleMode = _win_only ReadConsoleInput = _win_only OutHandle = 0 - InHandle = 0 \ No newline at end of file + InHandle = 0 From 3d526bab1fa7ba2e8ba9973454d56d4438055abc Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Sat, 25 May 2024 20:50:40 -0700 Subject: [PATCH 58/66] roll back other merge --- Lib/_pyrepl/simple_interact.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 63f76033317f1b..e5bc0abe674351 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -115,11 +115,8 @@ def run_multiline_interactive_console( mainmodule: ModuleType | None= None, future_flags: int = 0 ) -> None: import __main__ - try: - from .readline import _setup - _setup() - except ImportError: - pass + from .readline import _setup + _setup() mainmodule = mainmodule or __main__ console = InteractiveColoredConsole(mainmodule.__dict__, filename="") From 1146dbba45dfd1211b772d787c7febd9087f35a4 Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 28 May 2024 20:37:38 -0700 Subject: [PATCH 59/66] Catch more specific comment, avoid typing, fix comment --- Lib/_pyrepl/readline.py | 2 +- Lib/_pyrepl/windows_console.py | 5 +++-- Lib/test/test_pyrepl/__init__.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 9a1ddf14da6809..248f3854a29689 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -44,7 +44,7 @@ _error: tuple[type[Exception], ...] | type[Exception] try: from .unix_console import UnixConsole as Console, _error -except: +except ImportError: from .windows_console import WindowsConsole as Console, _error ENCODING = sys.getdefaultencoding() or "latin1" diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index d2f205753b438a..ccceb3c65538fa 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -38,7 +38,7 @@ SHORT, ) from ctypes import Structure, POINTER, Union -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from .console import Event, Console from .trace import trace from .utils import wlen @@ -57,6 +57,7 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: self.err = err self.descr = descr +TYPE_CHECKING = False if TYPE_CHECKING: from typing import IO @@ -353,7 +354,7 @@ def _getscrollbacksize(self) -> int: if not GetConsoleScreenBufferInfo(OutHandle, info): raise WinError(GetLastError()) - return cast(int, info.srWindow.Bottom) + return info.srWindow.Bottom def _read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() diff --git a/Lib/test/test_pyrepl/__init__.py b/Lib/test/test_pyrepl/__init__.py index 1b33819eb180b2..8359d9844623c2 100644 --- a/Lib/test/test_pyrepl/__init__.py +++ b/Lib/test/test_pyrepl/__init__.py @@ -4,7 +4,7 @@ from test.support.import_helper import import_module if sys.platform != "win32": - # Optionally test pyrepl. This currently requires that the + # On non-Windows platforms, testing pyrepl currently requires that the # 'curses' resource be given on the regrtest command line using the -u # option. Additionally, we need to attempt to import curses and readline. requires("curses") From 972e7ec68258118454a1ec85889a19122f9f0b8e Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 28 May 2024 21:00:09 -0700 Subject: [PATCH 60/66] Ignore type error on return of Any --- Lib/_pyrepl/windows_console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index ccceb3c65538fa..4e3752a18b0ec7 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -354,7 +354,7 @@ def _getscrollbacksize(self) -> int: if not GetConsoleScreenBufferInfo(OutHandle, info): raise WinError(GetLastError()) - return info.srWindow.Bottom + return info.srWindow.Bottom # type: ignore[no-any-return] def _read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() From d783daeeea41591cb95c083c64f08499891e8a6d Mon Sep 17 00:00:00 2001 From: dino Date: Tue, 28 May 2024 21:24:28 -0700 Subject: [PATCH 61/66] Don't import traceback on exception --- Lib/_pyrepl/__main__.py | 2 -- Lib/_pyrepl/console.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/__main__.py b/Lib/_pyrepl/__main__.py index b81dd2c4f84ff0..dae4ba6e178b9a 100644 --- a/Lib/_pyrepl/__main__.py +++ b/Lib/_pyrepl/__main__.py @@ -40,8 +40,6 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False): except Exception as e: from .trace import trace msg = f"warning: can't use pyrepl: {e}" - import traceback - traceback.print_exc() trace(msg) print(msg, file=sys.stderr) CAN_USE_PYREPL = False diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 42f23c49da5c48..fcabf785069ecb 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -23,7 +23,9 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import TYPE_CHECKING + + +TYPE_CHECKING = False if TYPE_CHECKING: from typing import IO From d1289af2786333e9406f80e54d75223509ac66d8 Mon Sep 17 00:00:00 2001 From: dino Date: Wed, 29 May 2024 09:06:25 -0700 Subject: [PATCH 62/66] Remove one more TYPE_CHECKING --- Lib/_pyrepl/windows_console.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 4e3752a18b0ec7..4356bc1d328470 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -38,7 +38,6 @@ SHORT, ) from ctypes import Structure, POINTER, Union -from typing import TYPE_CHECKING from .console import Event, Console from .trace import trace from .utils import wlen From db191a24256b0a80732b916cf8bb7435321376d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 30 May 2024 14:29:55 +0200 Subject: [PATCH 63/66] Update Lib/test/test_pyrepl/test_windows_console.py Co-authored-by: Anthony Shaw --- Lib/test/test_pyrepl/test_windows_console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 8a74f576ba5876..69a5695ad4734f 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -24,7 +24,7 @@ @patch("os.write") -@unittest.skipIf(sys.platform != "win32", "No Unix event queue on Windows") +@unittest.skipIf(sys.platform != "win32", "Test class specifically for Windows") class WindowsConsoleTests(TestCase): def console(self, events, **kwargs) -> Console: console = WindowsConsole() From fba84aa8386491156edff5e8fe3d74920bd5b3d2 Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 30 May 2024 23:10:56 -0700 Subject: [PATCH 64/66] Use _WindowsConsoleIO for output --- Lib/_pyrepl/windows_console.py | 7 +- Lib/test/test_pyrepl/test_windows_console.py | 111 ++++++++++--------- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 4356bc1d328470..4ae4f0d6bc6d3f 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -22,6 +22,7 @@ import os import sys +from _io import _WindowsConsoleIO from abc import ABC, abstractmethod from collections import deque from dataclasses import dataclass, field @@ -125,6 +126,7 @@ def __init__( self.height = 25 self.__offset = 0 self.event_queue: deque[Event] = deque() + self.out = _WindowsConsoleIO(self.output_fd, 'w') def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ @@ -275,8 +277,9 @@ def _disable_blinking(self): self.__write("\x1b[?12l") def __write(self, text: str) -> None: - os.write(self.output_fd, text.encode(self.encoding, "replace")) - + self.out.write(text.encode(self.encoding, "replace")) + self.out.flush() + @property def screen_xy(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 8a74f576ba5876..41c2c707100412 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -23,7 +23,7 @@ pass -@patch("os.write") +# @patch("os.write") @unittest.skipIf(sys.platform != "win32", "No Unix event queue on Windows") class WindowsConsoleTests(TestCase): def console(self, events, **kwargs) -> Console: @@ -33,6 +33,7 @@ def console(self, events, **kwargs) -> Console: console._hide_cursor = MagicMock() console._show_cursor = MagicMock() console._getscrollbacksize = MagicMock(42) + console.out.write = MagicMock() height = kwargs.get("height", 25) width = kwargs.get("width", 80) @@ -55,31 +56,31 @@ def handle_events_short(self, events): def handle_events_height_3(self, events): return self.handle_events(events, height=3) - def test_simple_addition(self, _os_write): + def test_simple_addition(self): code = "12+34" events = code_to_events(code) _, con = self.handle_events(events) - _os_write.assert_any_call(ANY, b"1") - _os_write.assert_any_call(ANY, b"2") - _os_write.assert_any_call(ANY, b"+") - _os_write.assert_any_call(ANY, b"3") - _os_write.assert_any_call(ANY, b"4") + con.out.write.assert_any_call(b"1") + con.out.write.assert_any_call(b"2") + con.out.write.assert_any_call(b"+") + con.out.write.assert_any_call(b"3") + con.out.write.assert_any_call(b"4") con.restore() - def test_wrap(self, _os_write): + def test_wrap(self): code = "12+34" events = code_to_events(code) _, con = self.handle_events_narrow(events) - _os_write.assert_any_call(ANY, b"1") - _os_write.assert_any_call(ANY, b"2") - _os_write.assert_any_call(ANY, b"+") - _os_write.assert_any_call(ANY, b"3") - _os_write.assert_any_call(ANY, b"\\") - _os_write.assert_any_call(ANY, b"\n") - _os_write.assert_any_call(ANY, b"4") + con.out.write.assert_any_call(b"1") + con.out.write.assert_any_call(b"2") + con.out.write.assert_any_call(b"+") + con.out.write.assert_any_call(b"3") + con.out.write.assert_any_call(b"\\") + con.out.write.assert_any_call(b"\n") + con.out.write.assert_any_call(b"4") con.restore() - def test_resize_wider(self, _os_write): + def test_resize_wider(self): code = "1234567890" events = code_to_events(code) reader, console = self.handle_events_narrow(events) @@ -101,13 +102,13 @@ def same_console(events): prepare_console=same_console, ) - _os_write.assert_any_call(ANY, self.move_right(2)) - _os_write.assert_any_call(ANY, self.move_up(2)) - _os_write.assert_any_call(ANY, b"567890") + 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.restore() - def test_resize_narrower(self, _os_write): + def test_resize_narrower(self): code = "1234567890" events = code_to_events(code) reader, console = self.handle_events(events) @@ -129,22 +130,22 @@ def same_console(events): prepare_console=same_console, ) - _os_write.assert_any_call(ANY, b"456\\") - _os_write.assert_any_call(ANY, b"789\\") + con.out.write.assert_any_call(b"456\\") + con.out.write.assert_any_call(b"789\\") con.restore() - def test_cursor_left(self, _os_write): + def test_cursor_left(self): code = "1" events = itertools.chain( code_to_events(code), [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))], ) _, con = self.handle_events(events) - _os_write.assert_any_call(ANY, self.move_left()) + con.out.write.assert_any_call(self.move_left()) con.restore() - def test_cursor_left_right(self, _os_write): + def test_cursor_left_right(self): code = "1" events = itertools.chain( code_to_events(code), @@ -154,21 +155,21 @@ def test_cursor_left_right(self, _os_write): ], ) _, con = self.handle_events(events) - _os_write.assert_any_call(ANY, self.move_left()) - _os_write.assert_any_call(ANY, self.move_right()) + con.out.write.assert_any_call(self.move_left()) + con.out.write.assert_any_call(self.move_right()) con.restore() - def test_cursor_up(self, _os_write): + def test_cursor_up(self): code = "1\n2+3" events = itertools.chain( code_to_events(code), [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))], ) _, con = self.handle_events(events) - _os_write.assert_any_call(ANY, self.move_up()) + con.out.write.assert_any_call(self.move_up()) con.restore() - def test_cursor_up_down(self, _os_write): + def test_cursor_up_down(self): code = "1\n2+3" events = itertools.chain( code_to_events(code), @@ -178,23 +179,23 @@ def test_cursor_up_down(self, _os_write): ], ) _, con = self.handle_events(events) - _os_write.assert_any_call(ANY, self.move_up()) - _os_write.assert_any_call(ANY, self.move_down()) + con.out.write.assert_any_call(self.move_up()) + con.out.write.assert_any_call(self.move_down()) con.restore() - def test_cursor_back_write(self, _os_write): + def test_cursor_back_write(self): events = itertools.chain( code_to_events("1"), [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))], code_to_events("2"), ) _, con = self.handle_events(events) - _os_write.assert_any_call(ANY, b"1") - _os_write.assert_any_call(ANY, self.move_left()) - _os_write.assert_any_call(ANY, b"21") + con.out.write.assert_any_call(b"1") + con.out.write.assert_any_call(self.move_left()) + con.out.write.assert_any_call(b"21") con.restore() - def test_multiline_function_move_up_short_terminal(self, _os_write): + def test_multiline_function_move_up_short_terminal(self): # fmt: off code = ( "def f():\n" @@ -210,11 +211,11 @@ def test_multiline_function_move_up_short_terminal(self, _os_write): ], ) _, con = self.handle_events_short(events) - _os_write.assert_any_call(ANY, self.move_left(5)) - _os_write.assert_any_call(ANY, self.move_up()) + con.out.write.assert_any_call(self.move_left(5)) + con.out.write.assert_any_call(self.move_up()) con.restore() - def test_multiline_function_move_up_down_short_terminal(self, _os_write): + def test_multiline_function_move_up_down_short_terminal(self): # fmt: off code = ( "def f():\n" @@ -232,11 +233,11 @@ def test_multiline_function_move_up_down_short_terminal(self, _os_write): ], ) _, con = self.handle_events_short(events) - _os_write.assert_any_call(ANY, self.move_left(8)) - _os_write.assert_any_call(ANY, self.erase_in_line()) + con.out.write.assert_any_call(self.move_left(8)) + con.out.write.assert_any_call(self.erase_in_line()) con.restore() - def test_resize_bigger_on_multiline_function(self, _os_write): + def test_resize_bigger_on_multiline_function(self): # fmt: off code = ( "def f():\n" @@ -262,19 +263,19 @@ def same_console(events): prepare_reader=same_reader, prepare_console=same_console, ) - _os_write.assert_has_calls( + con.out.write.assert_has_calls( [ - call(ANY, self.move_left(5)), - call(ANY, self.move_up()), - call(ANY, b"def f():"), - call(ANY, self.move_left(3)), - call(ANY, self.move_down()), + call(self.move_left(5)), + call(self.move_up()), + call(b"def f():"), + call(self.move_left(3)), + call(self.move_down()), ] ) console.restore() con.restore() - def test_resize_smaller_on_multiline_function(self, _os_write): + def test_resize_smaller_on_multiline_function(self): # fmt: off code = ( "def f():\n" @@ -300,12 +301,12 @@ def same_console(events): prepare_reader=same_reader, prepare_console=same_console, ) - _os_write.assert_has_calls( + con.out.write.assert_has_calls( [ - call(ANY, self.move_left(5)), - call(ANY, self.move_up()), - call(ANY, self.erase_in_line()), - call(ANY, b" foo"), + call(self.move_left(5)), + call(self.move_up()), + call(self.erase_in_line()), + call(b" foo"), ] ) console.restore() From 63d854edd0439d91e035ad37a80c770e5448e65f Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 30 May 2024 23:28:39 -0700 Subject: [PATCH 65/66] Remove get_abs_positon --- Lib/_pyrepl/windows_console.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 4ae4f0d6bc6d3f..fb2538ae4ebe86 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -19,10 +19,10 @@ from __future__ import annotations +import io import os import sys -from _io import _WindowsConsoleIO from abc import ABC, abstractmethod from collections import deque from dataclasses import dataclass, field @@ -57,6 +57,7 @@ def __init__(self, err: int | None, descr: str | None = None) -> None: self.err = err self.descr = descr + TYPE_CHECKING = False if TYPE_CHECKING: @@ -126,7 +127,7 @@ def __init__( self.height = 25 self.__offset = 0 self.event_queue: deque[Event] = deque() - self.out = _WindowsConsoleIO(self.output_fd, 'w') + self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined] def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ @@ -279,7 +280,7 @@ def _disable_blinking(self): def __write(self, text: str) -> None: self.out.write(text.encode(self.encoding, "replace")) self.out.flush() - + @property def screen_xy(self) -> tuple[int, int]: info = CONSOLE_SCREEN_BUFFER_INFO() @@ -302,14 +303,6 @@ def prepare(self) -> None: def restore(self) -> None: pass - def get_abs_position(self, x: int, y: int) -> tuple[int, int]: - cur_x, cur_y = self.screen_xy - dx = x - self.__posxy[0] - dy = y - self.__posxy[1] - cur_x += dx - cur_y += dy - return cur_x, cur_y - def _move_relative(self, x: int, y: int) -> None: """Moves relative to the current __posxy""" dx = x - self.__posxy[0] @@ -356,7 +349,7 @@ def _getscrollbacksize(self) -> int: if not GetConsoleScreenBufferInfo(OutHandle, info): raise WinError(GetLastError()) - return info.srWindow.Bottom # type: ignore[no-any-return] + return info.srWindow.Bottom # type: ignore[no-any-return] def _read_input(self) -> INPUT_RECORD | None: rec = INPUT_RECORD() From 513a14864c0b5d1bcbb1356af68aabc63b9e687c Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 31 May 2024 00:04:48 -0700 Subject: [PATCH 66/66] Fix tests on windows --- Lib/_pyrepl/windows_console.py | 14 +++++++++++--- Lib/test/test_pyrepl/test_windows_console.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index fb2538ae4ebe86..2277865e3262fc 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -20,6 +20,7 @@ from __future__ import annotations import io +from multiprocessing import Value import os import sys @@ -127,7 +128,11 @@ def __init__( self.height = 25 self.__offset = 0 self.event_queue: deque[Event] = deque() - self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined] + try: + self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined] + except ValueError: + # Console I/O is redirected, fallback... + self.out = None def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ @@ -278,8 +283,11 @@ def _disable_blinking(self): self.__write("\x1b[?12l") def __write(self, text: str) -> None: - self.out.write(text.encode(self.encoding, "replace")) - self.out.flush() + if self.out is not None: + self.out.write(text.encode(self.encoding, "replace")) + self.out.flush() + else: + os.write(self.output_fd, text.encode(self.encoding, "replace")) @property def screen_xy(self) -> tuple[int, int]: diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 72c9309588dd7a..e87dfe99b1a17d 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -32,7 +32,7 @@ def console(self, events, **kwargs) -> Console: console._hide_cursor = MagicMock() console._show_cursor = MagicMock() console._getscrollbacksize = MagicMock(42) - console.out.write = MagicMock() + console.out = MagicMock() height = kwargs.get("height", 25) width = kwargs.get("width", 80)