feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
from __future__ import unicode_literals
from prompt_toolkit.renderer import Output
from .win32_output import Win32Output
from .vt100_output import Vt100_Output
__all__ = (
'ConEmuOutput',
)
class ConEmuOutput(object):
"""
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):
self.win32_output = Win32Output(stdout)
self.vt100_output = Vt100_Output(stdout, lambda: None)
def __getattr__(self, name):
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,512 @@
"""
Parser for VT100 input stream.
"""
from __future__ import unicode_literals
import os
import re
import six
import termios
import tty
from six.moves import range
from ..keys import Keys
from ..key_binding.input_processor import KeyPress
__all__ = (
'InputStream',
'raw_mode',
'cooked_mode',
)
_DEBUG_RENDERER_INPUT = False
_DEBUG_RENDERER_INPUT_FILENAME = 'prompt-toolkit-render-input.log'
# Regex matching any CPR response
# (Note that we use '\Z' instead of '$', because '$' could include a trailing
# newline.)
_cpr_response_re = re.compile('^' + re.escape('\x1b[') + r'\d+;\d+R\Z')
# Mouse events:
# Typical: "Esc[MaB*" Urxvt: "Esc[96;14;13M" and for Xterm SGR: "Esc[<64;85;12M"
_mouse_event_re = re.compile('^' + re.escape('\x1b[') + r'(<?[\d;]+[mM]|M...)\Z')
# Regex matching any valid prefix of a CPR response.
# (Note that it doesn't contain the last character, the 'R'. The prefix has to
# be shorter.)
_cpr_response_prefix_re = re.compile('^' + re.escape('\x1b[') + r'[\d;]*\Z')
_mouse_event_prefix_re = re.compile('^' + re.escape('\x1b[') + r'(<?[\d;]*|M.{0,2})\Z')
class _Flush(object):
""" Helper object to indicate flush operation to the parser. """
pass
# Mapping of vt100 escape codes to Keys.
ANSI_SEQUENCES = {
'\x1b': Keys.Escape,
'\x00': Keys.ControlSpace, # Control-Space (Also for Ctrl-@)
'\x01': Keys.ControlA, # Control-A (home)
'\x02': Keys.ControlB, # Control-B (emacs cursor left)
'\x03': Keys.ControlC, # Control-C (interrupt)
'\x04': Keys.ControlD, # Control-D (exit)
'\x05': Keys.ControlE, # Contrel-E (end)
'\x06': Keys.ControlF, # Control-F (cursor forward)
'\x07': Keys.ControlG, # Control-G
'\x08': Keys.ControlH, # Control-H (8) (Identical to '\b')
'\x09': Keys.ControlI, # Control-I (9) (Identical to '\t')
'\x0a': Keys.ControlJ, # Control-J (10) (Identical to '\n')
'\x0b': Keys.ControlK, # Control-K (delete until end of line; vertical tab)
'\x0c': Keys.ControlL, # Control-L (clear; form feed)
'\x0d': Keys.ControlM, # Control-M (13) (Identical to '\r')
'\x0e': Keys.ControlN, # Control-N (14) (history forward)
'\x0f': Keys.ControlO, # Control-O (15)
'\x10': Keys.ControlP, # Control-P (16) (history back)
'\x11': Keys.ControlQ, # Control-Q
'\x12': Keys.ControlR, # Control-R (18) (reverse search)
'\x13': Keys.ControlS, # Control-S (19) (forward search)
'\x14': Keys.ControlT, # Control-T
'\x15': Keys.ControlU, # Control-U
'\x16': Keys.ControlV, # Control-V
'\x17': Keys.ControlW, # Control-W
'\x18': Keys.ControlX, # Control-X
'\x19': Keys.ControlY, # Control-Y (25)
'\x1a': Keys.ControlZ, # Control-Z
'\x1c': Keys.ControlBackslash, # Both Control-\ and Ctrl-|
'\x1d': Keys.ControlSquareClose, # Control-]
'\x1e': Keys.ControlCircumflex, # Control-^
'\x1f': Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hypen.)
'\x7f': Keys.Backspace, # (127) Backspace
'\x1b[A': Keys.Up,
'\x1b[B': Keys.Down,
'\x1b[C': Keys.Right,
'\x1b[D': Keys.Left,
'\x1b[H': Keys.Home,
'\x1bOH': Keys.Home,
'\x1b[F': Keys.End,
'\x1bOF': Keys.End,
'\x1b[3~': Keys.Delete,
'\x1b[3;2~': Keys.ShiftDelete, # xterm, gnome-terminal.
'\x1b[3;5~': Keys.ControlDelete, # xterm, gnome-terminal.
'\x1b[1~': Keys.Home, # tmux
'\x1b[4~': Keys.End, # tmux
'\x1b[5~': Keys.PageUp,
'\x1b[6~': Keys.PageDown,
'\x1b[7~': Keys.Home, # xrvt
'\x1b[8~': Keys.End, # xrvt
'\x1b[Z': Keys.BackTab, # shift + tab
'\x1b[2~': Keys.Insert,
'\x1bOP': Keys.F1,
'\x1bOQ': Keys.F2,
'\x1bOR': Keys.F3,
'\x1bOS': Keys.F4,
'\x1b[[A': Keys.F1, # Linux console.
'\x1b[[B': Keys.F2, # Linux console.
'\x1b[[C': Keys.F3, # Linux console.
'\x1b[[D': Keys.F4, # Linux console.
'\x1b[[E': Keys.F5, # Linux console.
'\x1b[11~': Keys.F1, # rxvt-unicode
'\x1b[12~': Keys.F2, # rxvt-unicode
'\x1b[13~': Keys.F3, # rxvt-unicode
'\x1b[14~': Keys.F4, # rxvt-unicode
'\x1b[15~': Keys.F5,
'\x1b[17~': Keys.F6,
'\x1b[18~': Keys.F7,
'\x1b[19~': Keys.F8,
'\x1b[20~': Keys.F9,
'\x1b[21~': Keys.F10,
'\x1b[23~': Keys.F11,
'\x1b[24~': Keys.F12,
'\x1b[25~': Keys.F13,
'\x1b[26~': Keys.F14,
'\x1b[28~': Keys.F15,
'\x1b[29~': Keys.F16,
'\x1b[31~': Keys.F17,
'\x1b[32~': Keys.F18,
'\x1b[33~': Keys.F19,
'\x1b[34~': Keys.F20,
# Xterm
'\x1b[1;2P': Keys.F13,
'\x1b[1;2Q': Keys.F14,
# '\x1b[1;2R': Keys.F15, # Conflicts with CPR response.
'\x1b[1;2S': Keys.F16,
'\x1b[15;2~': Keys.F17,
'\x1b[17;2~': Keys.F18,
'\x1b[18;2~': Keys.F19,
'\x1b[19;2~': Keys.F20,
'\x1b[20;2~': Keys.F21,
'\x1b[21;2~': Keys.F22,
'\x1b[23;2~': Keys.F23,
'\x1b[24;2~': Keys.F24,
'\x1b[1;5A': Keys.ControlUp, # Cursor Mode
'\x1b[1;5B': Keys.ControlDown, # Cursor Mode
'\x1b[1;5C': Keys.ControlRight, # Cursor Mode
'\x1b[1;5D': Keys.ControlLeft, # Cursor Mode
'\x1b[1;2A': Keys.ShiftUp,
'\x1b[1;2B': Keys.ShiftDown,
'\x1b[1;2C': Keys.ShiftRight,
'\x1b[1;2D': Keys.ShiftLeft,
# Tmux sends following keystrokes when control+arrow is pressed, but for
# Emacs ansi-term sends the same sequences for normal arrow keys. Consider
# it a normal arrow press, because that's more important.
'\x1bOA': Keys.Up,
'\x1bOB': Keys.Down,
'\x1bOC': Keys.Right,
'\x1bOD': Keys.Left,
'\x1b[5A': Keys.ControlUp,
'\x1b[5B': Keys.ControlDown,
'\x1b[5C': Keys.ControlRight,
'\x1b[5D': Keys.ControlLeft,
'\x1bOc': Keys.ControlRight, # rxvt
'\x1bOd': Keys.ControlLeft, # rxvt
'\x1b[200~': Keys.BracketedPaste, # Start of bracketed paste.
# Meta + arrow keys. Several terminals handle this differently.
# The following sequences are for xterm and gnome-terminal.
# (Iterm sends ESC followed by the normal arrow_up/down/left/right
# sequences, and the OSX Terminal sends ESCb and ESCf for "alt
# arrow_left" and "alt arrow_right." We don't handle these
# explicitely, in here, because would could not distinguesh between
# pressing ESC (to go to Vi navigation mode), followed by just the
# 'b' or 'f' key. These combinations are handled in
# the input processor.)
'\x1b[1;3D': (Keys.Escape, Keys.Left),
'\x1b[1;3C': (Keys.Escape, Keys.Right),
'\x1b[1;3A': (Keys.Escape, Keys.Up),
'\x1b[1;3B': (Keys.Escape, Keys.Down),
# Sequences generated by numpad 5. Not sure what it means. (It doesn't
# appear in 'infocmp'. Just ignore.
'\x1b[E': Keys.Ignore, # Xterm.
'\x1b[G': Keys.Ignore, # Linux console.
}
class _IsPrefixOfLongerMatchCache(dict):
"""
Dictiory that maps input sequences to a boolean indicating whether there is
any key that start with this characters.
"""
def __missing__(self, prefix):
# (hard coded) If this could be a prefix of a CPR response, return
# True.
if (_cpr_response_prefix_re.match(prefix) or _mouse_event_prefix_re.match(prefix)):
result = True
else:
# If this could be a prefix of anything else, also return True.
result = any(v for k, v in ANSI_SEQUENCES.items() if k.startswith(prefix) and k != prefix)
self[prefix] = result
return result
_IS_PREFIX_OF_LONGER_MATCH_CACHE = _IsPrefixOfLongerMatchCache()
class InputStream(object):
"""
Parser for VT100 input stream.
Feed the data through the `feed` method and the correct callbacks of the
`input_processor` will be called.
::
def callback(key):
pass
i = InputStream(callback)
i.feed('data\x01...')
:attr input_processor: :class:`~prompt_toolkit.key_binding.InputProcessor` instance.
"""
# Lookup table of ANSI escape sequences for a VT100 terminal
# Hint: in order to know what sequences your terminal writes to stdin, run
# "od -c" and start typing.
def __init__(self, feed_key_callback):
assert callable(feed_key_callback)
self.feed_key_callback = feed_key_callback
self.reset()
if _DEBUG_RENDERER_INPUT:
self.LOG = open(_DEBUG_RENDERER_INPUT_FILENAME, 'ab')
def reset(self, request=False):
self._in_bracketed_paste = False
self._start_parser()
def _start_parser(self):
"""
Start the parser coroutine.
"""
self._input_parser = self._input_parser_generator()
self._input_parser.send(None)
def _get_match(self, prefix):
"""
Return the key that maps to this prefix.
"""
# (hard coded) If we match a CPR response, return Keys.CPRResponse.
# (This one doesn't fit in the ANSI_SEQUENCES, because it contains
# integer variables.)
if _cpr_response_re.match(prefix):
return Keys.CPRResponse
elif _mouse_event_re.match(prefix):
return Keys.Vt100MouseEvent
# Otherwise, use the mappings.
try:
return ANSI_SEQUENCES[prefix]
except KeyError:
return None
def _input_parser_generator(self):
"""
Coroutine (state machine) for the input parser.
"""
prefix = ''
retry = False
flush = False
while True:
flush = False
if retry:
retry = False
else:
# Get next character.
c = yield
if c == _Flush:
flush = True
else:
prefix += c
# If we have some data, check for matches.
if prefix:
is_prefix_of_longer_match = _IS_PREFIX_OF_LONGER_MATCH_CACHE[prefix]
match = self._get_match(prefix)
# Exact matches found, call handlers..
if (flush or not is_prefix_of_longer_match) and match:
self._call_handler(match, prefix)
prefix = ''
# No exact match found.
elif (flush or not is_prefix_of_longer_match) and not match:
found = False
retry = True
# Loop over the input, try the longest match first and
# shift.
for i in range(len(prefix), 0, -1):
match= self._get_match(prefix[:i])
if match:
self._call_handler(match, prefix[:i])
prefix = prefix[i:]
found = True
if not found:
self._call_handler(prefix[0], prefix[0])
prefix = prefix[1:]
def _call_handler(self, key, insert_text):
"""
Callback to handler.
"""
if isinstance(key, tuple):
for k in key:
self._call_handler(k, insert_text)
else:
if key == Keys.BracketedPaste:
self._in_bracketed_paste = True
self._paste_buffer = ''
else:
self.feed_key_callback(KeyPress(key, insert_text))
def feed(self, data):
"""
Feed the input stream.
:param data: Input string (unicode).
"""
assert isinstance(data, six.text_type)
if _DEBUG_RENDERER_INPUT:
self.LOG.write(repr(data).encode('utf-8') + b'\n')
self.LOG.flush()
# Handle bracketed paste. (We bypass the parser that matches all other
# key presses and keep reading input until we see the end mark.)
# This is much faster then parsing character by character.
if self._in_bracketed_paste:
self._paste_buffer += data
end_mark = '\x1b[201~'
if end_mark in self._paste_buffer:
end_index = self._paste_buffer.index(end_mark)
# Feed content to key bindings.
paste_content = self._paste_buffer[:end_index]
self.feed_key_callback(KeyPress(Keys.BracketedPaste, paste_content))
# Quit bracketed paste mode and handle remaining input.
self._in_bracketed_paste = False
remaining = self._paste_buffer[end_index + len(end_mark):]
self._paste_buffer = ''
self.feed(remaining)
# Handle normal input character by character.
else:
for i, c in enumerate(data):
if self._in_bracketed_paste:
# Quit loop and process from this position when the parser
# entered bracketed paste.
self.feed(data[i:])
break
else:
# Replace \r by \n. (Some clients send \r instead of \n
# when enter is pressed. E.g. telnet and some other
# terminals.)
# XXX: We should remove this in a future version. It *is*
# now possible to recognise the difference.
# (We remove ICRNL/INLCR/IGNCR below.)
# However, this breaks IPython and maybe other applications,
# because they bind ControlJ (\n) for handling the Enter key.
# When this is removed, replace Enter=ControlJ by
# Enter=ControlM in keys.py.
if c == '\r':
c = '\n'
self._input_parser.send(c)
def flush(self):
"""
Flush the buffer of the input stream.
This will allow us to handle the escape key (or maybe meta) sooner.
The input received by the escape key is actually the same as the first
characters of e.g. Arrow-Up, so without knowing what follows the escape
sequence, we don't know whether escape has been pressed, or whether
it's something else. This flush function should be called after a
timeout, and processes everything that's still in the buffer as-is, so
without assuming any characters will folow.
"""
self._input_parser.send(_Flush)
def feed_and_flush(self, data):
"""
Wrapper around ``feed`` and ``flush``.
"""
self.feed(data)
self.flush()
class raw_mode(object):
"""
::
with raw_mode(stdin):
''' the pseudo-terminal stdin is now used in raw mode '''
We ignore errors when executing `tcgetattr` fails.
"""
# There are several reasons for ignoring errors:
# 1. To avoid the "Inappropriate ioctl for device" crash if somebody would
# execute this code (In a Python REPL, for instance):
#
# import os; f = open(os.devnull); os.dup2(f.fileno(), 0)
#
# The result is that the eventloop will stop correctly, because it has
# to logic to quit when stdin is closed. However, we should not fail at
# this point. See:
# https://github.com/jonathanslenders/python-prompt-toolkit/pull/393
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/392
# 2. Related, when stdin is an SSH pipe, and no full terminal was allocated.
# See: https://github.com/jonathanslenders/python-prompt-toolkit/pull/165
def __init__(self, fileno):
self.fileno = fileno
try:
self.attrs_before = termios.tcgetattr(fileno)
except termios.error:
# Ignore attribute errors.
self.attrs_before = None
def __enter__(self):
# NOTE: On os X systems, using pty.setraw() fails. Therefor we are using this:
try:
newattr = termios.tcgetattr(self.fileno)
except termios.error:
pass
else:
newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG])
newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG])
termios.tcsetattr(self.fileno, termios.TCSANOW, newattr)
# Put the terminal in cursor mode. (Instead of application mode.)
os.write(self.fileno, b'\x1b[?1l')
@classmethod
def _patch_lflag(cls, attrs):
return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
@classmethod
def _patch_iflag(cls, attrs):
return attrs & ~(
# Disable XON/XOFF flow control on output and input.
# (Don't capture Ctrl-S and Ctrl-Q.)
# Like executing: "stty -ixon."
termios.IXON | termios.IXOFF |
# Don't translate carriage return into newline on input.
termios.ICRNL | termios.INLCR | termios.IGNCR
)
def __exit__(self, *a, **kw):
if self.attrs_before is not None:
try:
termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before)
except termios.error:
pass
# # Put the terminal in application mode.
# self._stdout.write('\x1b[?1h')
class cooked_mode(raw_mode):
"""
The opposide of ``raw_mode``, used when we need cooked mode inside a
`raw_mode` block. Used in `CommandLineInterface.run_in_terminal`.::
with cooked_mode(stdin):
''' the pseudo-terminal stdin is now used in cooked mode. '''
"""
@classmethod
def _patch_lflag(cls, attrs):
return attrs | (termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
@classmethod
def _patch_iflag(cls, attrs):
# Turn the ICRNL flag back on. (Without this, calling `input()` in
# run_in_terminal doesn't work and displays ^M instead. Ptpython
# evaluates commands using `run_in_terminal`, so it's important that
# they translate ^M back into ^J.)
return attrs | termios.ICRNL

View File

@@ -0,0 +1,630 @@
"""
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/
"""
from __future__ import unicode_literals
from prompt_toolkit.filters import to_simple_filter, Condition
from prompt_toolkit.layout.screen import Size
from prompt_toolkit.renderer import Output
from prompt_toolkit.styles import ANSI_COLOR_NAMES
from six.moves import range
import array
import errno
import os
import six
__all__ = (
'Vt100_Output',
)
FG_ANSI_COLORS = {
'ansidefault': 39,
# Low intensity.
'ansiblack': 30,
'ansidarkred': 31,
'ansidarkgreen': 32,
'ansibrown': 33,
'ansidarkblue': 34,
'ansipurple': 35,
'ansiteal': 36,
'ansilightgray': 37,
# High intensity.
'ansidarkgray': 90,
'ansired': 91,
'ansigreen': 92,
'ansiyellow': 93,
'ansiblue': 94,
'ansifuchsia': 95,
'ansiturquoise': 96,
'ansiwhite': 97,
}
BG_ANSI_COLORS = {
'ansidefault': 49,
# Low intensity.
'ansiblack': 40,
'ansidarkred': 41,
'ansidarkgreen': 42,
'ansibrown': 43,
'ansidarkblue': 44,
'ansipurple': 45,
'ansiteal': 46,
'ansilightgray': 47,
# High intensity.
'ansidarkgray': 100,
'ansired': 101,
'ansigreen': 102,
'ansiyellow': 103,
'ansiblue': 104,
'ansifuchsia': 105,
'ansiturquoise': 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),
'ansidarkgray': (0x7f, 0x7f, 0x7f),
'ansiwhite': (0xff, 0xff, 0xff),
'ansilightgray': (0xe5, 0xe5, 0xe5),
# Low intensity.
'ansidarkred': (0xcd, 0x00, 0x00),
'ansidarkgreen': (0x00, 0xcd, 0x00),
'ansibrown': (0xcd, 0xcd, 0x00),
'ansidarkblue': (0x00, 0x00, 0xcd),
'ansipurple': (0xcd, 0x00, 0xcd),
'ansiteal': (0x00, 0xcd, 0xcd),
# High intensity.
'ansired': (0xff, 0x00, 0x00),
'ansigreen': (0x00, 0xff, 0x00),
'ansiyellow': (0xff, 0xff, 0x00),
'ansiblue': (0x00, 0x00, 0xff),
'ansifuchsia': (0xff, 0x00, 0xff),
'ansiturquoise': (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, g, b, exclude=()):
"""
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', )``.)
"""
assert isinstance(exclude, tuple)
# 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 += ('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
class _16ColorCache(dict):
"""
Cache which maps (r, g, b) tuples to 16 ansi colors.
:param bg: Cache for background colors, instead of foreground.
"""
def __init__(self, bg=False):
assert isinstance(bg, bool)
self.bg = bg
def get_code(self, value, exclude=()):
"""
Return a (ansi_code, ansi_name) tuple. (E.g. ``(44, 'ansiblue')``.) for
a given (r,g,b) value.
"""
key = (value, exclude)
if key not in self:
self[key] = self._get(value, exclude)
return self[key]
def _get(self, value, exclude=()):
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]
self[value] = code
return code, match
class _256ColorCache(dict):
"""
Cach which maps (r, g, b) tuples to 256 colors.
"""
def __init__(self):
# Build color table.
colors = []
# 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):
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):
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):
"""
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, true_color=False, ansi_colors_only=False):
assert isinstance(true_color, bool)
self.true_color = true_color
self.ansi_colors_only = to_simple_filter(ansi_colors_only)
def __missing__(self, attrs):
fgcolor, bgcolor, bold, underline, italic, blink, reverse = attrs
parts = []
parts.extend(self._colors_to_code(fgcolor, bgcolor))
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 parts:
result = '\x1b[0;' + ';'.join(parts) + 'm'
else:
result = '\x1b[0m'
self[attrs] = result
return result
def _color_name_to_rgb(self, color):
" 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, bg_color):
" 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 explicitely defined to be the same color.)
fg_ansi = [()]
def get(color, bg):
table = BG_ANSI_COLORS if bg else FG_ANSI_COLORS
if color is None:
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.ansi_colors_only():
if bg: # Background.
if fg_color != bg_color:
exclude = (fg_ansi[0], )
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[0] = name
return (code, )
# True colors. (Only when this feature is enabled.)
elif self.true_color:
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 = []
result.extend(get(fg_color, False))
result.extend(get(bg_color, True))
return map(six.text_type, result)
def _get_size(fileno):
# 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(b'h' if six.PY2 else u'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)
# 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 true_color: Use 24bit color instead of 256 colors. (Can be a :class:`SimpleFilter`.)
When `ansi_colors_only` is set, only 16 colors are used.
:param ansi_colors_only: Restrict to 16 ANSI colors only.
: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.
"""
def __init__(self, stdout, get_size, true_color=False,
ansi_colors_only=None, term=None, write_binary=True):
assert callable(get_size)
assert term is None or isinstance(term, six.text_type)
assert all(hasattr(stdout, a) for a in ('write', 'flush'))
if write_binary:
assert hasattr(stdout, 'encoding')
self._buffer = []
self.stdout = stdout
self.write_binary = write_binary
self.get_size = get_size
self.true_color = to_simple_filter(true_color)
self.term = term or 'xterm'
# ANSI colors only?
if ansi_colors_only is None:
# When not given, use the following default.
ANSI_COLORS_ONLY = bool(os.environ.get(
'PROMPT_TOOLKIT_ANSI_COLORS_ONLY', False))
@Condition
def ansi_colors_only():
return ANSI_COLORS_ONLY or term in ('linux', 'eterm-color')
else:
ansi_colors_only = to_simple_filter(ansi_colors_only)
self.ansi_colors_only = ansi_colors_only
# Cache for escape codes.
self._escape_code_cache = _EscapeCodeCache(ansi_colors_only=ansi_colors_only)
self._escape_code_cache_true_color = _EscapeCodeCache(
true_color=True, ansi_colors_only=ansi_colors_only)
@classmethod
def from_pty(cls, stdout, true_color=False, ansi_colors_only=None, term=None):
"""
Create an Output class from a pseudo terminal.
(This will take the dimensions by reading the pseudo
terminal attributes.)
"""
assert stdout.isatty()
def get_size():
rows, columns = _get_size(stdout.fileno())
return Size(rows=rows, columns=columns)
return cls(stdout, get_size, true_color=true_color,
ansi_colors_only=ansi_colors_only, term=term)
def fileno(self):
" Return file descriptor. "
return self.stdout.fileno()
def encoding(self):
" Return encoding used for stdout. "
return self.stdout.encoding
def write_raw(self, data):
"""
Write raw data to output.
"""
self._buffer.append(data)
def write(self, data):
"""
Write text to output.
(Removes vt100 escape codes. -- used for safely writing text.)
"""
self._buffer.append(data.replace('\x1b', '?'))
def set_title(self, title):
"""
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):
self.set_title('')
def erase_screen(self):
"""
Erases the screen with the background colour and moves the cursor to
home.
"""
self.write_raw('\x1b[2J')
def enter_alternate_screen(self):
self.write_raw('\x1b[?1049h\x1b[H')
def quit_alternate_screen(self):
self.write_raw('\x1b[?1049l')
def enable_mouse_support(self):
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):
self.write_raw('\x1b[?1000l')
self.write_raw('\x1b[?1015l')
self.write_raw('\x1b[?1006l')
def erase_end_of_line(self):
"""
Erases from the current cursor position to the end of the current line.
"""
self.write_raw('\x1b[K')
def erase_down(self):
"""
Erases the screen from the current line down to the bottom of the
screen.
"""
self.write_raw('\x1b[J')
def reset_attributes(self):
self.write_raw('\x1b[0m')
def set_attributes(self, attrs):
"""
Create new style and output.
:param attrs: `Attrs` instance.
"""
if self.true_color() and not self.ansi_colors_only():
self.write_raw(self._escape_code_cache_true_color[attrs])
else:
self.write_raw(self._escape_code_cache[attrs])
def disable_autowrap(self):
self.write_raw('\x1b[?7l')
def enable_autowrap(self):
self.write_raw('\x1b[?7h')
def enable_bracketed_paste(self):
self.write_raw('\x1b[?2004h')
def disable_bracketed_paste(self):
self.write_raw('\x1b[?2004l')
def cursor_goto(self, row=0, column=0):
""" Move cursor position. """
self.write_raw('\x1b[%i;%iH' % (row, column))
def cursor_up(self, amount):
if amount == 0:
pass
elif amount == 1:
self.write_raw('\x1b[A')
else:
self.write_raw('\x1b[%iA' % amount)
def cursor_down(self, amount):
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):
if amount == 0:
pass
elif amount == 1:
self.write_raw('\x1b[C')
else:
self.write_raw('\x1b[%iC' % amount)
def cursor_backward(self, amount):
if amount == 0:
pass
elif amount == 1:
self.write_raw('\b') # '\x1b[D'
else:
self.write_raw('\x1b[%iD' % amount)
def hide_cursor(self):
self.write_raw('\x1b[?25l')
def show_cursor(self):
self.write_raw('\x1b[?12l\x1b[?25h') # Stop blinking cursor and show.
def flush(self):
"""
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.
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 happpen 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):
"""
Asks for a cursor position report (CPR).
"""
self.write_raw('\x1b[6n')
self.flush()
def bell(self):
" Sound bell. "
self.write_raw('\a')
self.flush()

View File

@@ -0,0 +1,364 @@
from __future__ import unicode_literals
from ctypes import windll, pointer
from ctypes.wintypes import DWORD
from six.moves import range
from prompt_toolkit.key_binding.input_processor import KeyPress
from prompt_toolkit.keys import Keys
from prompt_toolkit.mouse_events import MouseEventType
from prompt_toolkit.win32_types import EventTypes, KEY_EVENT_RECORD, MOUSE_EVENT_RECORD, INPUT_RECORD, STD_INPUT_HANDLE
import msvcrt
import os
import sys
import six
__all__ = (
'ConsoleInputReader',
'raw_mode',
'cooked_mode'
)
class ConsoleInputReader(object):
"""
:param recognize_paste: When True, try to discover paste actions and turn
the event into a BracketedPaste.
"""
# Keys with character data.
mappings = {
b'\x1b': Keys.Escape,
b'\x00': Keys.ControlSpace, # Control-Space (Also for Ctrl-@)
b'\x01': Keys.ControlA, # Control-A (home)
b'\x02': Keys.ControlB, # Control-B (emacs cursor left)
b'\x03': Keys.ControlC, # Control-C (interrupt)
b'\x04': Keys.ControlD, # Control-D (exit)
b'\x05': Keys.ControlE, # Contrel-E (end)
b'\x06': Keys.ControlF, # Control-F (cursor forward)
b'\x07': Keys.ControlG, # Control-G
b'\x08': Keys.ControlH, # Control-H (8) (Identical to '\b')
b'\x09': Keys.ControlI, # Control-I (9) (Identical to '\t')
b'\x0a': Keys.ControlJ, # Control-J (10) (Identical to '\n')
b'\x0b': Keys.ControlK, # Control-K (delete until end of line; vertical tab)
b'\x0c': Keys.ControlL, # Control-L (clear; form feed)
b'\x0d': Keys.ControlJ, # Control-J NOTE: Windows sends \r instead of
# \n when pressing enter. We turn it into \n
# to be compatible with other platforms.
b'\x0e': Keys.ControlN, # Control-N (14) (history forward)
b'\x0f': Keys.ControlO, # Control-O (15)
b'\x10': Keys.ControlP, # Control-P (16) (history back)
b'\x11': Keys.ControlQ, # Control-Q
b'\x12': Keys.ControlR, # Control-R (18) (reverse search)
b'\x13': Keys.ControlS, # Control-S (19) (forward search)
b'\x14': Keys.ControlT, # Control-T
b'\x15': Keys.ControlU, # Control-U
b'\x16': Keys.ControlV, # Control-V
b'\x17': Keys.ControlW, # Control-W
b'\x18': Keys.ControlX, # Control-X
b'\x19': Keys.ControlY, # Control-Y (25)
b'\x1a': Keys.ControlZ, # Control-Z
b'\x1c': Keys.ControlBackslash, # Both Control-\ and Ctrl-|
b'\x1d': Keys.ControlSquareClose, # Control-]
b'\x1e': Keys.ControlCircumflex, # Control-^
b'\x1f': Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hypen.)
b'\x7f': Keys.Backspace, # (127) Backspace
}
# Keys that don't carry character data.
keycodes = {
# Home/End
33: Keys.PageUp,
34: Keys.PageDown,
35: Keys.End,
36: Keys.Home,
# Arrows
37: Keys.Left,
38: Keys.Up,
39: Keys.Right,
40: Keys.Down,
45: Keys.Insert,
46: Keys.Delete,
# F-keys.
112: Keys.F1,
113: Keys.F2,
114: Keys.F3,
115: Keys.F4,
116: Keys.F5,
117: Keys.F6,
118: Keys.F7,
119: Keys.F8,
120: Keys.F9,
121: Keys.F10,
122: Keys.F11,
123: Keys.F12,
}
LEFT_ALT_PRESSED = 0x0002
RIGHT_ALT_PRESSED = 0x0001
SHIFT_PRESSED = 0x0010
LEFT_CTRL_PRESSED = 0x0008
RIGHT_CTRL_PRESSED = 0x0004
def __init__(self, recognize_paste=True):
self._fdcon = None
self.recognize_paste = recognize_paste
# When stdin is a tty, use that handle, otherwise, create a handle from
# CONIN$.
if sys.stdin.isatty():
self.handle = windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)
else:
self._fdcon = os.open('CONIN$', os.O_RDWR | os.O_BINARY)
self.handle = msvcrt.get_osfhandle(self._fdcon)
def close(self):
" Close fdcon. "
if self._fdcon is not None:
os.close(self._fdcon)
def read(self):
"""
Return a list of `KeyPress` instances. It won't return anything when
there was nothing to read. (This function doesn't block.)
http://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx
"""
max_count = 2048 # Max events to read at the same time.
read = DWORD(0)
arrtype = INPUT_RECORD * max_count
input_records = arrtype()
# Get next batch of input event.
windll.kernel32.ReadConsoleInputW(
self.handle, pointer(input_records), max_count, pointer(read))
# First, get all the keys from the input buffer, in order to determine
# whether we should consider this a paste event or not.
all_keys = list(self._get_keys(read, input_records))
if self.recognize_paste and self._is_paste(all_keys):
gen = iter(all_keys)
for k in gen:
# Pasting: if the current key consists of text or \n, turn it
# into a BracketedPaste.
data = []
while k and (isinstance(k.key, six.text_type) or
k.key == Keys.ControlJ):
data.append(k.data)
try:
k = next(gen)
except StopIteration:
k = None
if data:
yield KeyPress(Keys.BracketedPaste, ''.join(data))
if k is not None:
yield k
else:
for k in all_keys:
yield k
def _get_keys(self, read, input_records):
"""
Generator that yields `KeyPress` objects from the input records.
"""
for i in range(read.value):
ir = input_records[i]
# Get the right EventType from the EVENT_RECORD.
# (For some reason the Windows console application 'cmder'
# [http://gooseberrycreative.com/cmder/] can return '0' for
# ir.EventType. -- Just ignore that.)
if ir.EventType in EventTypes:
ev = getattr(ir.Event, EventTypes[ir.EventType])
# Process if this is a key event. (We also have mouse, menu and
# focus events.)
if type(ev) == KEY_EVENT_RECORD and ev.KeyDown:
for key_press in self._event_to_key_presses(ev):
yield key_press
elif type(ev) == MOUSE_EVENT_RECORD:
for key_press in self._handle_mouse(ev):
yield key_press
@staticmethod
def _is_paste(keys):
"""
Return `True` when we should consider this list of keys as a paste
event. Pasted text on windows will be turned into a
`Keys.BracketedPaste` event. (It's not 100% correct, but it is probably
the best possible way to detect pasting of text and handle that
correctly.)
"""
# Consider paste when it contains at least one newline and at least one
# other character.
text_count = 0
newline_count = 0
for k in keys:
if isinstance(k.key, six.text_type):
text_count += 1
if k.key == Keys.ControlJ:
newline_count += 1
return newline_count >= 1 and text_count > 1
def _event_to_key_presses(self, ev):
"""
For this `KEY_EVENT_RECORD`, return a list of `KeyPress` instances.
"""
assert type(ev) == KEY_EVENT_RECORD and ev.KeyDown
result = None
u_char = ev.uChar.UnicodeChar
ascii_char = u_char.encode('utf-8')
# NOTE: We don't use `ev.uChar.AsciiChar`. That appears to be latin-1
# encoded. See also:
# https://github.com/ipython/ipython/issues/10004
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/389
if u_char == '\x00':
if ev.VirtualKeyCode in self.keycodes:
result = KeyPress(self.keycodes[ev.VirtualKeyCode], '')
else:
if ascii_char in self.mappings:
if self.mappings[ascii_char] == Keys.ControlJ:
u_char = '\n' # Windows sends \n, turn into \r for unix compatibility.
result = KeyPress(self.mappings[ascii_char], u_char)
else:
result = KeyPress(u_char, u_char)
# Correctly handle Control-Arrow keys.
if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or
ev.ControlKeyState & self.RIGHT_CTRL_PRESSED) and result:
if result.key == Keys.Left:
result.key = Keys.ControlLeft
if result.key == Keys.Right:
result.key = Keys.ControlRight
if result.key == Keys.Up:
result.key = Keys.ControlUp
if result.key == Keys.Down:
result.key = Keys.ControlDown
# Turn 'Tab' into 'BackTab' when shift was pressed.
if ev.ControlKeyState & self.SHIFT_PRESSED and result:
if result.key == Keys.Tab:
result.key = Keys.BackTab
# Turn 'Space' into 'ControlSpace' when control was pressed.
if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or
ev.ControlKeyState & self.RIGHT_CTRL_PRESSED) and result and result.data == ' ':
result = KeyPress(Keys.ControlSpace, ' ')
# Turn Control-Enter into META-Enter. (On a vt100 terminal, we cannot
# detect this combination. But it's really practical on Windows.)
if (ev.ControlKeyState & self.LEFT_CTRL_PRESSED or
ev.ControlKeyState & self.RIGHT_CTRL_PRESSED) and result and \
result.key == Keys.ControlJ:
return [KeyPress(Keys.Escape, ''), result]
# Return result. If alt was pressed, prefix the result with an
# 'Escape' key, just like unix VT100 terminals do.
# NOTE: Only replace the left alt with escape. The right alt key often
# acts as altgr and is used in many non US keyboard layouts for
# typing some special characters, like a backslash. We don't want
# all backslashes to be prefixed with escape. (Esc-\ has a
# meaning in E-macs, for instance.)
if result:
meta_pressed = ev.ControlKeyState & self.LEFT_ALT_PRESSED
if meta_pressed:
return [KeyPress(Keys.Escape, ''), result]
else:
return [result]
else:
return []
def _handle_mouse(self, ev):
"""
Handle mouse events. Return a list of KeyPress instances.
"""
FROM_LEFT_1ST_BUTTON_PRESSED = 0x1
result = []
# Check event type.
if ev.ButtonState == FROM_LEFT_1ST_BUTTON_PRESSED:
# On a key press, generate both the mouse down and up event.
for event_type in [MouseEventType.MOUSE_DOWN, MouseEventType.MOUSE_UP]:
data = ';'.join([
event_type,
str(ev.MousePosition.X),
str(ev.MousePosition.Y)
])
result.append(KeyPress(Keys.WindowsMouseEvent, data))
return result
class raw_mode(object):
"""
::
with raw_mode(stdin):
''' the windows terminal is now in 'raw' mode. '''
The ``fileno`` attribute is ignored. This is to be compatble with the
`raw_input` method of `.vt100_input`.
"""
def __init__(self, fileno=None):
self.handle = windll.kernel32.GetStdHandle(STD_INPUT_HANDLE)
def __enter__(self):
# Remember original mode.
original_mode = DWORD()
windll.kernel32.GetConsoleMode(self.handle, pointer(original_mode))
self.original_mode = original_mode
self._patch()
def _patch(self):
# Set raw
ENABLE_ECHO_INPUT = 0x0004
ENABLE_LINE_INPUT = 0x0002
ENABLE_PROCESSED_INPUT = 0x0001
windll.kernel32.SetConsoleMode(
self.handle, self.original_mode.value &
~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT))
def __exit__(self, *a, **kw):
# Restore original mode
windll.kernel32.SetConsoleMode(self.handle, self.original_mode)
class cooked_mode(raw_mode):
"""
::
with cooked_mode(stdin):
''' the pseudo-terminal stdin is now used in raw mode '''
"""
def _patch(self):
# Set cooked.
ENABLE_ECHO_INPUT = 0x0004
ENABLE_LINE_INPUT = 0x0002
ENABLE_PROCESSED_INPUT = 0x0001
windll.kernel32.SetConsoleMode(
self.handle, self.original_mode.value |
(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT))

View File

@@ -0,0 +1,556 @@
from __future__ import unicode_literals
from ctypes import windll, byref, ArgumentError, c_char, c_long, c_ulong, c_uint, pointer
from ctypes.wintypes import DWORD
from prompt_toolkit.renderer import Output
from prompt_toolkit.styles import ANSI_COLOR_NAMES
from prompt_toolkit.win32_types import CONSOLE_SCREEN_BUFFER_INFO, STD_OUTPUT_HANDLE, STD_INPUT_HANDLE, COORD, SMALL_RECT
import os
import six
__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 runing ``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):
# 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(NoConsoleScreenBufferError, self).__init__(message)
class Win32Output(Output):
"""
I/O abstraction for rendering to Windows consoles.
(cmd.exe and similar.)
"""
def __init__(self, stdout, use_complete_width=False):
self.use_complete_width = use_complete_width
self._buffer = []
self.stdout = stdout
self.hconsole = windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
self._in_alternate_screen = 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):
" Return file descriptor. "
return self.stdout.fileno()
def encoding(self):
" Return encoding used for stdout. "
return self.stdout.encoding
def write(self, data):
self._buffer.append(data)
def write_raw(self, data):
" For win32, there is no difference between write and write_raw. "
self.write(data)
def get_size(self):
from prompt_toolkit.layout.screen import 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):
"""
Set terminal title.
"""
assert isinstance(title, six.text_type)
self._winapi(windll.kernel32.SetConsoleTitleW, title)
def clear_title(self):
self._winapi(windll.kernel32.SetConsoleTitleW, '')
def erase_screen(self):
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):
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):
"""
"""
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):
" Reset the console foreground/background color. "
self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole,
self.default_attrs)
def set_attributes(self, attrs):
fgcolor, bgcolor, bold, underline, italic, blink, reverse = attrs
# Start from the default attributes.
attrs = self.default_attrs
# Override the last four bits: foreground color.
if fgcolor is not None:
attrs = attrs & ~0xf
attrs |= self.color_lookup_table.lookup_fg_color(fgcolor)
# Override the next four bits: background color.
if bgcolor is not None:
attrs = attrs & ~0xf0
attrs |= self.color_lookup_table.lookup_bg_color(bgcolor)
# Reverse: swap these four bits groups.
if reverse:
attrs = (attrs & ~0xff) | ((attrs & 0xf) << 4) | ((attrs & 0xf0) >> 4)
self._winapi(windll.kernel32.SetConsoleTextAttribute, self.hconsole, attrs)
def disable_autowrap(self):
# Not supported by Windows.
pass
def enable_autowrap(self):
# Not supported by Windows.
pass
def cursor_goto(self, row=0, column=0):
pos = COORD(x=column, y=row)
self._winapi(windll.kernel32.SetConsoleCursorPosition, self.hconsole, _coord_byval(pos))
def cursor_up(self, amount):
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):
self.cursor_up(-amount)
def cursor_forward(self, amount):
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):
self.cursor_forward(-amount)
def flush(self):
"""
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 soluton
# 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):
info = self.get_win32_screen_buffer_info()
return info.srWindow.Bottom - info.dwCursorPosition.Y + 1
def scroll_buffer_to_prompt(self):
"""
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):
"""
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 = 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):
"""
Make stdout again the active buffer.
"""
if self._in_alternate_screen:
stdout = 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):
ENABLE_MOUSE_INPUT = 0x10
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):
ENABLE_MOUSE_INPUT = 0x10
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):
pass
def show_cursor(self):
pass
@classmethod
def win32_refresh_window(cls):
"""
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 = windll.kernel32.GetConsoleWindow()
RDW_INVALIDATE = 0x0001
windll.user32.RedrawWindow(handle, None, None, c_uint(RDW_INVALIDATE))
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 BACKROUND_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):
" Create a table that maps the 16 named ansi colors to their Windows code. "
return {
'ansidefault': color_cls.BLACK,
'ansiblack': color_cls.BLACK,
'ansidarkgray': color_cls.BLACK | color_cls.INTENSITY,
'ansilightgray': color_cls.GRAY,
'ansiwhite': color_cls.GRAY | color_cls.INTENSITY,
# Low intensity.
'ansidarkred': color_cls.RED,
'ansidarkgreen': color_cls.GREEN,
'ansibrown': color_cls.YELLOW,
'ansidarkblue': color_cls.BLUE,
'ansipurple': color_cls.MAGENTA,
'ansiteal': color_cls.CYAN,
# High intensity.
'ansired': color_cls.RED | color_cls.INTENSITY,
'ansigreen': color_cls.GREEN | color_cls.INTENSITY,
'ansiyellow': color_cls.YELLOW | color_cls.INTENSITY,
'ansiblue': color_cls.BLUE | color_cls.INTENSITY,
'ansifuchsia': color_cls.MAGENTA | color_cls.INTENSITY,
'ansiturquoise': color_cls.CYAN | color_cls.INTENSITY,
}
FG_ANSI_COLORS = _create_ansi_color_dict(FOREGROUND_COLOR)
BG_ANSI_COLORS = _create_ansi_color_dict(BACKROUND_COLOR)
assert set(FG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
assert set(BG_ANSI_COLORS) == set(ANSI_COLOR_NAMES)
class ColorLookupTable(object):
"""
Inspired by pygments/formatters/terminal256.py
"""
def __init__(self):
self._win32_colors = self._build_color_table()
self.best_match = {} # Cache
@staticmethod
def _build_color_table():
"""
Build an RGB-to-256 color conversion table
"""
FG = FOREGROUND_COLOR
BG = BACKROUND_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, g, b):
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):
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):
"""
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):
"""
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]