123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683 |
- 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: <class 'TypeError'>: wrong type",)
- argument 2: <class 'TypeError'>: 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 %s, 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." % 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(("%r" % func.__name__).encode("utf-8") + b"\n")
- self.LOG.write(
- b" " + ", ".join(["%r" % i for i in a]).encode("utf-8") + b"\n"
- )
- self.LOG.write(
- b" "
- + ", ".join(["%r" % type(i) 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(("%r" % data).encode("utf-8") + 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]
|