Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

View file

@ -0,0 +1,13 @@
from .base import DummyOutput, Output
from .color_depth import ColorDepth
from .defaults import create_output
__all__ = [
# Base.
"Output",
"DummyOutput",
# Color depth.
"ColorDepth",
# Defaults.
"create_output",
]

View file

@ -0,0 +1,303 @@
"""
Interface for an output.
"""
from abc import ABCMeta, abstractmethod
from prompt_toolkit.data_structures import Size
from prompt_toolkit.styles import Attrs
from .color_depth import ColorDepth
__all__ = [
"Output",
"DummyOutput",
]
class Output(metaclass=ABCMeta):
"""
Base class defining the output interface for a
:class:`~prompt_toolkit.renderer.Renderer`.
Actual implementations are
:class:`~prompt_toolkit.output.vt100.Vt100_Output` and
:class:`~prompt_toolkit.output.win32.Win32Output`.
"""
@abstractmethod
def fileno(self) -> int:
" Return the file descriptor to which we can write for the output. "
@abstractmethod
def encoding(self) -> str:
"""
Return the encoding for this output, e.g. 'utf-8'.
(This is used mainly to know which characters are supported by the
output the data, so that the UI can provide alternatives, when
required.)
"""
@abstractmethod
def write(self, data: str) -> None:
" Write text (Terminal escape sequences will be removed/escaped.) "
@abstractmethod
def write_raw(self, data: str) -> None:
" Write text. "
@abstractmethod
def set_title(self, title: str) -> None:
" Set terminal title. "
@abstractmethod
def clear_title(self) -> None:
" Clear title again. (or restore previous title.) "
@abstractmethod
def flush(self) -> None:
" Write to output stream and flush. "
@abstractmethod
def erase_screen(self) -> None:
"""
Erases the screen with the background colour and moves the cursor to
home.
"""
@abstractmethod
def enter_alternate_screen(self) -> None:
" Go to the alternate screen buffer. (For full screen applications). "
@abstractmethod
def quit_alternate_screen(self) -> None:
" Leave the alternate screen buffer. "
@abstractmethod
def enable_mouse_support(self) -> None:
" Enable mouse. "
@abstractmethod
def disable_mouse_support(self) -> None:
" Disable mouse. "
@abstractmethod
def erase_end_of_line(self) -> None:
"""
Erases from the current cursor position to the end of the current line.
"""
@abstractmethod
def erase_down(self) -> None:
"""
Erases the screen from the current line down to the bottom of the
screen.
"""
@abstractmethod
def reset_attributes(self) -> None:
" Reset color and styling attributes. "
@abstractmethod
def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None:
" Set new color and styling attributes. "
@abstractmethod
def disable_autowrap(self) -> None:
" Disable auto line wrapping. "
@abstractmethod
def enable_autowrap(self) -> None:
" Enable auto line wrapping. "
@abstractmethod
def cursor_goto(self, row: int = 0, column: int = 0) -> None:
" Move cursor position. "
@abstractmethod
def cursor_up(self, amount: int) -> None:
" Move cursor `amount` place up. "
@abstractmethod
def cursor_down(self, amount: int) -> None:
" Move cursor `amount` place down. "
@abstractmethod
def cursor_forward(self, amount: int) -> None:
" Move cursor `amount` place forward. "
@abstractmethod
def cursor_backward(self, amount: int) -> None:
" Move cursor `amount` place backward. "
@abstractmethod
def hide_cursor(self) -> None:
" Hide cursor. "
@abstractmethod
def show_cursor(self) -> None:
" Show cursor. "
def ask_for_cpr(self) -> None:
"""
Asks for a cursor position report (CPR).
(VT100 only.)
"""
@property
def responds_to_cpr(self) -> bool:
"""
`True` if the `Application` can expect to receive a CPR response after
calling `ask_for_cpr` (this will come back through the corresponding
`Input`).
This is used to determine the amount of available rows we have below
the cursor position. In the first place, we have this so that the drop
down autocompletion menus are sized according to the available space.
On Windows, we don't need this, there we have
`get_rows_below_cursor_position`.
"""
return False
@abstractmethod
def get_size(self) -> Size:
" Return the size of the output window. "
def bell(self) -> None:
" Sound bell. "
def enable_bracketed_paste(self) -> None:
" For vt100 only. "
def disable_bracketed_paste(self) -> None:
" For vt100 only. "
def scroll_buffer_to_prompt(self) -> None:
" For Win32 only. "
def get_rows_below_cursor_position(self) -> int:
" For Windows only. "
raise NotImplementedError
@abstractmethod
def get_default_color_depth(self) -> ColorDepth:
"""
Get default color depth for this output.
This value will be used if no color depth was explicitely passed to the
`Application`.
.. note::
If the `$PROMPT_TOOLKIT_COLOR_DEPTH` environment variable has been
set, then `outputs.defaults.create_output` will pass this value to
the implementation as the default_color_depth, which is returned
here. (This is not used when the output corresponds to a
prompt_toolkit SSH/Telnet session.)
"""
class DummyOutput(Output):
"""
For testing. An output class that doesn't render anything.
"""
def fileno(self) -> int:
" There is no sensible default for fileno(). "
raise NotImplementedError
def encoding(self) -> str:
return "utf-8"
def write(self, data: str) -> None:
pass
def write_raw(self, data: str) -> None:
pass
def set_title(self, title: str) -> None:
pass
def clear_title(self) -> None:
pass
def flush(self) -> None:
pass
def erase_screen(self) -> None:
pass
def enter_alternate_screen(self) -> None:
pass
def quit_alternate_screen(self) -> None:
pass
def enable_mouse_support(self) -> None:
pass
def disable_mouse_support(self) -> None:
pass
def erase_end_of_line(self) -> None:
pass
def erase_down(self) -> None:
pass
def reset_attributes(self) -> None:
pass
def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None:
pass
def disable_autowrap(self) -> None:
pass
def enable_autowrap(self) -> None:
pass
def cursor_goto(self, row: int = 0, column: int = 0) -> None:
pass
def cursor_up(self, amount: int) -> None:
pass
def cursor_down(self, amount: int) -> None:
pass
def cursor_forward(self, amount: int) -> None:
pass
def cursor_backward(self, amount: int) -> None:
pass
def hide_cursor(self) -> None:
pass
def show_cursor(self) -> None:
pass
def ask_for_cpr(self) -> None:
pass
def bell(self) -> None:
pass
def enable_bracketed_paste(self) -> None:
pass
def disable_bracketed_paste(self) -> None:
pass
def scroll_buffer_to_prompt(self) -> None:
pass
def get_size(self) -> Size:
return Size(rows=40, columns=80)
def get_rows_below_cursor_position(self) -> int:
return 40
def get_default_color_depth(self) -> ColorDepth:
return ColorDepth.DEPTH_1_BIT

View file

@ -0,0 +1,58 @@
import os
from enum import Enum
from typing import Optional
__all__ = [
"ColorDepth",
]
class ColorDepth(str, Enum):
"""
Possible color depth values for the output.
"""
value: str
#: One color only.
DEPTH_1_BIT = "DEPTH_1_BIT"
#: ANSI Colors.
DEPTH_4_BIT = "DEPTH_4_BIT"
#: The default.
DEPTH_8_BIT = "DEPTH_8_BIT"
#: 24 bit True color.
DEPTH_24_BIT = "DEPTH_24_BIT"
# Aliases.
MONOCHROME = DEPTH_1_BIT
ANSI_COLORS_ONLY = DEPTH_4_BIT
DEFAULT = DEPTH_8_BIT
TRUE_COLOR = DEPTH_24_BIT
@classmethod
def from_env(cls) -> Optional["ColorDepth"]:
"""
Return the color depth if the $PROMPT_TOOLKIT_COLOR_DEPTH environment
variable has been set.
This is a way to enforce a certain color depth in all prompt_toolkit
applications.
"""
# Check the `PROMPT_TOOLKIT_COLOR_DEPTH` environment variable.
all_values = [i.value for i in ColorDepth]
if os.environ.get("PROMPT_TOOLKIT_COLOR_DEPTH") in all_values:
return cls(os.environ["PROMPT_TOOLKIT_COLOR_DEPTH"])
return None
@classmethod
def default(cls) -> "ColorDepth":
"""
Return the default color depth for the default output.
"""
from .defaults import create_output
return create_output().get_default_color_depth()

View file

@ -0,0 +1,59 @@
from typing import Any, Optional, TextIO
from prompt_toolkit.data_structures import Size
from prompt_toolkit.renderer import Output
from .color_depth import ColorDepth
from .vt100 import Vt100_Output
from .win32 import Win32Output
__all__ = [
"ConEmuOutput",
]
class ConEmuOutput:
"""
ConEmu (Windows) output abstraction.
ConEmu is a Windows console application, but it also supports ANSI escape
sequences. This output class is actually a proxy to both `Win32Output` and
`Vt100_Output`. It uses `Win32Output` for console sizing and scrolling, but
all cursor movements and scrolling happens through the `Vt100_Output`.
This way, we can have 256 colors in ConEmu and Cmder. Rendering will be
even a little faster as well.
http://conemu.github.io/
http://gooseberrycreative.com/cmder/
"""
def __init__(
self, stdout: TextIO, default_color_depth: Optional[ColorDepth] = None
) -> None:
self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth)
self.vt100_output = Vt100_Output(
stdout, lambda: Size(0, 0), default_color_depth=default_color_depth
)
@property
def responds_to_cpr(self) -> bool:
return False # We don't need this on Windows.
def __getattr__(self, name: str) -> Any:
if name in (
"get_size",
"get_rows_below_cursor_position",
"enable_mouse_support",
"disable_mouse_support",
"scroll_buffer_to_prompt",
"get_win32_screen_buffer_info",
"enable_bracketed_paste",
"disable_bracketed_paste",
):
return getattr(self.win32_output, name)
else:
return getattr(self.vt100_output, name)
Output.register(ConEmuOutput)

View file

@ -0,0 +1,76 @@
import sys
from typing import Optional, TextIO, cast
from prompt_toolkit.patch_stdout import StdoutProxy
from prompt_toolkit.utils import (
get_term_environment_variable,
is_conemu_ansi,
is_windows,
)
from .base import Output
from .color_depth import ColorDepth
__all__ = [
"create_output",
]
def create_output(
stdout: Optional[TextIO] = None, always_prefer_tty: bool = True
) -> Output:
"""
Return an :class:`~prompt_toolkit.output.Output` instance for the command
line.
:param stdout: The stdout object
:param always_prefer_tty: When set, look for `sys.stderr` if `sys.stdout`
is not a TTY. (The prompt_toolkit render output is not meant to be
consumed by something other then a terminal, so this is a reasonable
default.)
"""
# Consider TERM and PROMPT_TOOLKIT_COLOR_DEPTH environment variables.
# Notice that PROMPT_TOOLKIT_COLOR_DEPTH value is the default that's used
# if the Application doesn't override it.
term_from_env = get_term_environment_variable()
color_depth_from_env = ColorDepth.from_env()
if stdout is None:
# By default, render to stdout. If the output is piped somewhere else,
# render to stderr.
stdout = sys.stdout
if always_prefer_tty:
for io in [sys.stdout, sys.stderr]:
if io.isatty():
stdout = io
break
# If the patch_stdout context manager has been used, then sys.stdout is
# replaced by this proxy. For prompt_toolkit applications, we want to use
# the real stdout.
while isinstance(stdout, StdoutProxy):
stdout = stdout.original_stdout
if is_windows():
from .conemu import ConEmuOutput
from .win32 import Win32Output
from .windows10 import Windows10_Output, is_win_vt100_enabled
if is_win_vt100_enabled():
return cast(
Output,
Windows10_Output(stdout, default_color_depth=color_depth_from_env),
)
if is_conemu_ansi():
return cast(
Output, ConEmuOutput(stdout, default_color_depth=color_depth_from_env)
)
else:
return Win32Output(stdout, default_color_depth=color_depth_from_env)
else:
from .vt100 import Vt100_Output
return Vt100_Output.from_pty(
stdout, term=term_from_env, default_color_depth=color_depth_from_env
)

View file

@ -0,0 +1,738 @@
"""
Output for vt100 terminals.
A lot of thanks, regarding outputting of colors, goes to the Pygments project:
(We don't rely on Pygments anymore, because many things are very custom, and
everything has been highly optimized.)
http://pygments.org/
"""
import array
import errno
import io
import os
import sys
from typing import (
IO,
Callable,
Dict,
Hashable,
Iterable,
List,
Optional,
Sequence,
Set,
TextIO,
Tuple,
)
from prompt_toolkit.data_structures import Size
from prompt_toolkit.output import Output
from prompt_toolkit.styles import ANSI_COLOR_NAMES, Attrs
from prompt_toolkit.utils import is_dumb_terminal
from .color_depth import ColorDepth
__all__ = [
"Vt100_Output",
]
FG_ANSI_COLORS = {
"ansidefault": 39,
# Low intensity.
"ansiblack": 30,
"ansired": 31,
"ansigreen": 32,
"ansiyellow": 33,
"ansiblue": 34,
"ansimagenta": 35,
"ansicyan": 36,
"ansigray": 37,
# High intensity.
"ansibrightblack": 90,
"ansibrightred": 91,
"ansibrightgreen": 92,
"ansibrightyellow": 93,
"ansibrightblue": 94,
"ansibrightmagenta": 95,
"ansibrightcyan": 96,
"ansiwhite": 97,
}
BG_ANSI_COLORS = {
"ansidefault": 49,
# Low intensity.
"ansiblack": 40,
"ansired": 41,
"ansigreen": 42,
"ansiyellow": 43,
"ansiblue": 44,
"ansimagenta": 45,
"ansicyan": 46,
"ansigray": 47,
# High intensity.
"ansibrightblack": 100,
"ansibrightred": 101,
"ansibrightgreen": 102,
"ansibrightyellow": 103,
"ansibrightblue": 104,
"ansibrightmagenta": 105,
"ansibrightcyan": 106,
"ansiwhite": 107,
}
ANSI_COLORS_TO_RGB = {
"ansidefault": (
0x00,
0x00,
0x00,
), # Don't use, 'default' doesn't really have a value.
"ansiblack": (0x00, 0x00, 0x00),
"ansigray": (0xE5, 0xE5, 0xE5),
"ansibrightblack": (0x7F, 0x7F, 0x7F),
"ansiwhite": (0xFF, 0xFF, 0xFF),
# Low intensity.
"ansired": (0xCD, 0x00, 0x00),
"ansigreen": (0x00, 0xCD, 0x00),
"ansiyellow": (0xCD, 0xCD, 0x00),
"ansiblue": (0x00, 0x00, 0xCD),
"ansimagenta": (0xCD, 0x00, 0xCD),
"ansicyan": (0x00, 0xCD, 0xCD),
# High intensity.
"ansibrightred": (0xFF, 0x00, 0x00),
"ansibrightgreen": (0x00, 0xFF, 0x00),
"ansibrightyellow": (0xFF, 0xFF, 0x00),
"ansibrightblue": (0x00, 0x00, 0xFF),
"ansibrightmagenta": (0xFF, 0x00, 0xFF),
"ansibrightcyan": (0x00, 0xFF, 0xFF),
}
assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
assert set(ANSI_COLORS_TO_RGB) == set(ANSI_COLOR_NAMES)
def _get_closest_ansi_color(r: int, g: int, b: int, exclude: Sequence[str] = ()) -> str:
"""
Find closest ANSI color. Return it by name.
:param r: Red (Between 0 and 255.)
:param g: Green (Between 0 and 255.)
:param b: Blue (Between 0 and 255.)
:param exclude: A tuple of color names to exclude. (E.g. ``('ansired', )``.)
"""
exclude = list(exclude)
# When we have a bit of saturation, avoid the gray-like colors, otherwise,
# too often the distance to the gray color is less.
saturation = abs(r - g) + abs(g - b) + abs(b - r) # Between 0..510
if saturation > 30:
exclude.extend(["ansilightgray", "ansidarkgray", "ansiwhite", "ansiblack"])
# Take the closest color.
# (Thanks to Pygments for this part.)
distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
match = "ansidefault"
for name, (r2, g2, b2) in ANSI_COLORS_TO_RGB.items():
if name != "ansidefault" and name not in exclude:
d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
if d < distance:
match = name
distance = d
return match
_ColorCodeAndName = Tuple[int, str]
class _16ColorCache:
"""
Cache which maps (r, g, b) tuples to 16 ansi colors.
:param bg: Cache for background colors, instead of foreground.
"""
def __init__(self, bg: bool = False) -> None:
self.bg = bg
self._cache: Dict[Hashable, _ColorCodeAndName] = {}
def get_code(
self, value: Tuple[int, int, int], exclude: Sequence[str] = ()
) -> _ColorCodeAndName:
"""
Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for
a given (r,g,b) value.
"""
key: Hashable = (value, tuple(exclude))
cache = self._cache
if key not in cache:
cache[key] = self._get(value, exclude)
return cache[key]
def _get(
self, value: Tuple[int, int, int], exclude: Sequence[str] = ()
) -> _ColorCodeAndName:
r, g, b = value
match = _get_closest_ansi_color(r, g, b, exclude=exclude)
# Turn color name into code.
if self.bg:
code = BG_ANSI_COLORS[match]
else:
code = FG_ANSI_COLORS[match]
return code, match
class _256ColorCache(Dict[Tuple[int, int, int], int]):
"""
Cache which maps (r, g, b) tuples to 256 colors.
"""
def __init__(self) -> None:
# Build color table.
colors: List[Tuple[int, int, int]] = []
# colors 0..15: 16 basic colors
colors.append((0x00, 0x00, 0x00)) # 0
colors.append((0xCD, 0x00, 0x00)) # 1
colors.append((0x00, 0xCD, 0x00)) # 2
colors.append((0xCD, 0xCD, 0x00)) # 3
colors.append((0x00, 0x00, 0xEE)) # 4
colors.append((0xCD, 0x00, 0xCD)) # 5
colors.append((0x00, 0xCD, 0xCD)) # 6
colors.append((0xE5, 0xE5, 0xE5)) # 7
colors.append((0x7F, 0x7F, 0x7F)) # 8
colors.append((0xFF, 0x00, 0x00)) # 9
colors.append((0x00, 0xFF, 0x00)) # 10
colors.append((0xFF, 0xFF, 0x00)) # 11
colors.append((0x5C, 0x5C, 0xFF)) # 12
colors.append((0xFF, 0x00, 0xFF)) # 13
colors.append((0x00, 0xFF, 0xFF)) # 14
colors.append((0xFF, 0xFF, 0xFF)) # 15
# colors 16..232: the 6x6x6 color cube
valuerange = (0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF)
for i in range(217):
r = valuerange[(i // 36) % 6]
g = valuerange[(i // 6) % 6]
b = valuerange[i % 6]
colors.append((r, g, b))
# colors 233..253: grayscale
for i in range(1, 22):
v = 8 + i * 10
colors.append((v, v, v))
self.colors = colors
def __missing__(self, value: Tuple[int, int, int]) -> int:
r, g, b = value
# Find closest color.
# (Thanks to Pygments for this!)
distance = 257 * 257 * 3 # "infinity" (>distance from #000000 to #ffffff)
match = 0
for i, (r2, g2, b2) in enumerate(self.colors):
if i >= 16: # XXX: We ignore the 16 ANSI colors when mapping RGB
# to the 256 colors, because these highly depend on
# the color scheme of the terminal.
d = (r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2
if d < distance:
match = i
distance = d
# Turn color name into code.
self[value] = match
return match
_16_fg_colors = _16ColorCache(bg=False)
_16_bg_colors = _16ColorCache(bg=True)
_256_colors = _256ColorCache()
class _EscapeCodeCache(Dict[Attrs, str]):
"""
Cache for VT100 escape codes. It maps
(fgcolor, bgcolor, bold, underline, reverse) tuples to VT100 escape sequences.
:param true_color: When True, use 24bit colors instead of 256 colors.
"""
def __init__(self, color_depth: ColorDepth) -> None:
self.color_depth = color_depth
def __missing__(self, attrs: Attrs) -> str:
fgcolor, bgcolor, bold, underline, italic, blink, reverse, hidden = attrs
parts: List[str] = []
parts.extend(self._colors_to_code(fgcolor or "", bgcolor or ""))
if bold:
parts.append("1")
if italic:
parts.append("3")
if blink:
parts.append("5")
if underline:
parts.append("4")
if reverse:
parts.append("7")
if hidden:
parts.append("8")
if parts:
result = "\x1b[0;" + ";".join(parts) + "m"
else:
result = "\x1b[0m"
self[attrs] = result
return result
def _color_name_to_rgb(self, color: str) -> Tuple[int, int, int]:
" Turn 'ffffff', into (0xff, 0xff, 0xff). "
try:
rgb = int(color, 16)
except ValueError:
raise
else:
r = (rgb >> 16) & 0xFF
g = (rgb >> 8) & 0xFF
b = rgb & 0xFF
return r, g, b
def _colors_to_code(self, fg_color: str, bg_color: str) -> Iterable[str]:
"""
Return a tuple with the vt100 values that represent this color.
"""
# When requesting ANSI colors only, and both fg/bg color were converted
# to ANSI, ensure that the foreground and background color are not the
# same. (Unless they were explicitly defined to be the same color.)
fg_ansi = ""
def get(color: str, bg: bool) -> List[int]:
nonlocal fg_ansi
table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
if not color or self.color_depth == ColorDepth.DEPTH_1_BIT:
return []
# 16 ANSI colors. (Given by name.)
elif color in table:
return [table[color]]
# RGB colors. (Defined as 'ffffff'.)
else:
try:
rgb = self._color_name_to_rgb(color)
except ValueError:
return []
# When only 16 colors are supported, use that.
if self.color_depth == ColorDepth.DEPTH_4_BIT:
if bg: # Background.
if fg_color != bg_color:
exclude = [fg_ansi]
else:
exclude = []
code, name = _16_bg_colors.get_code(rgb, exclude=exclude)
return [code]
else: # Foreground.
code, name = _16_fg_colors.get_code(rgb)
fg_ansi = name
return [code]
# True colors. (Only when this feature is enabled.)
elif self.color_depth == ColorDepth.DEPTH_24_BIT:
r, g, b = rgb
return [(48 if bg else 38), 2, r, g, b]
# 256 RGB colors.
else:
return [(48 if bg else 38), 5, _256_colors[rgb]]
result: List[int] = []
result.extend(get(fg_color, False))
result.extend(get(bg_color, True))
return map(str, result)
def _get_size(fileno: int) -> Tuple[int, int]:
# Thanks to fabric (fabfile.org), and
# http://sqizit.bartletts.id.au/2011/02/14/pseudo-terminals-in-python/
"""
Get the size of this pseudo terminal.
:param fileno: stdout.fileno()
:returns: A (rows, cols) tuple.
"""
# Inline imports, because these modules are not available on Windows.
# (This file is used by ConEmuOutput, which is used on Windows.)
import fcntl
import termios
# Buffer for the C call
buf = array.array("h", [0, 0, 0, 0])
# Do TIOCGWINSZ (Get)
# Note: We should not pass 'True' as a fourth parameter to 'ioctl'. (True
# is the default.) This causes segmentation faults on some systems.
# See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/364
fcntl.ioctl(fileno, termios.TIOCGWINSZ, buf) # type: ignore
# Return rows, cols
return buf[0], buf[1]
class Vt100_Output(Output):
"""
:param get_size: A callable which returns the `Size` of the output terminal.
:param stdout: Any object with has a `write` and `flush` method + an 'encoding' property.
:param term: The terminal environment variable. (xterm, xterm-256color, linux, ...)
:param write_binary: Encode the output before writing it. If `True` (the
default), the `stdout` object is supposed to expose an `encoding` attribute.
"""
# For the error messages. Only display "Output is not a terminal" once per
# file descriptor.
_fds_not_a_terminal: Set[int] = set()
def __init__(
self,
stdout: TextIO,
get_size: Callable[[], Size],
term: Optional[str] = None,
write_binary: bool = True,
default_color_depth: Optional[ColorDepth] = None,
) -> None:
assert all(hasattr(stdout, a) for a in ("write", "flush"))
if write_binary:
assert hasattr(stdout, "encoding")
self._buffer: List[str] = []
self.stdout = stdout
self.write_binary = write_binary
self.default_color_depth = default_color_depth
self._get_size = get_size
self.term = term
# Cache for escape codes.
self._escape_code_caches: Dict[ColorDepth, _EscapeCodeCache] = {
ColorDepth.DEPTH_1_BIT: _EscapeCodeCache(ColorDepth.DEPTH_1_BIT),
ColorDepth.DEPTH_4_BIT: _EscapeCodeCache(ColorDepth.DEPTH_4_BIT),
ColorDepth.DEPTH_8_BIT: _EscapeCodeCache(ColorDepth.DEPTH_8_BIT),
ColorDepth.DEPTH_24_BIT: _EscapeCodeCache(ColorDepth.DEPTH_24_BIT),
}
@classmethod
def from_pty(
cls,
stdout: TextIO,
term: Optional[str] = None,
default_color_depth: Optional[ColorDepth] = None,
) -> "Vt100_Output":
"""
Create an Output class from a pseudo terminal.
(This will take the dimensions by reading the pseudo
terminal attributes.)
"""
fd: Optional[int]
# Normally, this requires a real TTY device, but people instantiate
# this class often during unit tests as well. For convenience, we print
# an error message, use standard dimensions, and go on.
try:
fd = stdout.fileno()
except io.UnsupportedOperation:
fd = None
if not stdout.isatty() and (fd is None or fd not in cls._fds_not_a_terminal):
msg = "Warning: Output is not a terminal (fd=%r).\n"
sys.stderr.write(msg % fd)
sys.stderr.flush()
if fd is not None:
cls._fds_not_a_terminal.add(fd)
def get_size() -> Size:
# If terminal (incorrectly) reports its size as 0, pick a
# reasonable default. See
# https://github.com/ipython/ipython/issues/10071
rows, columns = (None, None)
# It is possible that `stdout` is no longer a TTY device at this
# point. In that case we get an `OSError` in the ioctl call in
# `get_size`. See:
# https://github.com/prompt-toolkit/python-prompt-toolkit/pull/1021
try:
rows, columns = _get_size(stdout.fileno())
except OSError:
pass
return Size(rows=rows or 24, columns=columns or 80)
return cls(stdout, get_size, term=term, default_color_depth=default_color_depth)
def get_size(self) -> Size:
return self._get_size()
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_raw(self, data: str) -> None:
"""
Write raw data to output.
"""
self._buffer.append(data)
def write(self, data: str) -> None:
"""
Write text to output.
(Removes vt100 escape codes. -- used for safely writing text.)
"""
self._buffer.append(data.replace("\x1b", "?"))
def set_title(self, title: str) -> None:
"""
Set terminal title.
"""
if self.term not in (
"linux",
"eterm-color",
): # Not supported by the Linux console.
self.write_raw(
"\x1b]2;%s\x07" % title.replace("\x1b", "").replace("\x07", "")
)
def clear_title(self) -> None:
self.set_title("")
def erase_screen(self) -> None:
"""
Erases the screen with the background colour and moves the cursor to
home.
"""
self.write_raw("\x1b[2J")
def enter_alternate_screen(self) -> None:
self.write_raw("\x1b[?1049h\x1b[H")
def quit_alternate_screen(self) -> None:
self.write_raw("\x1b[?1049l")
def enable_mouse_support(self) -> None:
self.write_raw("\x1b[?1000h")
# Enable urxvt Mouse mode. (For terminals that understand this.)
self.write_raw("\x1b[?1015h")
# Also enable Xterm SGR mouse mode. (For terminals that understand this.)
self.write_raw("\x1b[?1006h")
# Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr
# extensions.
def disable_mouse_support(self) -> None:
self.write_raw("\x1b[?1000l")
self.write_raw("\x1b[?1015l")
self.write_raw("\x1b[?1006l")
def erase_end_of_line(self) -> None:
"""
Erases from the current cursor position to the end of the current line.
"""
self.write_raw("\x1b[K")
def erase_down(self) -> None:
"""
Erases the screen from the current line down to the bottom of the
screen.
"""
self.write_raw("\x1b[J")
def reset_attributes(self) -> None:
self.write_raw("\x1b[0m")
def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None:
"""
Create new style and output.
:param attrs: `Attrs` instance.
"""
# Get current depth.
escape_code_cache = self._escape_code_caches[color_depth]
# Write escape character.
self.write_raw(escape_code_cache[attrs])
def disable_autowrap(self) -> None:
self.write_raw("\x1b[?7l")
def enable_autowrap(self) -> None:
self.write_raw("\x1b[?7h")
def enable_bracketed_paste(self) -> None:
self.write_raw("\x1b[?2004h")
def disable_bracketed_paste(self) -> None:
self.write_raw("\x1b[?2004l")
def cursor_goto(self, row: int = 0, column: int = 0) -> None:
"""
Move cursor position.
"""
self.write_raw("\x1b[%i;%iH" % (row, column))
def cursor_up(self, amount: int) -> None:
if amount == 0:
pass
elif amount == 1:
self.write_raw("\x1b[A")
else:
self.write_raw("\x1b[%iA" % amount)
def cursor_down(self, amount: int) -> None:
if amount == 0:
pass
elif amount == 1:
# Note: Not the same as '\n', '\n' can cause the window content to
# scroll.
self.write_raw("\x1b[B")
else:
self.write_raw("\x1b[%iB" % amount)
def cursor_forward(self, amount: int) -> None:
if amount == 0:
pass
elif amount == 1:
self.write_raw("\x1b[C")
else:
self.write_raw("\x1b[%iC" % amount)
def cursor_backward(self, amount: int) -> None:
if amount == 0:
pass
elif amount == 1:
self.write_raw("\b") # '\x1b[D'
else:
self.write_raw("\x1b[%iD" % amount)
def hide_cursor(self) -> None:
self.write_raw("\x1b[?25l")
def show_cursor(self) -> None:
self.write_raw("\x1b[?12l\x1b[?25h") # Stop blinking cursor and show.
def flush(self) -> None:
"""
Write to output stream and flush.
"""
if not self._buffer:
return
data = "".join(self._buffer)
try:
# (We try to encode ourself, because that way we can replace
# characters that don't exist in the character set, avoiding
# UnicodeEncodeError crashes. E.g. u'\xb7' does not appear in 'ascii'.)
# My Arch Linux installation of july 2015 reported 'ANSI_X3.4-1968'
# for sys.stdout.encoding in xterm.
out: IO
if self.write_binary:
if hasattr(self.stdout, "buffer"):
out = self.stdout.buffer # Py3.
else:
out = self.stdout
out.write(data.encode(self.stdout.encoding or "utf-8", "replace"))
else:
self.stdout.write(data)
self.stdout.flush()
except IOError as e:
if e.args and e.args[0] == errno.EINTR:
# Interrupted system call. Can happen in case of a window
# resize signal. (Just ignore. The resize handler will render
# again anyway.)
pass
elif e.args and e.args[0] == 0:
# This can happen when there is a lot of output and the user
# sends a KeyboardInterrupt by pressing Control-C. E.g. in
# a Python REPL when we execute "while True: print('test')".
# (The `ptpython` REPL uses this `Output` class instead of
# `stdout` directly -- in order to be network transparent.)
# So, just ignore.
pass
else:
raise
self._buffer = []
def ask_for_cpr(self) -> None:
"""
Asks for a cursor position report (CPR).
"""
self.write_raw("\x1b[6n")
self.flush()
@property
def responds_to_cpr(self) -> bool:
# When the input is a tty, we assume that CPR is supported.
# It's not when the input is piped from Pexpect.
if os.environ.get("PROMPT_TOOLKIT_NO_CPR", "") == "1":
return False
if is_dumb_terminal(self.term):
return False
try:
return self.stdout.isatty()
except ValueError:
return False # ValueError: I/O operation on closed file
def bell(self) -> None:
" Sound bell. "
self.write_raw("\a")
self.flush()
def get_default_color_depth(self) -> ColorDepth:
"""
Return the default color depth for a vt100 terminal, according to the
our term value.
We prefer 256 colors almost always, because this is what most terminals
support these days, and is a good default.
"""
if self.default_color_depth is not None:
return self.default_color_depth
term = self.term
if term is None:
return ColorDepth.DEFAULT
if is_dumb_terminal(term):
return ColorDepth.DEPTH_1_BIT
if term in ("linux", "eterm-color"):
return ColorDepth.DEPTH_4_BIT
return ColorDepth.DEFAULT

View file

@ -0,0 +1,656 @@
import os
from ctypes import (
ArgumentError,
byref,
c_char,
c_long,
c_uint,
c_ulong,
pointer,
windll,
)
from ctypes.wintypes import DWORD, HANDLE
from typing import Dict, List, Optional, TextIO, Tuple
from prompt_toolkit.data_structures import Size
from prompt_toolkit.renderer import Output
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 .color_depth import ColorDepth
__all__ = [
"Win32Output",
]
def _coord_byval(coord):
"""
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)
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: Optional[ColorDepth] = None,
) -> None:
self.use_complete_width = use_complete_width
self.default_color_depth = default_color_depth
self._buffer: List[str] = []
self.stdout = 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, *a, **kw):
"""
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(
(" Error in %r %r %s\n" % (func.__name__, e, e)).encode("utf-8")
)
def get_win32_screen_buffer_info(self):
"""
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, length):
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, 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(sr.X, 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(max(0, sr.X + amount), 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 oder 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
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 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
@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
# For now, by default, always use 4 bit color on Windows 10 by default,
# even when vt100 escape sequences with
# ENABLE_VIRTUAL_TERMINAL_PROCESSING are supported. We don't have a
# reliable way yet to know whether our console supports true color or
# only 4-bit.
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) -> 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]

View file

@ -0,0 +1,104 @@
from ctypes import byref, windll
from ctypes.wintypes import DWORD, HANDLE
from typing import Any, Optional, TextIO
from prompt_toolkit.data_structures import Size
from prompt_toolkit.renderer import Output
from prompt_toolkit.utils import is_windows
from prompt_toolkit.win32_types import STD_OUTPUT_HANDLE
from .color_depth import ColorDepth
from .vt100 import Vt100_Output
from .win32 import Win32Output
__all__ = [
"Windows10_Output",
]
# See: https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms686033(v=vs.85).aspx
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
class Windows10_Output:
"""
Windows 10 output abstraction. This enables and uses vt100 escape sequences.
"""
def __init__(
self, stdout: TextIO, default_color_depth: Optional[ColorDepth] = None
) -> None:
self.win32_output = Win32Output(stdout, default_color_depth=default_color_depth)
self.vt100_output = Vt100_Output(
stdout, lambda: Size(0, 0), default_color_depth=default_color_depth
)
self._hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE))
def flush(self) -> None:
"""
Write to output stream and flush.
"""
original_mode = DWORD(0)
# Remember the previous console mode.
windll.kernel32.GetConsoleMode(self._hconsole, byref(original_mode))
# Enable processing of vt100 sequences.
windll.kernel32.SetConsoleMode(
self._hconsole,
DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING),
)
try:
self.vt100_output.flush()
finally:
# Restore console mode.
windll.kernel32.SetConsoleMode(self._hconsole, original_mode)
@property
def responds_to_cpr(self) -> bool:
return False # We don't need this on Windows.
def __getattr__(self, name: str) -> Any:
if name in (
"get_size",
"get_rows_below_cursor_position",
"enable_mouse_support",
"disable_mouse_support",
"scroll_buffer_to_prompt",
"get_win32_screen_buffer_info",
"enable_bracketed_paste",
"disable_bracketed_paste",
"get_default_color_depth",
):
return getattr(self.win32_output, name)
else:
return getattr(self.vt100_output, name)
Output.register(Windows10_Output)
def is_win_vt100_enabled() -> bool:
"""
Returns True when we're running Windows and VT100 escape sequences are
supported.
"""
if not is_windows():
return False
hconsole = HANDLE(windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE))
# Get original console mode.
original_mode = DWORD(0)
windll.kernel32.GetConsoleMode(hconsole, byref(original_mode))
try:
# Try to enable VT100 sequences.
result = windll.kernel32.SetConsoleMode(
hconsole, DWORD(ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
)
return result == 1
finally:
windll.kernel32.SetConsoleMode(hconsole, original_mode)