from __future__ import annotations import sys assert sys.platform == "win32" import os from ctypes import ArgumentError, byref, c_char, c_long, c_uint, c_ulong, pointer from ctypes.wintypes import DWORD, HANDLE from typing import Callable, TextIO, TypeVar from prompt_toolkit.cursor_shapes import CursorShape from prompt_toolkit.data_structures import Size from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs from prompt_toolkit.utils import get_cwidth from prompt_toolkit.win32_types import ( CONSOLE_SCREEN_BUFFER_INFO, COORD, SMALL_RECT, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, ) from ..utils import SPHINX_AUTODOC_RUNNING from .base import Output from .color_depth import ColorDepth # Do not import win32-specific stuff when generating documentation. # Otherwise RTD would be unable to generate docs for this module. if not SPHINX_AUTODOC_RUNNING: from ctypes import windll __all__ = [ "Win32Output", ] def _coord_byval(coord: COORD) -> c_long: """ Turns a COORD object into a c_long. This will cause it to be passed by value instead of by reference. (That is what I think at least.) When running ``ptipython`` is run (only with IPython), we often got the following error:: Error in 'SetConsoleCursorPosition'. ArgumentError("argument 2: : wrong type",) argument 2: : wrong type It was solved by turning ``COORD`` parameters into a ``c_long`` like this. More info: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx """ return c_long(coord.Y * 0x10000 | coord.X & 0xFFFF) #: If True: write the output of the renderer also to the following file. This #: is very useful for debugging. (e.g.: to see that we don't write more bytes #: than required.) _DEBUG_RENDER_OUTPUT = False _DEBUG_RENDER_OUTPUT_FILENAME = r"prompt-toolkit-windows-output.log" class NoConsoleScreenBufferError(Exception): """ Raised when the application is not running inside a Windows Console, but the user tries to instantiate Win32Output. """ def __init__(self) -> None: # Are we running in 'xterm' on Windows, like git-bash for instance? xterm = "xterm" in os.environ.get("TERM", "") if xterm: message = ( "Found {}, while expecting a Windows console. " 'Maybe try to run this program using "winpty" ' "or run it in cmd.exe instead. Or otherwise, " "in case of Cygwin, use the Python executable " "that is compiled for Cygwin.".format(os.environ["TERM"]) ) else: message = "No Windows console found. Are you running cmd.exe?" super().__init__(message) _T = TypeVar("_T") class Win32Output(Output): """ I/O abstraction for rendering to Windows consoles. (cmd.exe and similar.) """ def __init__( self, stdout: TextIO, use_complete_width: bool = False, default_color_depth: ColorDepth | None = None, ) -> None: self.use_complete_width = use_complete_width self.default_color_depth = default_color_depth self._buffer: list[str] = [] self.stdout: TextIO = stdout self.hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) self._in_alternate_screen = False self._hidden = False self.color_lookup_table = ColorLookupTable() # Remember the default console colors. info = self.get_win32_screen_buffer_info() self.default_attrs = info.wAttributes if info else 15 if _DEBUG_RENDER_OUTPUT: self.LOG = open(_DEBUG_RENDER_OUTPUT_FILENAME, "ab") def fileno(self) -> int: "Return file descriptor." return self.stdout.fileno() def encoding(self) -> str: "Return encoding used for stdout." return self.stdout.encoding def write(self, data: str) -> None: if self._hidden: data = " " * get_cwidth(data) self._buffer.append(data) def write_raw(self, data: str) -> None: "For win32, there is no difference between write and write_raw." self.write(data) def get_size(self) -> Size: info = self.get_win32_screen_buffer_info() # We take the width of the *visible* region as the size. Not the width # of the complete screen buffer. (Unless use_complete_width has been # set.) if self.use_complete_width: width = info.dwSize.X else: width = info.srWindow.Right - info.srWindow.Left height = info.srWindow.Bottom - info.srWindow.Top + 1 # We avoid the right margin, windows will wrap otherwise. maxwidth = info.dwSize.X - 1 width = min(maxwidth, width) # Create `Size` object. return Size(rows=height, columns=width) def _winapi(self, func: Callable[..., _T], *a: object, **kw: object) -> _T: """ Flush and call win API function. """ self.flush() if _DEBUG_RENDER_OUTPUT: self.LOG.write((f"{func.__name__!r}").encode() + b"\n") self.LOG.write( b" " + ", ".join([f"{i!r}" for i in a]).encode("utf-8") + b"\n" ) self.LOG.write( b" " + ", ".join([f"{type(i)!r}" for i in a]).encode("utf-8") + b"\n" ) self.LOG.flush() try: return func(*a, **kw) except ArgumentError as e: if _DEBUG_RENDER_OUTPUT: self.LOG.write((f" Error in {func.__name__!r} {e!r} {e}\n").encode()) raise def get_win32_screen_buffer_info(self) -> CONSOLE_SCREEN_BUFFER_INFO: """ Return Screen buffer info. """ # NOTE: We don't call the `GetConsoleScreenBufferInfo` API through # `self._winapi`. Doing so causes Python to crash on certain 64bit # Python versions. (Reproduced with 64bit Python 2.7.6, on Windows # 10). It is not clear why. Possibly, it has to do with passing # these objects as an argument, or through *args. # The Python documentation contains the following - possibly related - warning: # ctypes does not support passing unions or structures with # bit-fields to functions by value. While this may work on 32-bit # x86, it's not guaranteed by the library to work in the general # case. Unions and structures with bit-fields should always be # passed to functions by pointer. # Also see: # - https://github.com/ipython/ipython/issues/10070 # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/406 # - https://github.com/jonathanslenders/python-prompt-toolkit/issues/86 self.flush() sbinfo = CONSOLE_SCREEN_BUFFER_INFO() success = windll.kernel32.GetConsoleScreenBufferInfo( self.hconsole, byref(sbinfo) ) # success = self._winapi(windll.kernel32.GetConsoleScreenBufferInfo, # self.hconsole, byref(sbinfo)) if success: return sbinfo else: raise NoConsoleScreenBufferError def set_title(self, title: str) -> None: """ Set terminal title. """ self._winapi(windll.kernel32.SetConsoleTitleW, title) def clear_title(self) -> None: self._winapi(windll.kernel32.SetConsoleTitleW, "") def erase_screen(self) -> None: start = COORD(0, 0) sbinfo = self.get_win32_screen_buffer_info() length = sbinfo.dwSize.X * sbinfo.dwSize.Y self.cursor_goto(row=0, column=0) self._erase(start, length) def erase_down(self) -> None: sbinfo = self.get_win32_screen_buffer_info() size = sbinfo.dwSize start = sbinfo.dwCursorPosition length = (size.X - size.X) + size.X * (size.Y - sbinfo.dwCursorPosition.Y) self._erase(start, length) def erase_end_of_line(self) -> None: """""" sbinfo = self.get_win32_screen_buffer_info() start = sbinfo.dwCursorPosition length = sbinfo.dwSize.X - sbinfo.dwCursorPosition.X self._erase(start, length) def _erase(self, start: COORD, length: int) -> None: chars_written = c_ulong() self._winapi( windll.kernel32.FillConsoleOutputCharacterA, self.hconsole, c_char(b" "), DWORD(length), _coord_byval(start), byref(chars_written), ) # Reset attributes. sbinfo = self.get_win32_screen_buffer_info() self._winapi( windll.kernel32.FillConsoleOutputAttribute, self.hconsole, sbinfo.wAttributes, length, _coord_byval(start), byref(chars_written), ) def reset_attributes(self) -> None: "Reset the console foreground/background color." self._winapi( windll.kernel32.SetConsoleTextAttribute, self.hconsole, self.default_attrs ) self._hidden = False def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: ( fgcolor, bgcolor, bold, underline, strike, italic, blink, reverse, hidden, ) = attrs self._hidden = bool(hidden) # Start from the default attributes. win_attrs: int = self.default_attrs if color_depth != ColorDepth.DEPTH_1_BIT: # Override the last four bits: foreground color. if fgcolor: win_attrs = win_attrs & ~0xF win_attrs |= self.color_lookup_table.lookup_fg_color(fgcolor) # Override the next four bits: background color. if bgcolor: win_attrs = win_attrs & ~0xF0 win_attrs |= self.color_lookup_table.lookup_bg_color(bgcolor) # Reverse: swap these four bits groups. if reverse: win_attrs = ( (win_attrs & ~0xFF) | ((win_attrs & 0xF) << 4) | ((win_attrs & 0xF0) >> 4) ) self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, win_attrs) def disable_autowrap(self) -> None: # Not supported by Windows. pass def enable_autowrap(self) -> None: # Not supported by Windows. pass def cursor_goto(self, row: int = 0, column: int = 0) -> None: pos = COORD(X=column, Y=row) self._winapi( windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) ) def cursor_up(self, amount: int) -> None: sr = self.get_win32_screen_buffer_info().dwCursorPosition pos = COORD(X=sr.X, Y=sr.Y - amount) self._winapi( windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) ) def cursor_down(self, amount: int) -> None: self.cursor_up(-amount) def cursor_forward(self, amount: int) -> None: sr = self.get_win32_screen_buffer_info().dwCursorPosition # assert sr.X + amount >= 0, 'Negative cursor position: x=%r amount=%r' % (sr.X, amount) pos = COORD(X=max(0, sr.X + amount), Y=sr.Y) self._winapi( windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos) ) def cursor_backward(self, amount: int) -> None: self.cursor_forward(-amount) def flush(self) -> None: """ Write to output stream and flush. """ if not self._buffer: # Only flush stdout buffer. (It could be that Python still has # something in its buffer. -- We want to be sure to print that in # the correct color.) self.stdout.flush() return data = "".join(self._buffer) if _DEBUG_RENDER_OUTPUT: self.LOG.write((f"{data!r}").encode() + b"\n") self.LOG.flush() # Print characters one by one. This appears to be the best solution # in order to avoid traces of vertical lines when the completion # menu disappears. for b in data: written = DWORD() retval = windll.kernel32.WriteConsoleW( self.hconsole, b, 1, byref(written), None ) assert retval != 0 self._buffer = [] def get_rows_below_cursor_position(self) -> int: info = self.get_win32_screen_buffer_info() return info.srWindow.Bottom - info.dwCursorPosition.Y + 1 def scroll_buffer_to_prompt(self) -> None: """ To be called before drawing the prompt. This should scroll the console to left, with the cursor at the bottom (if possible). """ # Get current window size info = self.get_win32_screen_buffer_info() sr = info.srWindow cursor_pos = info.dwCursorPosition result = SMALL_RECT() # Scroll to the left. result.Left = 0 result.Right = sr.Right - sr.Left # Scroll vertical win_height = sr.Bottom - sr.Top if 0 < sr.Bottom - cursor_pos.Y < win_height - 1: # no vertical scroll if cursor already on the screen result.Bottom = sr.Bottom else: result.Bottom = max(win_height, cursor_pos.Y) result.Top = result.Bottom - win_height # Scroll API self._winapi( windll.kernel32.SetConsoleWindowInfo, self.hconsole, True, byref(result) ) def enter_alternate_screen(self) -> None: """ Go to alternate screen buffer. """ if not self._in_alternate_screen: GENERIC_READ = 0x80000000 GENERIC_WRITE = 0x40000000 # Create a new console buffer and activate that one. handle = HANDLE( self._winapi( windll.kernel32.CreateConsoleScreenBuffer, GENERIC_READ | GENERIC_WRITE, DWORD(0), None, DWORD(1), None, ) ) self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, handle) self.hconsole = handle self._in_alternate_screen = True def quit_alternate_screen(self) -> None: """ Make stdout again the active buffer. """ if self._in_alternate_screen: stdout = HANDLE( self._winapi(windll.kernel32.GetStdHandle, STD_OUTPUT_HANDLE) ) self._winapi(windll.kernel32.SetConsoleActiveScreenBuffer, stdout) self._winapi(windll.kernel32.CloseHandle, self.hconsole) self.hconsole = stdout self._in_alternate_screen = False def enable_mouse_support(self) -> None: ENABLE_MOUSE_INPUT = 0x10 # This `ENABLE_QUICK_EDIT_MODE` flag needs to be cleared for mouse # support to work, but it's possible that it was already cleared # before. ENABLE_QUICK_EDIT_MODE = 0x0040 handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) original_mode = DWORD() self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) self._winapi( windll.kernel32.SetConsoleMode, handle, (original_mode.value | ENABLE_MOUSE_INPUT) & ~ENABLE_QUICK_EDIT_MODE, ) def disable_mouse_support(self) -> None: ENABLE_MOUSE_INPUT = 0x10 handle = HANDLE(windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)) original_mode = DWORD() self._winapi(windll.kernel32.GetConsoleMode, handle, pointer(original_mode)) self._winapi( windll.kernel32.SetConsoleMode, handle, original_mode.value & ~ENABLE_MOUSE_INPUT, ) def hide_cursor(self) -> None: pass def show_cursor(self) -> None: pass def set_cursor_shape(self, cursor_shape: CursorShape) -> None: pass def reset_cursor_shape(self) -> None: pass @classmethod def win32_refresh_window(cls) -> None: """ Call win32 API to refresh the whole Window. This is sometimes necessary when the application paints background for completion menus. When the menu disappears, it leaves traces due to a bug in the Windows Console. Sending a repaint request solves it. """ # Get console handle handle = HANDLE(windll.kernel32.GetConsoleWindow()) RDW_INVALIDATE = 0x0001 windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE)) def get_default_color_depth(self) -> ColorDepth: """ Return the default color depth for a windows terminal. Contrary to the Vt100 implementation, this doesn't depend on a $TERM variable. """ if self.default_color_depth is not None: return self.default_color_depth return ColorDepth.DEPTH_4_BIT class FOREGROUND_COLOR: BLACK = 0x0000 BLUE = 0x0001 GREEN = 0x0002 CYAN = 0x0003 RED = 0x0004 MAGENTA = 0x0005 YELLOW = 0x0006 GRAY = 0x0007 INTENSITY = 0x0008 # Foreground color is intensified. class BACKGROUND_COLOR: BLACK = 0x0000 BLUE = 0x0010 GREEN = 0x0020 CYAN = 0x0030 RED = 0x0040 MAGENTA = 0x0050 YELLOW = 0x0060 GRAY = 0x0070 INTENSITY = 0x0080 # Background color is intensified. def _create_ansi_color_dict( color_cls: type[FOREGROUND_COLOR] | type[BACKGROUND_COLOR], ) -> dict[str, int]: "Create a table that maps the 16 named ansi colors to their Windows code." return { "ansidefault": color_cls.BLACK, "ansiblack": color_cls.BLACK, "ansigray": color_cls.GRAY, "ansibrightblack": color_cls.BLACK | color_cls.INTENSITY, "ansiwhite": color_cls.GRAY | color_cls.INTENSITY, # Low intensity. "ansired": color_cls.RED, "ansigreen": color_cls.GREEN, "ansiyellow": color_cls.YELLOW, "ansiblue": color_cls.BLUE, "ansimagenta": color_cls.MAGENTA, "ansicyan": color_cls.CYAN, # High intensity. "ansibrightred": color_cls.RED | color_cls.INTENSITY, "ansibrightgreen": color_cls.GREEN | color_cls.INTENSITY, "ansibrightyellow": color_cls.YELLOW | color_cls.INTENSITY, "ansibrightblue": color_cls.BLUE | color_cls.INTENSITY, "ansibrightmagenta": color_cls.MAGENTA | color_cls.INTENSITY, "ansibrightcyan": color_cls.CYAN | color_cls.INTENSITY, } FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR) BG_ANSI_COLORS = _create_ansi_color_dict(BACKGROUND_COLOR) assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES) class ColorLookupTable: """ Inspired by pygments/formatters/terminal256.py """ def __init__(self) -> None: self._win32_colors = self._build_color_table() # Cache (map color string to foreground and background code). self.best_match: dict[str, tuple[int, int]] = {} @staticmethod def _build_color_table() -> list[tuple[int, int, int, int, int]]: """ Build an RGB-to-256 color conversion table """ FG = FOREGROUND_COLOR BG = BACKGROUND_COLOR return [ (0x00, 0x00, 0x00, FG.BLACK, BG.BLACK), (0x00, 0x00, 0xAA, FG.BLUE, BG.BLUE), (0x00, 0xAA, 0x00, FG.GREEN, BG.GREEN), (0x00, 0xAA, 0xAA, FG.CYAN, BG.CYAN), (0xAA, 0x00, 0x00, FG.RED, BG.RED), (0xAA, 0x00, 0xAA, FG.MAGENTA, BG.MAGENTA), (0xAA, 0xAA, 0x00, FG.YELLOW, BG.YELLOW), (0x88, 0x88, 0x88, FG.GRAY, BG.GRAY), (0x44, 0x44, 0xFF, FG.BLUE | FG.INTENSITY, BG.BLUE | BG.INTENSITY), (0x44, 0xFF, 0x44, FG.GREEN | FG.INTENSITY, BG.GREEN | BG.INTENSITY), (0x44, 0xFF, 0xFF, FG.CYAN | FG.INTENSITY, BG.CYAN | BG.INTENSITY), (0xFF, 0x44, 0x44, FG.RED | FG.INTENSITY, BG.RED | BG.INTENSITY), (0xFF, 0x44, 0xFF, FG.MAGENTA | FG.INTENSITY, BG.MAGENTA | BG.INTENSITY), (0xFF, 0xFF, 0x44, FG.YELLOW | FG.INTENSITY, BG.YELLOW | BG.INTENSITY), (0x44, 0x44, 0x44, FG.BLACK | FG.INTENSITY, BG.BLACK | BG.INTENSITY), (0xFF, 0xFF, 0xFF, FG.GRAY | FG.INTENSITY, BG.GRAY | BG.INTENSITY), ] def _closest_color(self, r: int, g: int, b: int) -> tuple[int, int]: distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff) fg_match = 0 bg_match = 0 for r_, g_, b_, fg_, bg_ in self._win32_colors: rd = r - r_ gd = g - g_ bd = b - b_ d = rd * rd + gd * gd + bd * bd if d < distance: fg_match = fg_ bg_match = bg_ distance = d return fg_match, bg_match def _color_indexes(self, color: str) -> tuple[int, int]: indexes = self.best_match.get(color, None) if indexes is None: try: rgb = int(str(color), 16) except ValueError: rgb = 0 r = (rgb >> 16) & 0xFF g = (rgb >> 8) & 0xFF b = rgb & 0xFF indexes = self._closest_color(r, g, b) self.best_match[color] = indexes return indexes def lookup_fg_color(self, fg_color: str) -> int: """ Return the color for use in the `windll.kernel32.SetConsoleTextAttribute` API call. :param fg_color: Foreground as text. E.g. 'ffffff' or 'red' """ # Foreground. if fg_color in FG_ANSI_COLORS: return FG_ANSI_COLORS[fg_color] else: return self._color_indexes(fg_color)[0] def lookup_bg_color(self, bg_color: str) -> int: """ Return the color for use in the `windll.kernel32.SetConsoleTextAttribute` API call. :param bg_color: Background as text. E.g. 'ffffff' or 'red' """ # Background. if bg_color in BG_ANSI_COLORS: return BG_ANSI_COLORS[bg_color] else: return self._color_indexes(bg_color)[1]