diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/python/blessed | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/blessed')
23 files changed, 5120 insertions, 0 deletions
diff --git a/third_party/python/blessed/blessed-1.19.1.dist-info/LICENSE b/third_party/python/blessed/blessed-1.19.1.dist-info/LICENSE new file mode 100644 index 0000000000..4b0713283e --- /dev/null +++ b/third_party/python/blessed/blessed-1.19.1.dist-info/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Jeff Quast +Copyright (c) 2011 Erik Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/python/blessed/blessed-1.19.1.dist-info/METADATA b/third_party/python/blessed/blessed-1.19.1.dist-info/METADATA new file mode 100644 index 0000000000..c0e3c98cdd --- /dev/null +++ b/third_party/python/blessed/blessed-1.19.1.dist-info/METADATA @@ -0,0 +1,269 @@ +Metadata-Version: 2.1 +Name: blessed +Version: 1.19.1 +Summary: Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities. +Home-page: https://github.com/jquast/blessed +Author: Jeff Quast, Erik Rose, Avram Lubkin +Author-email: contact@jeffquast.com +License: MIT +Project-URL: Documentation, https://blessed.readthedocs.io +Keywords: terminal,sequences,tty,curses,ncurses,formatting,style,color,console,keyboard,ansi,xterm +Platform: UNKNOWN +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: Console :: Curses +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development :: User Interfaces +Classifier: Topic :: Terminals +Classifier: Typing :: Typed +Requires-Python: >=2.7 +License-File: LICENSE +Requires-Dist: wcwidth (>=0.1.4) +Requires-Dist: six (>=1.9.0) +Requires-Dist: jinxed (>=1.1.0) ; platform_system == "Windows" +Requires-Dist: ordereddict (==1.1) ; python_version < "2.7" +Requires-Dist: backports.functools-lru-cache (>=1.2.1) ; python_version < "3.2" + +| |pypi_downloads| |codecov| |windows| |linux| |mac| |bsd| + +Introduction +============ + +Blessed is an easy, practical *library* for making *terminal* apps, by providing an elegant, +well-documented interface to Colors_, Keyboard_ input, and screen position and Location_ +capabilities. + +.. code-block:: python + + from blessed import Terminal + + term = Terminal() + + print(term.home + term.clear + term.move_y(term.height // 2)) + print(term.black_on_darkkhaki(term.center('press any key to continue.'))) + + with term.cbreak(), term.hidden_cursor(): + inp = term.inkey() + + print(term.move_down(2) + 'You pressed ' + term.bold(repr(inp))) + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/demo_basic_intro.gif + :alt: Animation of running the code example + +It's meant to be *fun* and *easy*, to do basic terminal graphics and styling with Python using +*blessed*. Terminal_ is the only class you need to import and the only object you should need for +Terminal capabilities. + +Whether you want to improve CLI apps with colors, or make fullscreen applications or games, +*blessed* should help get you started quickly. Your users will love it because it works on Windows, +Mac, and Linux, and you will love it because it has plenty of documentation and examples! + +Full documentation at https://blessed.readthedocs.io/en/latest/ + +Examples +-------- + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/blessed_demo_intro.gif + :alt: Animations of x11-colorpicker.py, bounce.py, worms.py, and plasma.py + + x11-colorpicker.py_, bounce.py_, worms.py_, and plasma.py_, from our repository. + +Exemplary 3rd-party examples which use *blessed*, + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/demo_3rdparty_voltron.png + :alt: Screenshot of 'Voltron' (By the author of Voltron, from their README). + + Voltron_ is an extensible debugger UI toolkit written in Python + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/demo_3rdparty_cursewords.gif + :alt: Animation of 'cursewords' (By the author of cursewords, from their README). + + cursewords_ is "graphical" command line program for solving crossword puzzles in the terminal. + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/demo_3rdparty_githeat.gif + :alt: Animation of 'githeat.interactive', using blessed repository at the time of capture. + + GitHeat_ builds an interactive heatmap of git history. + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/demo_3rdparty_dashing.gif + :alt: Animations from 'Dashing' (By the author of Dashing, from their README) + + Dashing_ is a library to quickly create terminal-based dashboards. + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/demo_3rdparty_enlighten.gif + :alt: Animations from 'Enlighten' (By the author of Enlighten, from their README) + + Enlighten_ is a console progress bar library that allows simultaneous output without redirection. + +.. figure:: https://dxtz6bzwq9sxx.cloudfront.net/blessed_3rdparty_macht.gif + :alt: Demonstration of 'macht', a 2048 clone + + macht_ is a clone of the (briefly popular) puzzle game, 2048. + +Requirements +------------ + +*Blessed* works with Windows, Mac, Linux, and BSD's, on Python 2.7, 3.4, 3.5, 3.6, 3.7, and 3.8. + +Brief Overview +-------------- + +*Blessed* is more than just a Python wrapper around curses_: + +* Styles_, Colors_, and maybe a little positioning without necessarily clearing the whole screen + first. +* Works great with Python's new f-strings_ or any other kind of string formatting. +* Provides up-to-the-moment Location_ and terminal height and width, so you can respond to terminal + size changes. +* Avoids making a mess if the output gets piped to a non-terminal, you can output sequences to any + file-like object such as *StringIO*, files, pipes or sockets. +* Uses `terminfo(5)`_ so it works with any terminal type and capability: No more C-like calls to + tigetstr_ and tparm_. +* Non-obtrusive calls to only the capabilities database ensures that you are free to mix and match + with calls to any other curses application code or library you like. +* Provides context managers `Terminal.fullscreen()`_ and `Terminal.hidden_cursor()`_ to safely + express terminal modes, curses development will no longer fudge up your shell. +* Act intelligently when somebody redirects your output to a file, omitting all of the special + sequences colors, but still containing all of the text. + +*Blessed* is a fork of `blessings <https://github.com/erikrose/blessings>`_, which does all of +the same above with the same API, as well as following **enhancements**: + +* Windows support, new since Dec. 2019! +* Dead-simple keyboard handling: safely decoding unicode input in your system's preferred locale and + supports application/arrow keys. +* 24-bit color support, using `Terminal.color_rgb()`_ and `Terminal.on_color_rgb()`_ and all X11 + Colors_ by name, and not by number. +* Determine cursor location using `Terminal.get_location()`_, enter key-at-a-time input mode using + `Terminal.cbreak()`_ or `Terminal.raw()`_ context managers, and read timed key presses using + `Terminal.inkey()`_. +* Allows the *printable length* of strings that contain sequences to be determined by + `Terminal.length()`_, supporting additional methods `Terminal.wrap()`_ and `Terminal.center()`_, + terminal-aware variants of the built-in function `textwrap.wrap()`_ and method `str.center()`_, + respectively. +* Allows sequences to be removed from strings that contain them, using `Terminal.strip_seqs()`_ or + sequences and whitespace using `Terminal.strip()`_. + +Before And After +---------------- + +With the built-in curses_ module, this is how you would typically +print some underlined text at the bottom of the screen: + +.. code-block:: python + + from curses import tigetstr, setupterm, tparm + from fcntl import ioctl + from os import isatty + import struct + import sys + from termios import TIOCGWINSZ + + # If we want to tolerate having our output piped to other commands or + # files without crashing, we need to do all this branching: + if hasattr(sys.stdout, 'fileno') and isatty(sys.stdout.fileno()): + setupterm() + sc = tigetstr('sc') + cup = tigetstr('cup') + rc = tigetstr('rc') + underline = tigetstr('smul') + normal = tigetstr('sgr0') + else: + sc = cup = rc = underline = normal = '' + + # Save cursor position. + print(sc) + + if cup: + # tigetnum('lines') doesn't always update promptly, hence this: + height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0] + + # Move cursor to bottom. + print(tparm(cup, height - 1, 0)) + + print('This is {under}underlined{normal}!' + .format(under=underline, normal=normal)) + + # Restore cursor position. + print(rc) + +The same program with *Blessed* is simply: + +.. code-block:: python + + from blessed import Terminal + + term = Terminal() + with term.location(0, term.height - 1): + print('This is ' + term.underline('underlined') + '!', end='') + +.. _curses: https://docs.python.org/3/library/curses.html +.. _tigetstr: http://man.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/tigetstr.3 +.. _tparm: http://man.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/tparm.3 +.. _`terminfo(5)`: https://invisible-island.net/ncurses/man/terminfo.5.html +.. _str.center(): https://docs.python.org/3/library/stdtypes.html#str.center +.. _textwrap.wrap(): https://docs.python.org/3/library/textwrap.html#textwrap.wrap +.. _Terminal: https://blessed.readthedocs.io/en/stable/terminal.html +.. _`Terminal.fullscreen()`: https://blessed.readthedocs.io/en/latest/api/terminal.html#blessed.terminal.Terminal.fullscreen +.. _`Terminal.get_location()`: https://blessed.readthedocs.io/en/latest/location.html#finding-the-cursor +.. _`Terminal.color_rgb()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.color_rgb +.. _`Terminal.hidden_cursor()`: https://blessed.readthedocs.io/en/latest/api/terminal.html#blessed.terminal.Terminal.hidden_cursor +.. _`Terminal.on_color_rgb()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.on_color_rgb +.. _`Terminal.length()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.length +.. _`Terminal.strip()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.strip +.. _`Terminal.rstrip()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.rstrip +.. _`Terminal.lstrip()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.lstrip +.. _`Terminal.strip_seqs()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.strip_seqs +.. _`Terminal.wrap()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.wrap +.. _`Terminal.center()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.center +.. _`Terminal.rjust()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.rjust +.. _`Terminal.ljust()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.ljust +.. _`Terminal.cbreak()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.cbreak +.. _`Terminal.raw()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.raw +.. _`Terminal.inkey()`: https://blessed.readthedocs.io/en/stable/api/terminal.html#blessed.terminal.Terminal.inkey +.. _Colors: https://blessed.readthedocs.io/en/stable/colors.html +.. _Styles: https://blessed.readthedocs.io/en/stable/terminal.html#styles +.. _Location: https://blessed.readthedocs.io/en/stable/location.html +.. _Keyboard: https://blessed.readthedocs.io/en/stable/keyboard.html +.. _Examples: https://blessed.readthedocs.io/en/stable/examples.html +.. _x11-colorpicker.py: https://blessed.readthedocs.io/en/stable/examples.html#x11-colorpicker-py +.. _bounce.py: https://blessed.readthedocs.io/en/stable/examples.html#bounce-py +.. _worms.py: https://blessed.readthedocs.io/en/stable/examples.html#worms-py +.. _plasma.py: https://blessed.readthedocs.io/en/stable/examples.html#plasma-py +.. _Voltron: https://github.com/snare/voltron +.. _cursewords: https://github.com/thisisparker/cursewords +.. _GitHeat: https://github.com/AmmsA/Githeat +.. _Dashing: https://github.com/FedericoCeratto/dashing +.. _Enlighten: https://github.com/Rockhopper-Technologies/enlighten +.. _macht: https://github.com/rolfmorel/macht +.. _f-strings: https://docs.python.org/3/reference/lexical_analysis.html#f-strings +.. |pypi_downloads| image:: https://img.shields.io/pypi/dm/blessed.svg?logo=pypi + :alt: Downloads + :target: https://pypi.org/project/blessed/ +.. |codecov| image:: https://codecov.io/gh/jquast/blessed/branch/master/graph/badge.svg + :alt: codecov.io Code Coverage + :target: https://codecov.io/gh/jquast/blessed/ +.. |linux| image:: https://img.shields.io/badge/Linux-yes-success?logo=linux + :alt: Linux supported +.. |windows| image:: https://img.shields.io/badge/Windows-NEW-success?logo=windows + :alt: Windows supported +.. |mac| image:: https://img.shields.io/badge/MacOS-yes-success?logo=apple + :alt: MacOS supported +.. |bsd| image:: https://img.shields.io/badge/BSD-yes-success?logo=freebsd + :alt: BSD supported + + diff --git a/third_party/python/blessed/blessed-1.19.1.dist-info/RECORD b/third_party/python/blessed/blessed-1.19.1.dist-info/RECORD new file mode 100644 index 0000000000..ddbe360887 --- /dev/null +++ b/third_party/python/blessed/blessed-1.19.1.dist-info/RECORD @@ -0,0 +1,23 @@ +blessed/__init__.py,sha256=cVCRzlNO_XNDjs-hlDkzv5m3o5BMXxD-hVm30bbWQ1w,687 +blessed/_capabilities.py,sha256=Thj8lgDvhfM6TttvziDu0mabqZqYnwAwC3NTtSMntxc,6292 +blessed/_capabilities.pyi,sha256=If5dG9LhrIyTxUuCuV44bNxnWMk3S7Xvry3oGOBHp2k,265 +blessed/color.py,sha256=D5VmWAsZxSYIERKmkJ7FVhZgNssEtUkV8GcMbDnoxVk,7502 +blessed/color.pyi,sha256=4DNLFe-SMCVKbsvuMFLWUzNCdy45bgtZuk3wP51HlKQ,690 +blessed/colorspace.py,sha256=GEoKL18C9VrBlt-LqbhFirBuhqUmS-tyy0sb9VNJS5U,35322 +blessed/colorspace.pyi,sha256=zwo_F4rf0GSPJsW2irvxHQ3SqNlGgt7VQOFN2hXnTnw,234 +blessed/formatters.py,sha256=3QQiwMC51EdXy6-ZbCPRfw87X49jErvk2-k1yIBwk9g,19416 +blessed/formatters.pyi,sha256=gDgcWIk3pqif7SZgL5DG--GjO7QXwzen5UNgqQ_XHsw,2091 +blessed/keyboard.py,sha256=2WNuDJp_bb5tyRho6cGUup6Hqvp7bFgGDC5D5HbJ2-I,17687 +blessed/keyboard.pyi,sha256=9ibu_A44OkWKcdy_36EeHVhK4ybnurig5arrXxwVOeM,796 +blessed/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +blessed/sequences.py,sha256=Hjs2ddTcXFWqxdL_PKuMoPvaJ3gRRjsZxdy9dByXvLU,17126 +blessed/sequences.pyi,sha256=_zJkZm8S015g242mRUoEp12Qs27DS348vFqkSRsYfHc,1852 +blessed/terminal.py,sha256=QA7yt-E7U72WF7RGkfR7g_HcGAuGFalF3aUP6Zm1L6Y,59889 +blessed/terminal.pyi,sha256=ujihEHr4Ii6r8xw2UBOnAjcY1Wm7dlGbGwfLBqyQQiU,4078 +blessed/win_terminal.py,sha256=uGl52EiEq4K3udsZJHn2tlnESUHg_77fxWrEuFD77WY,5804 +blessed/win_terminal.pyi,sha256=GoS67cnj927_SXZRr1WCLD21ie_w7zlA20V33clpV7E,333 +blessed-1.19.1.dist-info/LICENSE,sha256=YBSQ1biC0QDEeC-dqb_dI_lg5reeNazESZQ5XBj01X0,1083 +blessed-1.19.1.dist-info/METADATA,sha256=H1PBstJVFYedNvC-3vOFQE0PItwv5f6wtmlKy0P5hYQ,13155 +blessed-1.19.1.dist-info/WHEEL,sha256=Z-nyYpwrcSqxfdux5Mbn_DQ525iP7J2DG3JgGvOYyTQ,110 +blessed-1.19.1.dist-info/top_level.txt,sha256=2lUIfLwFZtAucvesS5UE8_MxXID5rSx_3gJ2-1JGckA,8 +blessed-1.19.1.dist-info/RECORD,, diff --git a/third_party/python/blessed/blessed-1.19.1.dist-info/WHEEL b/third_party/python/blessed/blessed-1.19.1.dist-info/WHEEL new file mode 100644 index 0000000000..01b8fc7d4a --- /dev/null +++ b/third_party/python/blessed/blessed-1.19.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.36.2) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/third_party/python/blessed/blessed-1.19.1.dist-info/top_level.txt b/third_party/python/blessed/blessed-1.19.1.dist-info/top_level.txt new file mode 100644 index 0000000000..d43de1b8be --- /dev/null +++ b/third_party/python/blessed/blessed-1.19.1.dist-info/top_level.txt @@ -0,0 +1 @@ +blessed diff --git a/third_party/python/blessed/blessed/__init__.py b/third_party/python/blessed/blessed/__init__.py new file mode 100644 index 0000000000..d1a89c1af7 --- /dev/null +++ b/third_party/python/blessed/blessed/__init__.py @@ -0,0 +1,23 @@ +""" +A thin, practical wrapper around terminal capabilities in Python. + +http://pypi.python.org/pypi/blessed +""" +# std imports +import sys as _sys +import platform as _platform + +# isort: off +if _platform.system() == 'Windows': + from blessed.win_terminal import Terminal +else: + from blessed.terminal import Terminal # type: ignore + +if (3, 0, 0) <= _sys.version_info[:3] < (3, 2, 3): + # Good till 3.2.10 + # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. + raise ImportError('Blessed needs Python 3.2.3 or greater for Python 3 ' + 'support due to http://bugs.python.org/issue10570.') + +__all__ = ('Terminal',) +__version__ = "1.19.1" diff --git a/third_party/python/blessed/blessed/_capabilities.py b/third_party/python/blessed/blessed/_capabilities.py new file mode 100644 index 0000000000..c4df54bca6 --- /dev/null +++ b/third_party/python/blessed/blessed/_capabilities.py @@ -0,0 +1,168 @@ +"""Terminal capability builder patterns.""" +# std imports +import re +from collections import OrderedDict + +__all__ = ( + 'CAPABILITY_DATABASE', + 'CAPABILITIES_RAW_MIXIN', + 'CAPABILITIES_ADDITIVES', + 'CAPABILITIES_CAUSE_MOVEMENT', +) + +CAPABILITY_DATABASE = OrderedDict(( + ('bell', ('bel', {})), + ('carriage_return', ('cr', {})), + ('change_scroll_region', ('csr', {'nparams': 2})), + ('clear_all_tabs', ('tbc', {})), + ('clear_screen', ('clear', {})), + ('clr_bol', ('el1', {})), + ('clr_eol', ('el', {})), + ('clr_eos', ('clear_eos', {})), + ('column_address', ('hpa', {'nparams': 1})), + ('cursor_address', ('cup', {'nparams': 2, 'match_grouped': True})), + ('cursor_down', ('cud1', {})), + ('cursor_home', ('home', {})), + ('cursor_invisible', ('civis', {})), + ('cursor_left', ('cub1', {})), + ('cursor_normal', ('cnorm', {})), + ('cursor_report', ('u6', {'nparams': 2, 'match_grouped': True})), + ('cursor_right', ('cuf1', {})), + ('cursor_up', ('cuu1', {})), + ('cursor_visible', ('cvvis', {})), + ('delete_character', ('dch1', {})), + ('delete_line', ('dl1', {})), + ('enter_blink_mode', ('blink', {})), + ('enter_bold_mode', ('bold', {})), + ('enter_dim_mode', ('dim', {})), + ('enter_fullscreen', ('smcup', {})), + ('enter_standout_mode', ('standout', {})), + ('enter_superscript_mode', ('superscript', {})), + ('enter_susimpleript_mode', ('susimpleript', {})), + ('enter_underline_mode', ('underline', {})), + ('erase_chars', ('ech', {'nparams': 1})), + ('exit_alt_charset_mode', ('rmacs', {})), + ('exit_am_mode', ('rmam', {})), + ('exit_attribute_mode', ('sgr0', {})), + ('exit_ca_mode', ('rmcup', {})), + ('exit_fullscreen', ('rmcup', {})), + ('exit_insert_mode', ('rmir', {})), + ('exit_standout_mode', ('rmso', {})), + ('exit_underline_mode', ('rmul', {})), + ('flash_hook', ('hook', {})), + ('flash_screen', ('flash', {})), + ('insert_line', ('il1', {})), + ('keypad_local', ('rmkx', {})), + ('keypad_xmit', ('smkx', {})), + ('meta_off', ('rmm', {})), + ('meta_on', ('smm', {})), + ('orig_pair', ('op', {})), + ('parm_down_cursor', ('cud', {'nparams': 1})), + ('parm_left_cursor', ('cub', {'nparams': 1, 'match_grouped': True})), + ('parm_dch', ('dch', {'nparams': 1})), + ('parm_delete_line', ('dl', {'nparams': 1})), + ('parm_ich', ('ich', {'nparams': 1})), + ('parm_index', ('indn', {'nparams': 1})), + ('parm_insert_line', ('il', {'nparams': 1})), + ('parm_right_cursor', ('cuf', {'nparams': 1, 'match_grouped': True})), + ('parm_rindex', ('rin', {'nparams': 1})), + ('parm_up_cursor', ('cuu', {'nparams': 1})), + ('print_screen', ('mc0', {})), + ('prtr_off', ('mc4', {})), + ('prtr_on', ('mc5', {})), + ('reset_1string', ('r1', {})), + ('reset_2string', ('r2', {})), + ('reset_3string', ('r3', {})), + ('restore_cursor', ('rc', {})), + ('row_address', ('vpa', {'nparams': 1})), + ('save_cursor', ('sc', {})), + ('scroll_forward', ('ind', {})), + ('scroll_reverse', ('rev', {})), + ('set0_des_seq', ('s0ds', {})), + ('set1_des_seq', ('s1ds', {})), + ('set2_des_seq', ('s2ds', {})), + ('set3_des_seq', ('s3ds', {})), + # this 'color' is deceiving, but often matching, and a better match + # than set_a_attributes1 or set_a_foreground. + ('color', ('_foreground_color', {'nparams': 1, 'match_any': True, + 'numeric': 1})), + ('set_a_foreground', ('color', {'nparams': 1, 'match_any': True, + 'numeric': 1})), + ('set_a_background', ('on_color', {'nparams': 1, 'match_any': True, + 'numeric': 1})), + ('set_tab', ('hts', {})), + ('tab', ('ht', {})), + ('italic', ('sitm', {})), + ('no_italic', ('sitm', {})), +)) + +CAPABILITIES_RAW_MIXIN = { + 'bell': re.escape('\a'), + 'carriage_return': re.escape('\r'), + 'cursor_left': re.escape('\b'), + 'cursor_report': re.escape('\x1b') + r'\[(\d+)\;(\d+)R', + 'cursor_right': re.escape('\x1b') + r'\[C', + 'exit_attribute_mode': re.escape('\x1b') + r'\[m', + 'parm_left_cursor': re.escape('\x1b') + r'\[(\d+)D', + 'parm_right_cursor': re.escape('\x1b') + r'\[(\d+)C', + 'restore_cursor': re.escape(r'\x1b\[u'), + 'save_cursor': re.escape(r'\x1b\[s'), + 'scroll_forward': re.escape('\n'), + 'set0_des_seq': re.escape('\x1b(B'), + 'tab': re.escape('\t'), +} +_ANY_NOTESC = '[^' + re.escape('\x1b') + ']*' + +CAPABILITIES_ADDITIVES = { + 'link': ('link', + re.escape('\x1b') + r'\]8;' + _ANY_NOTESC + ';' + + _ANY_NOTESC + re.escape('\x1b') + '\\\\'), + 'color256': ('color', re.escape('\x1b') + r'\[38;5;\d+m'), + 'on_color256': ('on_color', re.escape('\x1b') + r'\[48;5;\d+m'), + 'color_rgb': ('color_rgb', re.escape('\x1b') + r'\[38;2;\d+;\d+;\d+m'), + 'on_color_rgb': ('on_color_rgb', re.escape('\x1b') + r'\[48;2;\d+;\d+;\d+m'), + 'shift_in': ('', re.escape('\x0f')), + 'shift_out': ('', re.escape('\x0e')), + # sgr(...) outputs strangely, use the basic ANSI/EMCA-48 codes here. + 'set_a_attributes1': ( + 'sgr', re.escape('\x1b') + r'\[\d+m'), + 'set_a_attributes2': ( + 'sgr', re.escape('\x1b') + r'\[\d+\;\d+m'), + 'set_a_attributes3': ( + 'sgr', re.escape('\x1b') + r'\[\d+\;\d+\;\d+m'), + 'set_a_attributes4': ( + 'sgr', re.escape('\x1b') + r'\[\d+\;\d+\;\d+\;\d+m'), + # this helps where xterm's sgr0 includes set0_des_seq, we'd + # rather like to also match this immediate substring. + 'sgr0': ('sgr0', re.escape('\x1b') + r'\[m'), + 'backspace': ('', re.escape('\b')), + 'ascii_tab': ('', re.escape('\t')), + 'clr_eol': ('', re.escape('\x1b[K')), + 'clr_eol0': ('', re.escape('\x1b[0K')), + 'clr_bol': ('', re.escape('\x1b[1K')), + 'clr_eosK': ('', re.escape('\x1b[2K')), +} + +CAPABILITIES_CAUSE_MOVEMENT = ( + 'ascii_tab', + 'backspace', + 'carriage_return', + 'clear_screen', + 'column_address', + 'cursor_address', + 'cursor_down', + 'cursor_home', + 'cursor_left', + 'cursor_right', + 'cursor_up', + 'enter_fullscreen', + 'exit_fullscreen', + 'parm_down_cursor', + 'parm_left_cursor', + 'parm_right_cursor', + 'parm_up_cursor', + 'restore_cursor', + 'row_address', + 'scroll_forward', + 'tab', +) diff --git a/third_party/python/blessed/blessed/_capabilities.pyi b/third_party/python/blessed/blessed/_capabilities.pyi new file mode 100644 index 0000000000..04c59c35fa --- /dev/null +++ b/third_party/python/blessed/blessed/_capabilities.pyi @@ -0,0 +1,7 @@ +# std imports +from typing import Any, Dict, Tuple, OrderedDict + +CAPABILITY_DATABASE: OrderedDict[str, Tuple[str, Dict[str, Any]]] +CAPABILITIES_RAW_MIXIN: Dict[str, str] +CAPABILITIES_ADDITIVES: Dict[str, Tuple[str, str]] +CAPABILITIES_CAUSE_MOVEMENT: Tuple[str, ...] diff --git a/third_party/python/blessed/blessed/color.py b/third_party/python/blessed/blessed/color.py new file mode 100644 index 0000000000..482fc0e11e --- /dev/null +++ b/third_party/python/blessed/blessed/color.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +""" +Sub-module providing color functions. + +References, + +- https://en.wikipedia.org/wiki/Color_difference +- http://www.easyrgb.com/en/math.php +- Measuring Colour by R.W.G. Hunt and M.R. Pointer +""" + +# std imports +from math import cos, exp, sin, sqrt, atan2 + +# isort: off +try: + from functools import lru_cache +except ImportError: + # lru_cache was added in Python 3.2 + from backports.functools_lru_cache import lru_cache + + +def rgb_to_xyz(red, green, blue): + """ + Convert standard RGB color to XYZ color. + + :arg int red: RGB value of Red. + :arg int green: RGB value of Green. + :arg int blue: RGB value of Blue. + :returns: Tuple (X, Y, Z) representing XYZ color + :rtype: tuple + + D65/2° standard illuminant + """ + rgb = [] + for val in red, green, blue: + val /= 255.0 + if val > 0.04045: + val = pow((val + 0.055) / 1.055, 2.4) + else: + val /= 12.92 + val *= 100 + rgb.append(val) + + red, green, blue = rgb # pylint: disable=unbalanced-tuple-unpacking + x_val = red * 0.4124 + green * 0.3576 + blue * 0.1805 + y_val = red * 0.2126 + green * 0.7152 + blue * 0.0722 + z_val = red * 0.0193 + green * 0.1192 + blue * 0.9505 + + return x_val, y_val, z_val + + +def xyz_to_lab(x_val, y_val, z_val): + """ + Convert XYZ color to CIE-Lab color. + + :arg float x_val: XYZ value of X. + :arg float y_val: XYZ value of Y. + :arg float z_val: XYZ value of Z. + :returns: Tuple (L, a, b) representing CIE-Lab color + :rtype: tuple + + D65/2° standard illuminant + """ + xyz = [] + for val, ref in (x_val, 95.047), (y_val, 100.0), (z_val, 108.883): + val /= ref + val = pow(val, 1 / 3.0) if val > 0.008856 else 7.787 * val + 16 / 116.0 + xyz.append(val) + + x_val, y_val, z_val = xyz # pylint: disable=unbalanced-tuple-unpacking + cie_l = 116 * y_val - 16 + cie_a = 500 * (x_val - y_val) + cie_b = 200 * (y_val - z_val) + + return cie_l, cie_a, cie_b + + +@lru_cache(maxsize=256) +def rgb_to_lab(red, green, blue): + """ + Convert RGB color to CIE-Lab color. + + :arg int red: RGB value of Red. + :arg int green: RGB value of Green. + :arg int blue: RGB value of Blue. + :returns: Tuple (L, a, b) representing CIE-Lab color + :rtype: tuple + + D65/2° standard illuminant + """ + return xyz_to_lab(*rgb_to_xyz(red, green, blue)) + + +def dist_rgb(rgb1, rgb2): + """ + Determine distance between two rgb colors. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + This works by treating RGB colors as coordinates in three dimensional + space and finding the closest point within the configured color range + using the formula:: + + d^2 = (r2 - r1)^2 + (g2 - g1)^2 + (b2 - b1)^2 + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + return sum(pow(rgb1[idx] - rgb2[idx], 2) for idx in (0, 1, 2)) + + +def dist_rgb_weighted(rgb1, rgb2): + """ + Determine the weighted distance between two rgb colors. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + Similar to a standard distance formula, the values are weighted + to approximate human perception of color differences + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + red_mean = (rgb1[0] + rgb2[0]) / 2.0 + + return ((2 + red_mean / 256) * pow(rgb1[0] - rgb2[0], 2) + + 4 * pow(rgb1[1] - rgb2[1], 2) + + (2 + (255 - red_mean) / 256) * pow(rgb1[2] - rgb2[2], 2)) + + +def dist_cie76(rgb1, rgb2): + """ + Determine distance between two rgb colors using the CIE94 algorithm. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + l_1, a_1, b_1 = rgb_to_lab(*rgb1) + l_2, a_2, b_2 = rgb_to_lab(*rgb2) + return pow(l_1 - l_2, 2) + pow(a_1 - a_2, 2) + pow(b_1 - b_2, 2) + + +def dist_cie94(rgb1, rgb2): + # pylint: disable=too-many-locals + """ + Determine distance between two rgb colors using the CIE94 algorithm. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + l_1, a_1, b_1 = rgb_to_lab(*rgb1) + l_2, a_2, b_2 = rgb_to_lab(*rgb2) + + s_l = k_l = k_c = k_h = 1 + k_1 = 0.045 + k_2 = 0.015 + + delta_l = l_1 - l_2 + delta_a = a_1 - a_2 + delta_b = b_1 - b_2 + c_1 = sqrt(a_1 ** 2 + b_1 ** 2) + c_2 = sqrt(a_2 ** 2 + b_2 ** 2) + delta_c = c_1 - c_2 + delta_h = sqrt(delta_a ** 2 + delta_b ** 2 + delta_c ** 2) + s_c = 1 + k_1 * c_1 + s_h = 1 + k_2 * c_1 + + return ((delta_l / (k_l * s_l)) ** 2 + # pylint: disable=superfluous-parens + (delta_c / (k_c * s_c)) ** 2 + + (delta_h / (k_h * s_h)) ** 2) + + +def dist_cie2000(rgb1, rgb2): + # pylint: disable=too-many-locals + """ + Determine distance between two rgb colors using the CIE2000 algorithm. + + :arg tuple rgb1: RGB color definition + :arg tuple rgb2: RGB color definition + :returns: Square of the distance between provided colors + :rtype: float + + For efficiency, the square of the distance is returned + which is sufficient for comparisons + """ + s_l = k_l = k_c = k_h = 1 + + l_1, a_1, b_1 = rgb_to_lab(*rgb1) + l_2, a_2, b_2 = rgb_to_lab(*rgb2) + + delta_l = l_2 - l_1 + l_mean = (l_1 + l_2) / 2 + + c_1 = sqrt(a_1 ** 2 + b_1 ** 2) + c_2 = sqrt(a_2 ** 2 + b_2 ** 2) + c_mean = (c_1 + c_2) / 2 + delta_c = c_1 - c_2 + + g_x = sqrt(c_mean ** 7 / (c_mean ** 7 + 25 ** 7)) + h_1 = atan2(b_1, a_1 + (a_1 / 2) * (1 - g_x)) % 360 + h_2 = atan2(b_2, a_2 + (a_2 / 2) * (1 - g_x)) % 360 + + if 0 in (c_1, c_2): + delta_h_prime = 0 + h_mean = h_1 + h_2 + else: + delta_h_prime = h_2 - h_1 + if abs(delta_h_prime) <= 180: + h_mean = (h_1 + h_2) / 2 + else: + if h_2 <= h_1: + delta_h_prime += 360 + else: + delta_h_prime -= 360 + h_mean = (h_1 + h_2 + 360) / 2 if h_1 + h_2 < 360 else (h_1 + h_2 - 360) / 2 + + delta_h = 2 * sqrt(c_1 * c_2) * sin(delta_h_prime / 2) + + t_x = (1 - + 0.17 * cos(h_mean - 30) + + 0.24 * cos(2 * h_mean) + + 0.32 * cos(3 * h_mean + 6) - + 0.20 * cos(4 * h_mean - 63)) + + s_l = 1 + (0.015 * (l_mean - 50) ** 2) / sqrt(20 + (l_mean - 50) ** 2) + s_c = 1 + 0.045 * c_mean + s_h = 1 + 0.015 * c_mean * t_x + r_t = -2 * g_x * sin(abs(60 * exp(-1 * abs((delta_h - 275) / 25) ** 2))) + + delta_l = delta_l / (k_l * s_l) + delta_c = delta_c / (k_c * s_c) + delta_h = delta_h / (k_h * s_h) + + return delta_l ** 2 + delta_c ** 2 + delta_h ** 2 + r_t * delta_c * delta_h + + +COLOR_DISTANCE_ALGORITHMS = {'rgb': dist_rgb, + 'rgb-weighted': dist_rgb_weighted, + 'cie76': dist_cie76, + 'cie94': dist_cie94, + 'cie2000': dist_cie2000} diff --git a/third_party/python/blessed/blessed/color.pyi b/third_party/python/blessed/blessed/color.pyi new file mode 100644 index 0000000000..ece82e3e98 --- /dev/null +++ b/third_party/python/blessed/blessed/color.pyi @@ -0,0 +1,17 @@ +# std imports +from typing import Dict, Tuple, Callable + +_RGB = Tuple[int, int, int] + +def rgb_to_xyz(red: int, green: int, blue: int) -> Tuple[float, float, float]: ... +def xyz_to_lab( + x_val: float, y_val: float, z_val: float +) -> Tuple[float, float, float]: ... +def rgb_to_lab(red: int, green: int, blue: int) -> Tuple[float, float, float]: ... +def dist_rgb(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_rgb_weighted(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_cie76(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_cie94(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_cie2000(rgb1: _RGB, rgb2: _RGB) -> float: ... + +COLOR_DISTANCE_ALGORITHMS: Dict[str, Callable[[_RGB, _RGB], float]] diff --git a/third_party/python/blessed/blessed/colorspace.py b/third_party/python/blessed/blessed/colorspace.py new file mode 100644 index 0000000000..36fe646b35 --- /dev/null +++ b/third_party/python/blessed/blessed/colorspace.py @@ -0,0 +1,973 @@ +""" +Color reference data. + +References, + +- https://github.com/freedesktop/xorg-rgb/blob/master/rgb.txt +- https://github.com/ThomasDickey/xterm-snapshots/blob/master/256colres.h +- https://github.com/ThomasDickey/xterm-snapshots/blob/master/XTerm-col.ad +- https://en.wikipedia.org/wiki/ANSI_escape_code#Colors +- https://gist.github.com/XVilka/8346728 +- https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/ +- http://jdebp.uk/Softwares/nosh/guide/TerminalCapabilities.html +""" +# std imports +import collections + +__all__ = ( + 'CGA_COLORS', + 'RGBColor', + 'RGB_256TABLE', + 'X11_COLORNAMES_TO_RGB', +) + +CGA_COLORS = set( + ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white')) + + +class RGBColor(collections.namedtuple("RGBColor", ["red", "green", "blue"])): + """Named tuple for an RGB color definition.""" + + def __str__(self): + return '#{0:02x}{1:02x}{2:02x}'.format(*self) + + +#: X11 Color names to (XTerm-defined) RGB values from xorg-rgb/rgb.txt +X11_COLORNAMES_TO_RGB = { + 'aliceblue': RGBColor(240, 248, 255), + 'antiquewhite': RGBColor(250, 235, 215), + 'antiquewhite1': RGBColor(255, 239, 219), + 'antiquewhite2': RGBColor(238, 223, 204), + 'antiquewhite3': RGBColor(205, 192, 176), + 'antiquewhite4': RGBColor(139, 131, 120), + 'aqua': RGBColor(0, 255, 255), + 'aquamarine': RGBColor(127, 255, 212), + 'aquamarine1': RGBColor(127, 255, 212), + 'aquamarine2': RGBColor(118, 238, 198), + 'aquamarine3': RGBColor(102, 205, 170), + 'aquamarine4': RGBColor(69, 139, 116), + 'azure': RGBColor(240, 255, 255), + 'azure1': RGBColor(240, 255, 255), + 'azure2': RGBColor(224, 238, 238), + 'azure3': RGBColor(193, 205, 205), + 'azure4': RGBColor(131, 139, 139), + 'beige': RGBColor(245, 245, 220), + 'bisque': RGBColor(255, 228, 196), + 'bisque1': RGBColor(255, 228, 196), + 'bisque2': RGBColor(238, 213, 183), + 'bisque3': RGBColor(205, 183, 158), + 'bisque4': RGBColor(139, 125, 107), + 'black': RGBColor(0, 0, 0), + 'blanchedalmond': RGBColor(255, 235, 205), + 'blue': RGBColor(0, 0, 255), + 'blue1': RGBColor(0, 0, 255), + 'blue2': RGBColor(0, 0, 238), + 'blue3': RGBColor(0, 0, 205), + 'blue4': RGBColor(0, 0, 139), + 'blueviolet': RGBColor(138, 43, 226), + 'brown': RGBColor(165, 42, 42), + 'brown1': RGBColor(255, 64, 64), + 'brown2': RGBColor(238, 59, 59), + 'brown3': RGBColor(205, 51, 51), + 'brown4': RGBColor(139, 35, 35), + 'burlywood': RGBColor(222, 184, 135), + 'burlywood1': RGBColor(255, 211, 155), + 'burlywood2': RGBColor(238, 197, 145), + 'burlywood3': RGBColor(205, 170, 125), + 'burlywood4': RGBColor(139, 115, 85), + 'cadetblue': RGBColor(95, 158, 160), + 'cadetblue1': RGBColor(152, 245, 255), + 'cadetblue2': RGBColor(142, 229, 238), + 'cadetblue3': RGBColor(122, 197, 205), + 'cadetblue4': RGBColor(83, 134, 139), + 'chartreuse': RGBColor(127, 255, 0), + 'chartreuse1': RGBColor(127, 255, 0), + 'chartreuse2': RGBColor(118, 238, 0), + 'chartreuse3': RGBColor(102, 205, 0), + 'chartreuse4': RGBColor(69, 139, 0), + 'chocolate': RGBColor(210, 105, 30), + 'chocolate1': RGBColor(255, 127, 36), + 'chocolate2': RGBColor(238, 118, 33), + 'chocolate3': RGBColor(205, 102, 29), + 'chocolate4': RGBColor(139, 69, 19), + 'coral': RGBColor(255, 127, 80), + 'coral1': RGBColor(255, 114, 86), + 'coral2': RGBColor(238, 106, 80), + 'coral3': RGBColor(205, 91, 69), + 'coral4': RGBColor(139, 62, 47), + 'cornflowerblue': RGBColor(100, 149, 237), + 'cornsilk': RGBColor(255, 248, 220), + 'cornsilk1': RGBColor(255, 248, 220), + 'cornsilk2': RGBColor(238, 232, 205), + 'cornsilk3': RGBColor(205, 200, 177), + 'cornsilk4': RGBColor(139, 136, 120), + 'crimson': RGBColor(220, 20, 60), + 'cyan': RGBColor(0, 255, 255), + 'cyan1': RGBColor(0, 255, 255), + 'cyan2': RGBColor(0, 238, 238), + 'cyan3': RGBColor(0, 205, 205), + 'cyan4': RGBColor(0, 139, 139), + 'darkblue': RGBColor(0, 0, 139), + 'darkcyan': RGBColor(0, 139, 139), + 'darkgoldenrod': RGBColor(184, 134, 11), + 'darkgoldenrod1': RGBColor(255, 185, 15), + 'darkgoldenrod2': RGBColor(238, 173, 14), + 'darkgoldenrod3': RGBColor(205, 149, 12), + 'darkgoldenrod4': RGBColor(139, 101, 8), + 'darkgray': RGBColor(169, 169, 169), + 'darkgreen': RGBColor(0, 100, 0), + 'darkgrey': RGBColor(169, 169, 169), + 'darkkhaki': RGBColor(189, 183, 107), + 'darkmagenta': RGBColor(139, 0, 139), + 'darkolivegreen': RGBColor(85, 107, 47), + 'darkolivegreen1': RGBColor(202, 255, 112), + 'darkolivegreen2': RGBColor(188, 238, 104), + 'darkolivegreen3': RGBColor(162, 205, 90), + 'darkolivegreen4': RGBColor(110, 139, 61), + 'darkorange': RGBColor(255, 140, 0), + 'darkorange1': RGBColor(255, 127, 0), + 'darkorange2': RGBColor(238, 118, 0), + 'darkorange3': RGBColor(205, 102, 0), + 'darkorange4': RGBColor(139, 69, 0), + 'darkorchid': RGBColor(153, 50, 204), + 'darkorchid1': RGBColor(191, 62, 255), + 'darkorchid2': RGBColor(178, 58, 238), + 'darkorchid3': RGBColor(154, 50, 205), + 'darkorchid4': RGBColor(104, 34, 139), + 'darkred': RGBColor(139, 0, 0), + 'darksalmon': RGBColor(233, 150, 122), + 'darkseagreen': RGBColor(143, 188, 143), + 'darkseagreen1': RGBColor(193, 255, 193), + 'darkseagreen2': RGBColor(180, 238, 180), + 'darkseagreen3': RGBColor(155, 205, 155), + 'darkseagreen4': RGBColor(105, 139, 105), + 'darkslateblue': RGBColor(72, 61, 139), + 'darkslategray': RGBColor(47, 79, 79), + 'darkslategray1': RGBColor(151, 255, 255), + 'darkslategray2': RGBColor(141, 238, 238), + 'darkslategray3': RGBColor(121, 205, 205), + 'darkslategray4': RGBColor(82, 139, 139), + 'darkslategrey': RGBColor(47, 79, 79), + 'darkturquoise': RGBColor(0, 206, 209), + 'darkviolet': RGBColor(148, 0, 211), + 'deeppink': RGBColor(255, 20, 147), + 'deeppink1': RGBColor(255, 20, 147), + 'deeppink2': RGBColor(238, 18, 137), + 'deeppink3': RGBColor(205, 16, 118), + 'deeppink4': RGBColor(139, 10, 80), + 'deepskyblue': RGBColor(0, 191, 255), + 'deepskyblue1': RGBColor(0, 191, 255), + 'deepskyblue2': RGBColor(0, 178, 238), + 'deepskyblue3': RGBColor(0, 154, 205), + 'deepskyblue4': RGBColor(0, 104, 139), + 'dimgray': RGBColor(105, 105, 105), + 'dimgrey': RGBColor(105, 105, 105), + 'dodgerblue': RGBColor(30, 144, 255), + 'dodgerblue1': RGBColor(30, 144, 255), + 'dodgerblue2': RGBColor(28, 134, 238), + 'dodgerblue3': RGBColor(24, 116, 205), + 'dodgerblue4': RGBColor(16, 78, 139), + 'firebrick': RGBColor(178, 34, 34), + 'firebrick1': RGBColor(255, 48, 48), + 'firebrick2': RGBColor(238, 44, 44), + 'firebrick3': RGBColor(205, 38, 38), + 'firebrick4': RGBColor(139, 26, 26), + 'floralwhite': RGBColor(255, 250, 240), + 'forestgreen': RGBColor(34, 139, 34), + 'fuchsia': RGBColor(255, 0, 255), + 'gainsboro': RGBColor(220, 220, 220), + 'ghostwhite': RGBColor(248, 248, 255), + 'gold': RGBColor(255, 215, 0), + 'gold1': RGBColor(255, 215, 0), + 'gold2': RGBColor(238, 201, 0), + 'gold3': RGBColor(205, 173, 0), + 'gold4': RGBColor(139, 117, 0), + 'goldenrod': RGBColor(218, 165, 32), + 'goldenrod1': RGBColor(255, 193, 37), + 'goldenrod2': RGBColor(238, 180, 34), + 'goldenrod3': RGBColor(205, 155, 29), + 'goldenrod4': RGBColor(139, 105, 20), + 'gray': RGBColor(190, 190, 190), + 'gray0': RGBColor(0, 0, 0), + 'gray1': RGBColor(3, 3, 3), + 'gray10': RGBColor(26, 26, 26), + 'gray100': RGBColor(255, 255, 255), + 'gray11': RGBColor(28, 28, 28), + 'gray12': RGBColor(31, 31, 31), + 'gray13': RGBColor(33, 33, 33), + 'gray14': RGBColor(36, 36, 36), + 'gray15': RGBColor(38, 38, 38), + 'gray16': RGBColor(41, 41, 41), + 'gray17': RGBColor(43, 43, 43), + 'gray18': RGBColor(46, 46, 46), + 'gray19': RGBColor(48, 48, 48), + 'gray2': RGBColor(5, 5, 5), + 'gray20': RGBColor(51, 51, 51), + 'gray21': RGBColor(54, 54, 54), + 'gray22': RGBColor(56, 56, 56), + 'gray23': RGBColor(59, 59, 59), + 'gray24': RGBColor(61, 61, 61), + 'gray25': RGBColor(64, 64, 64), + 'gray26': RGBColor(66, 66, 66), + 'gray27': RGBColor(69, 69, 69), + 'gray28': RGBColor(71, 71, 71), + 'gray29': RGBColor(74, 74, 74), + 'gray3': RGBColor(8, 8, 8), + 'gray30': RGBColor(77, 77, 77), + 'gray31': RGBColor(79, 79, 79), + 'gray32': RGBColor(82, 82, 82), + 'gray33': RGBColor(84, 84, 84), + 'gray34': RGBColor(87, 87, 87), + 'gray35': RGBColor(89, 89, 89), + 'gray36': RGBColor(92, 92, 92), + 'gray37': RGBColor(94, 94, 94), + 'gray38': RGBColor(97, 97, 97), + 'gray39': RGBColor(99, 99, 99), + 'gray4': RGBColor(10, 10, 10), + 'gray40': RGBColor(102, 102, 102), + 'gray41': RGBColor(105, 105, 105), + 'gray42': RGBColor(107, 107, 107), + 'gray43': RGBColor(110, 110, 110), + 'gray44': RGBColor(112, 112, 112), + 'gray45': RGBColor(115, 115, 115), + 'gray46': RGBColor(117, 117, 117), + 'gray47': RGBColor(120, 120, 120), + 'gray48': RGBColor(122, 122, 122), + 'gray49': RGBColor(125, 125, 125), + 'gray5': RGBColor(13, 13, 13), + 'gray50': RGBColor(127, 127, 127), + 'gray51': RGBColor(130, 130, 130), + 'gray52': RGBColor(133, 133, 133), + 'gray53': RGBColor(135, 135, 135), + 'gray54': RGBColor(138, 138, 138), + 'gray55': RGBColor(140, 140, 140), + 'gray56': RGBColor(143, 143, 143), + 'gray57': RGBColor(145, 145, 145), + 'gray58': RGBColor(148, 148, 148), + 'gray59': RGBColor(150, 150, 150), + 'gray6': RGBColor(15, 15, 15), + 'gray60': RGBColor(153, 153, 153), + 'gray61': RGBColor(156, 156, 156), + 'gray62': RGBColor(158, 158, 158), + 'gray63': RGBColor(161, 161, 161), + 'gray64': RGBColor(163, 163, 163), + 'gray65': RGBColor(166, 166, 166), + 'gray66': RGBColor(168, 168, 168), + 'gray67': RGBColor(171, 171, 171), + 'gray68': RGBColor(173, 173, 173), + 'gray69': RGBColor(176, 176, 176), + 'gray7': RGBColor(18, 18, 18), + 'gray70': RGBColor(179, 179, 179), + 'gray71': RGBColor(181, 181, 181), + 'gray72': RGBColor(184, 184, 184), + 'gray73': RGBColor(186, 186, 186), + 'gray74': RGBColor(189, 189, 189), + 'gray75': RGBColor(191, 191, 191), + 'gray76': RGBColor(194, 194, 194), + 'gray77': RGBColor(196, 196, 196), + 'gray78': RGBColor(199, 199, 199), + 'gray79': RGBColor(201, 201, 201), + 'gray8': RGBColor(20, 20, 20), + 'gray80': RGBColor(204, 204, 204), + 'gray81': RGBColor(207, 207, 207), + 'gray82': RGBColor(209, 209, 209), + 'gray83': RGBColor(212, 212, 212), + 'gray84': RGBColor(214, 214, 214), + 'gray85': RGBColor(217, 217, 217), + 'gray86': RGBColor(219, 219, 219), + 'gray87': RGBColor(222, 222, 222), + 'gray88': RGBColor(224, 224, 224), + 'gray89': RGBColor(227, 227, 227), + 'gray9': RGBColor(23, 23, 23), + 'gray90': RGBColor(229, 229, 229), + 'gray91': RGBColor(232, 232, 232), + 'gray92': RGBColor(235, 235, 235), + 'gray93': RGBColor(237, 237, 237), + 'gray94': RGBColor(240, 240, 240), + 'gray95': RGBColor(242, 242, 242), + 'gray96': RGBColor(245, 245, 245), + 'gray97': RGBColor(247, 247, 247), + 'gray98': RGBColor(250, 250, 250), + 'gray99': RGBColor(252, 252, 252), + 'green': RGBColor(0, 255, 0), + 'green1': RGBColor(0, 255, 0), + 'green2': RGBColor(0, 238, 0), + 'green3': RGBColor(0, 205, 0), + 'green4': RGBColor(0, 139, 0), + 'greenyellow': RGBColor(173, 255, 47), + 'grey': RGBColor(190, 190, 190), + 'grey0': RGBColor(0, 0, 0), + 'grey1': RGBColor(3, 3, 3), + 'grey10': RGBColor(26, 26, 26), + 'grey100': RGBColor(255, 255, 255), + 'grey11': RGBColor(28, 28, 28), + 'grey12': RGBColor(31, 31, 31), + 'grey13': RGBColor(33, 33, 33), + 'grey14': RGBColor(36, 36, 36), + 'grey15': RGBColor(38, 38, 38), + 'grey16': RGBColor(41, 41, 41), + 'grey17': RGBColor(43, 43, 43), + 'grey18': RGBColor(46, 46, 46), + 'grey19': RGBColor(48, 48, 48), + 'grey2': RGBColor(5, 5, 5), + 'grey20': RGBColor(51, 51, 51), + 'grey21': RGBColor(54, 54, 54), + 'grey22': RGBColor(56, 56, 56), + 'grey23': RGBColor(59, 59, 59), + 'grey24': RGBColor(61, 61, 61), + 'grey25': RGBColor(64, 64, 64), + 'grey26': RGBColor(66, 66, 66), + 'grey27': RGBColor(69, 69, 69), + 'grey28': RGBColor(71, 71, 71), + 'grey29': RGBColor(74, 74, 74), + 'grey3': RGBColor(8, 8, 8), + 'grey30': RGBColor(77, 77, 77), + 'grey31': RGBColor(79, 79, 79), + 'grey32': RGBColor(82, 82, 82), + 'grey33': RGBColor(84, 84, 84), + 'grey34': RGBColor(87, 87, 87), + 'grey35': RGBColor(89, 89, 89), + 'grey36': RGBColor(92, 92, 92), + 'grey37': RGBColor(94, 94, 94), + 'grey38': RGBColor(97, 97, 97), + 'grey39': RGBColor(99, 99, 99), + 'grey4': RGBColor(10, 10, 10), + 'grey40': RGBColor(102, 102, 102), + 'grey41': RGBColor(105, 105, 105), + 'grey42': RGBColor(107, 107, 107), + 'grey43': RGBColor(110, 110, 110), + 'grey44': RGBColor(112, 112, 112), + 'grey45': RGBColor(115, 115, 115), + 'grey46': RGBColor(117, 117, 117), + 'grey47': RGBColor(120, 120, 120), + 'grey48': RGBColor(122, 122, 122), + 'grey49': RGBColor(125, 125, 125), + 'grey5': RGBColor(13, 13, 13), + 'grey50': RGBColor(127, 127, 127), + 'grey51': RGBColor(130, 130, 130), + 'grey52': RGBColor(133, 133, 133), + 'grey53': RGBColor(135, 135, 135), + 'grey54': RGBColor(138, 138, 138), + 'grey55': RGBColor(140, 140, 140), + 'grey56': RGBColor(143, 143, 143), + 'grey57': RGBColor(145, 145, 145), + 'grey58': RGBColor(148, 148, 148), + 'grey59': RGBColor(150, 150, 150), + 'grey6': RGBColor(15, 15, 15), + 'grey60': RGBColor(153, 153, 153), + 'grey61': RGBColor(156, 156, 156), + 'grey62': RGBColor(158, 158, 158), + 'grey63': RGBColor(161, 161, 161), + 'grey64': RGBColor(163, 163, 163), + 'grey65': RGBColor(166, 166, 166), + 'grey66': RGBColor(168, 168, 168), + 'grey67': RGBColor(171, 171, 171), + 'grey68': RGBColor(173, 173, 173), + 'grey69': RGBColor(176, 176, 176), + 'grey7': RGBColor(18, 18, 18), + 'grey70': RGBColor(179, 179, 179), + 'grey71': RGBColor(181, 181, 181), + 'grey72': RGBColor(184, 184, 184), + 'grey73': RGBColor(186, 186, 186), + 'grey74': RGBColor(189, 189, 189), + 'grey75': RGBColor(191, 191, 191), + 'grey76': RGBColor(194, 194, 194), + 'grey77': RGBColor(196, 196, 196), + 'grey78': RGBColor(199, 199, 199), + 'grey79': RGBColor(201, 201, 201), + 'grey8': RGBColor(20, 20, 20), + 'grey80': RGBColor(204, 204, 204), + 'grey81': RGBColor(207, 207, 207), + 'grey82': RGBColor(209, 209, 209), + 'grey83': RGBColor(212, 212, 212), + 'grey84': RGBColor(214, 214, 214), + 'grey85': RGBColor(217, 217, 217), + 'grey86': RGBColor(219, 219, 219), + 'grey87': RGBColor(222, 222, 222), + 'grey88': RGBColor(224, 224, 224), + 'grey89': RGBColor(227, 227, 227), + 'grey9': RGBColor(23, 23, 23), + 'grey90': RGBColor(229, 229, 229), + 'grey91': RGBColor(232, 232, 232), + 'grey92': RGBColor(235, 235, 235), + 'grey93': RGBColor(237, 237, 237), + 'grey94': RGBColor(240, 240, 240), + 'grey95': RGBColor(242, 242, 242), + 'grey96': RGBColor(245, 245, 245), + 'grey97': RGBColor(247, 247, 247), + 'grey98': RGBColor(250, 250, 250), + 'grey99': RGBColor(252, 252, 252), + 'honeydew': RGBColor(240, 255, 240), + 'honeydew1': RGBColor(240, 255, 240), + 'honeydew2': RGBColor(224, 238, 224), + 'honeydew3': RGBColor(193, 205, 193), + 'honeydew4': RGBColor(131, 139, 131), + 'hotpink': RGBColor(255, 105, 180), + 'hotpink1': RGBColor(255, 110, 180), + 'hotpink2': RGBColor(238, 106, 167), + 'hotpink3': RGBColor(205, 96, 144), + 'hotpink4': RGBColor(139, 58, 98), + 'indianred': RGBColor(205, 92, 92), + 'indianred1': RGBColor(255, 106, 106), + 'indianred2': RGBColor(238, 99, 99), + 'indianred3': RGBColor(205, 85, 85), + 'indianred4': RGBColor(139, 58, 58), + 'indigo': RGBColor(75, 0, 130), + 'ivory': RGBColor(255, 255, 240), + 'ivory1': RGBColor(255, 255, 240), + 'ivory2': RGBColor(238, 238, 224), + 'ivory3': RGBColor(205, 205, 193), + 'ivory4': RGBColor(139, 139, 131), + 'khaki': RGBColor(240, 230, 140), + 'khaki1': RGBColor(255, 246, 143), + 'khaki2': RGBColor(238, 230, 133), + 'khaki3': RGBColor(205, 198, 115), + 'khaki4': RGBColor(139, 134, 78), + 'lavender': RGBColor(230, 230, 250), + 'lavenderblush': RGBColor(255, 240, 245), + 'lavenderblush1': RGBColor(255, 240, 245), + 'lavenderblush2': RGBColor(238, 224, 229), + 'lavenderblush3': RGBColor(205, 193, 197), + 'lavenderblush4': RGBColor(139, 131, 134), + 'lawngreen': RGBColor(124, 252, 0), + 'lemonchiffon': RGBColor(255, 250, 205), + 'lemonchiffon1': RGBColor(255, 250, 205), + 'lemonchiffon2': RGBColor(238, 233, 191), + 'lemonchiffon3': RGBColor(205, 201, 165), + 'lemonchiffon4': RGBColor(139, 137, 112), + 'lightblue': RGBColor(173, 216, 230), + 'lightblue1': RGBColor(191, 239, 255), + 'lightblue2': RGBColor(178, 223, 238), + 'lightblue3': RGBColor(154, 192, 205), + 'lightblue4': RGBColor(104, 131, 139), + 'lightcoral': RGBColor(240, 128, 128), + 'lightcyan': RGBColor(224, 255, 255), + 'lightcyan1': RGBColor(224, 255, 255), + 'lightcyan2': RGBColor(209, 238, 238), + 'lightcyan3': RGBColor(180, 205, 205), + 'lightcyan4': RGBColor(122, 139, 139), + 'lightgoldenrod': RGBColor(238, 221, 130), + 'lightgoldenrod1': RGBColor(255, 236, 139), + 'lightgoldenrod2': RGBColor(238, 220, 130), + 'lightgoldenrod3': RGBColor(205, 190, 112), + 'lightgoldenrod4': RGBColor(139, 129, 76), + 'lightgoldenrodyellow': RGBColor(250, 250, 210), + 'lightgray': RGBColor(211, 211, 211), + 'lightgreen': RGBColor(144, 238, 144), + 'lightgrey': RGBColor(211, 211, 211), + 'lightpink': RGBColor(255, 182, 193), + 'lightpink1': RGBColor(255, 174, 185), + 'lightpink2': RGBColor(238, 162, 173), + 'lightpink3': RGBColor(205, 140, 149), + 'lightpink4': RGBColor(139, 95, 101), + 'lightsalmon': RGBColor(255, 160, 122), + 'lightsalmon1': RGBColor(255, 160, 122), + 'lightsalmon2': RGBColor(238, 149, 114), + 'lightsalmon3': RGBColor(205, 129, 98), + 'lightsalmon4': RGBColor(139, 87, 66), + 'lightseagreen': RGBColor(32, 178, 170), + 'lightskyblue': RGBColor(135, 206, 250), + 'lightskyblue1': RGBColor(176, 226, 255), + 'lightskyblue2': RGBColor(164, 211, 238), + 'lightskyblue3': RGBColor(141, 182, 205), + 'lightskyblue4': RGBColor(96, 123, 139), + 'lightslateblue': RGBColor(132, 112, 255), + 'lightslategray': RGBColor(119, 136, 153), + 'lightslategrey': RGBColor(119, 136, 153), + 'lightsteelblue': RGBColor(176, 196, 222), + 'lightsteelblue1': RGBColor(202, 225, 255), + 'lightsteelblue2': RGBColor(188, 210, 238), + 'lightsteelblue3': RGBColor(162, 181, 205), + 'lightsteelblue4': RGBColor(110, 123, 139), + 'lightyellow': RGBColor(255, 255, 224), + 'lightyellow1': RGBColor(255, 255, 224), + 'lightyellow2': RGBColor(238, 238, 209), + 'lightyellow3': RGBColor(205, 205, 180), + 'lightyellow4': RGBColor(139, 139, 122), + 'lime': RGBColor(0, 255, 0), + 'limegreen': RGBColor(50, 205, 50), + 'linen': RGBColor(250, 240, 230), + 'magenta': RGBColor(255, 0, 255), + 'magenta1': RGBColor(255, 0, 255), + 'magenta2': RGBColor(238, 0, 238), + 'magenta3': RGBColor(205, 0, 205), + 'magenta4': RGBColor(139, 0, 139), + 'maroon': RGBColor(176, 48, 96), + 'maroon1': RGBColor(255, 52, 179), + 'maroon2': RGBColor(238, 48, 167), + 'maroon3': RGBColor(205, 41, 144), + 'maroon4': RGBColor(139, 28, 98), + 'mediumaquamarine': RGBColor(102, 205, 170), + 'mediumblue': RGBColor(0, 0, 205), + 'mediumorchid': RGBColor(186, 85, 211), + 'mediumorchid1': RGBColor(224, 102, 255), + 'mediumorchid2': RGBColor(209, 95, 238), + 'mediumorchid3': RGBColor(180, 82, 205), + 'mediumorchid4': RGBColor(122, 55, 139), + 'mediumpurple': RGBColor(147, 112, 219), + 'mediumpurple1': RGBColor(171, 130, 255), + 'mediumpurple2': RGBColor(159, 121, 238), + 'mediumpurple3': RGBColor(137, 104, 205), + 'mediumpurple4': RGBColor(93, 71, 139), + 'mediumseagreen': RGBColor(60, 179, 113), + 'mediumslateblue': RGBColor(123, 104, 238), + 'mediumspringgreen': RGBColor(0, 250, 154), + 'mediumturquoise': RGBColor(72, 209, 204), + 'mediumvioletred': RGBColor(199, 21, 133), + 'midnightblue': RGBColor(25, 25, 112), + 'mintcream': RGBColor(245, 255, 250), + 'mistyrose': RGBColor(255, 228, 225), + 'mistyrose1': RGBColor(255, 228, 225), + 'mistyrose2': RGBColor(238, 213, 210), + 'mistyrose3': RGBColor(205, 183, 181), + 'mistyrose4': RGBColor(139, 125, 123), + 'moccasin': RGBColor(255, 228, 181), + 'navajowhite': RGBColor(255, 222, 173), + 'navajowhite1': RGBColor(255, 222, 173), + 'navajowhite2': RGBColor(238, 207, 161), + 'navajowhite3': RGBColor(205, 179, 139), + 'navajowhite4': RGBColor(139, 121, 94), + 'navy': RGBColor(0, 0, 128), + 'navyblue': RGBColor(0, 0, 128), + 'oldlace': RGBColor(253, 245, 230), + 'olive': RGBColor(128, 128, 0), + 'olivedrab': RGBColor(107, 142, 35), + 'olivedrab1': RGBColor(192, 255, 62), + 'olivedrab2': RGBColor(179, 238, 58), + 'olivedrab3': RGBColor(154, 205, 50), + 'olivedrab4': RGBColor(105, 139, 34), + 'orange': RGBColor(255, 165, 0), + 'orange1': RGBColor(255, 165, 0), + 'orange2': RGBColor(238, 154, 0), + 'orange3': RGBColor(205, 133, 0), + 'orange4': RGBColor(139, 90, 0), + 'orangered': RGBColor(255, 69, 0), + 'orangered1': RGBColor(255, 69, 0), + 'orangered2': RGBColor(238, 64, 0), + 'orangered3': RGBColor(205, 55, 0), + 'orangered4': RGBColor(139, 37, 0), + 'orchid': RGBColor(218, 112, 214), + 'orchid1': RGBColor(255, 131, 250), + 'orchid2': RGBColor(238, 122, 233), + 'orchid3': RGBColor(205, 105, 201), + 'orchid4': RGBColor(139, 71, 137), + 'palegoldenrod': RGBColor(238, 232, 170), + 'palegreen': RGBColor(152, 251, 152), + 'palegreen1': RGBColor(154, 255, 154), + 'palegreen2': RGBColor(144, 238, 144), + 'palegreen3': RGBColor(124, 205, 124), + 'palegreen4': RGBColor(84, 139, 84), + 'paleturquoise': RGBColor(175, 238, 238), + 'paleturquoise1': RGBColor(187, 255, 255), + 'paleturquoise2': RGBColor(174, 238, 238), + 'paleturquoise3': RGBColor(150, 205, 205), + 'paleturquoise4': RGBColor(102, 139, 139), + 'palevioletred': RGBColor(219, 112, 147), + 'palevioletred1': RGBColor(255, 130, 171), + 'palevioletred2': RGBColor(238, 121, 159), + 'palevioletred3': RGBColor(205, 104, 137), + 'palevioletred4': RGBColor(139, 71, 93), + 'papayawhip': RGBColor(255, 239, 213), + 'peachpuff': RGBColor(255, 218, 185), + 'peachpuff1': RGBColor(255, 218, 185), + 'peachpuff2': RGBColor(238, 203, 173), + 'peachpuff3': RGBColor(205, 175, 149), + 'peachpuff4': RGBColor(139, 119, 101), + 'peru': RGBColor(205, 133, 63), + 'pink': RGBColor(255, 192, 203), + 'pink1': RGBColor(255, 181, 197), + 'pink2': RGBColor(238, 169, 184), + 'pink3': RGBColor(205, 145, 158), + 'pink4': RGBColor(139, 99, 108), + 'plum': RGBColor(221, 160, 221), + 'plum1': RGBColor(255, 187, 255), + 'plum2': RGBColor(238, 174, 238), + 'plum3': RGBColor(205, 150, 205), + 'plum4': RGBColor(139, 102, 139), + 'powderblue': RGBColor(176, 224, 230), + 'purple': RGBColor(160, 32, 240), + 'purple1': RGBColor(155, 48, 255), + 'purple2': RGBColor(145, 44, 238), + 'purple3': RGBColor(125, 38, 205), + 'purple4': RGBColor(85, 26, 139), + 'rebeccapurple': RGBColor(102, 51, 153), + 'red': RGBColor(255, 0, 0), + 'red1': RGBColor(255, 0, 0), + 'red2': RGBColor(238, 0, 0), + 'red3': RGBColor(205, 0, 0), + 'red4': RGBColor(139, 0, 0), + 'rosybrown': RGBColor(188, 143, 143), + 'rosybrown1': RGBColor(255, 193, 193), + 'rosybrown2': RGBColor(238, 180, 180), + 'rosybrown3': RGBColor(205, 155, 155), + 'rosybrown4': RGBColor(139, 105, 105), + 'royalblue': RGBColor(65, 105, 225), + 'royalblue1': RGBColor(72, 118, 255), + 'royalblue2': RGBColor(67, 110, 238), + 'royalblue3': RGBColor(58, 95, 205), + 'royalblue4': RGBColor(39, 64, 139), + 'saddlebrown': RGBColor(139, 69, 19), + 'salmon': RGBColor(250, 128, 114), + 'salmon1': RGBColor(255, 140, 105), + 'salmon2': RGBColor(238, 130, 98), + 'salmon3': RGBColor(205, 112, 84), + 'salmon4': RGBColor(139, 76, 57), + 'sandybrown': RGBColor(244, 164, 96), + 'seagreen': RGBColor(46, 139, 87), + 'seagreen1': RGBColor(84, 255, 159), + 'seagreen2': RGBColor(78, 238, 148), + 'seagreen3': RGBColor(67, 205, 128), + 'seagreen4': RGBColor(46, 139, 87), + 'seashell': RGBColor(255, 245, 238), + 'seashell1': RGBColor(255, 245, 238), + 'seashell2': RGBColor(238, 229, 222), + 'seashell3': RGBColor(205, 197, 191), + 'seashell4': RGBColor(139, 134, 130), + 'sienna': RGBColor(160, 82, 45), + 'sienna1': RGBColor(255, 130, 71), + 'sienna2': RGBColor(238, 121, 66), + 'sienna3': RGBColor(205, 104, 57), + 'sienna4': RGBColor(139, 71, 38), + 'silver': RGBColor(192, 192, 192), + 'skyblue': RGBColor(135, 206, 235), + 'skyblue1': RGBColor(135, 206, 255), + 'skyblue2': RGBColor(126, 192, 238), + 'skyblue3': RGBColor(108, 166, 205), + 'skyblue4': RGBColor(74, 112, 139), + 'slateblue': RGBColor(106, 90, 205), + 'slateblue1': RGBColor(131, 111, 255), + 'slateblue2': RGBColor(122, 103, 238), + 'slateblue3': RGBColor(105, 89, 205), + 'slateblue4': RGBColor(71, 60, 139), + 'slategray': RGBColor(112, 128, 144), + 'slategray1': RGBColor(198, 226, 255), + 'slategray2': RGBColor(185, 211, 238), + 'slategray3': RGBColor(159, 182, 205), + 'slategray4': RGBColor(108, 123, 139), + 'slategrey': RGBColor(112, 128, 144), + 'snow': RGBColor(255, 250, 250), + 'snow1': RGBColor(255, 250, 250), + 'snow2': RGBColor(238, 233, 233), + 'snow3': RGBColor(205, 201, 201), + 'snow4': RGBColor(139, 137, 137), + 'springgreen': RGBColor(0, 255, 127), + 'springgreen1': RGBColor(0, 255, 127), + 'springgreen2': RGBColor(0, 238, 118), + 'springgreen3': RGBColor(0, 205, 102), + 'springgreen4': RGBColor(0, 139, 69), + 'steelblue': RGBColor(70, 130, 180), + 'steelblue1': RGBColor(99, 184, 255), + 'steelblue2': RGBColor(92, 172, 238), + 'steelblue3': RGBColor(79, 148, 205), + 'steelblue4': RGBColor(54, 100, 139), + 'tan': RGBColor(210, 180, 140), + 'tan1': RGBColor(255, 165, 79), + 'tan2': RGBColor(238, 154, 73), + 'tan3': RGBColor(205, 133, 63), + 'tan4': RGBColor(139, 90, 43), + 'teal': RGBColor(0, 128, 128), + 'thistle': RGBColor(216, 191, 216), + 'thistle1': RGBColor(255, 225, 255), + 'thistle2': RGBColor(238, 210, 238), + 'thistle3': RGBColor(205, 181, 205), + 'thistle4': RGBColor(139, 123, 139), + 'tomato': RGBColor(255, 99, 71), + 'tomato1': RGBColor(255, 99, 71), + 'tomato2': RGBColor(238, 92, 66), + 'tomato3': RGBColor(205, 79, 57), + 'tomato4': RGBColor(139, 54, 38), + 'turquoise': RGBColor(64, 224, 208), + 'turquoise1': RGBColor(0, 245, 255), + 'turquoise2': RGBColor(0, 229, 238), + 'turquoise3': RGBColor(0, 197, 205), + 'turquoise4': RGBColor(0, 134, 139), + 'violet': RGBColor(238, 130, 238), + 'violetred': RGBColor(208, 32, 144), + 'violetred1': RGBColor(255, 62, 150), + 'violetred2': RGBColor(238, 58, 140), + 'violetred3': RGBColor(205, 50, 120), + 'violetred4': RGBColor(139, 34, 82), + 'webgray': RGBColor(128, 128, 128), + 'webgreen': RGBColor(0, 128, 0), + 'webgrey': RGBColor(128, 128, 128), + 'webmaroon': RGBColor(128, 0, 0), + 'webpurple': RGBColor(128, 0, 128), + 'wheat': RGBColor(245, 222, 179), + 'wheat1': RGBColor(255, 231, 186), + 'wheat2': RGBColor(238, 216, 174), + 'wheat3': RGBColor(205, 186, 150), + 'wheat4': RGBColor(139, 126, 102), + 'white': RGBColor(255, 255, 255), + 'whitesmoke': RGBColor(245, 245, 245), + 'x11gray': RGBColor(190, 190, 190), + 'x11green': RGBColor(0, 255, 0), + 'x11grey': RGBColor(190, 190, 190), + 'x11maroon': RGBColor(176, 48, 96), + 'x11purple': RGBColor(160, 32, 240), + 'yellow': RGBColor(255, 255, 0), + 'yellow1': RGBColor(255, 255, 0), + 'yellow2': RGBColor(238, 238, 0), + 'yellow3': RGBColor(205, 205, 0), + 'yellow4': RGBColor(139, 139, 0), + 'yellowgreen': RGBColor(154, 205, 50) +} + +#: Curses color indices of 8, 16, and 256-color terminals +RGB_256TABLE = ( + RGBColor(0, 0, 0), + RGBColor(205, 0, 0), + RGBColor(0, 205, 0), + RGBColor(205, 205, 0), + RGBColor(0, 0, 238), + RGBColor(205, 0, 205), + RGBColor(0, 205, 205), + RGBColor(229, 229, 229), + RGBColor(127, 127, 127), + RGBColor(255, 0, 0), + RGBColor(0, 255, 0), + RGBColor(255, 255, 0), + RGBColor(92, 92, 255), + RGBColor(255, 0, 255), + RGBColor(0, 255, 255), + RGBColor(255, 255, 255), + RGBColor(0, 0, 0), + RGBColor(0, 0, 95), + RGBColor(0, 0, 135), + RGBColor(0, 0, 175), + RGBColor(0, 0, 215), + RGBColor(0, 0, 255), + RGBColor(0, 95, 0), + RGBColor(0, 95, 95), + RGBColor(0, 95, 135), + RGBColor(0, 95, 175), + RGBColor(0, 95, 215), + RGBColor(0, 95, 255), + RGBColor(0, 135, 0), + RGBColor(0, 135, 95), + RGBColor(0, 135, 135), + RGBColor(0, 135, 175), + RGBColor(0, 135, 215), + RGBColor(0, 135, 255), + RGBColor(0, 175, 0), + RGBColor(0, 175, 95), + RGBColor(0, 175, 135), + RGBColor(0, 175, 175), + RGBColor(0, 175, 215), + RGBColor(0, 175, 255), + RGBColor(0, 215, 0), + RGBColor(0, 215, 95), + RGBColor(0, 215, 135), + RGBColor(0, 215, 175), + RGBColor(0, 215, 215), + RGBColor(0, 215, 255), + RGBColor(0, 255, 0), + RGBColor(0, 255, 95), + RGBColor(0, 255, 135), + RGBColor(0, 255, 175), + RGBColor(0, 255, 215), + RGBColor(0, 255, 255), + RGBColor(95, 0, 0), + RGBColor(95, 0, 95), + RGBColor(95, 0, 135), + RGBColor(95, 0, 175), + RGBColor(95, 0, 215), + RGBColor(95, 0, 255), + RGBColor(95, 95, 0), + RGBColor(95, 95, 95), + RGBColor(95, 95, 135), + RGBColor(95, 95, 175), + RGBColor(95, 95, 215), + RGBColor(95, 95, 255), + RGBColor(95, 135, 0), + RGBColor(95, 135, 95), + RGBColor(95, 135, 135), + RGBColor(95, 135, 175), + RGBColor(95, 135, 215), + RGBColor(95, 135, 255), + RGBColor(95, 175, 0), + RGBColor(95, 175, 95), + RGBColor(95, 175, 135), + RGBColor(95, 175, 175), + RGBColor(95, 175, 215), + RGBColor(95, 175, 255), + RGBColor(95, 215, 0), + RGBColor(95, 215, 95), + RGBColor(95, 215, 135), + RGBColor(95, 215, 175), + RGBColor(95, 215, 215), + RGBColor(95, 215, 255), + RGBColor(95, 255, 0), + RGBColor(95, 255, 95), + RGBColor(95, 255, 135), + RGBColor(95, 255, 175), + RGBColor(95, 255, 215), + RGBColor(95, 255, 255), + RGBColor(135, 0, 0), + RGBColor(135, 0, 95), + RGBColor(135, 0, 135), + RGBColor(135, 0, 175), + RGBColor(135, 0, 215), + RGBColor(135, 0, 255), + RGBColor(135, 95, 0), + RGBColor(135, 95, 95), + RGBColor(135, 95, 135), + RGBColor(135, 95, 175), + RGBColor(135, 95, 215), + RGBColor(135, 95, 255), + RGBColor(135, 135, 0), + RGBColor(135, 135, 95), + RGBColor(135, 135, 135), + RGBColor(135, 135, 175), + RGBColor(135, 135, 215), + RGBColor(135, 135, 255), + RGBColor(135, 175, 0), + RGBColor(135, 175, 95), + RGBColor(135, 175, 135), + RGBColor(135, 175, 175), + RGBColor(135, 175, 215), + RGBColor(135, 175, 255), + RGBColor(135, 215, 0), + RGBColor(135, 215, 95), + RGBColor(135, 215, 135), + RGBColor(135, 215, 175), + RGBColor(135, 215, 215), + RGBColor(135, 215, 255), + RGBColor(135, 255, 0), + RGBColor(135, 255, 95), + RGBColor(135, 255, 135), + RGBColor(135, 255, 175), + RGBColor(135, 255, 215), + RGBColor(135, 255, 255), + RGBColor(175, 0, 0), + RGBColor(175, 0, 95), + RGBColor(175, 0, 135), + RGBColor(175, 0, 175), + RGBColor(175, 0, 215), + RGBColor(175, 0, 255), + RGBColor(175, 95, 0), + RGBColor(175, 95, 95), + RGBColor(175, 95, 135), + RGBColor(175, 95, 175), + RGBColor(175, 95, 215), + RGBColor(175, 95, 255), + RGBColor(175, 135, 0), + RGBColor(175, 135, 95), + RGBColor(175, 135, 135), + RGBColor(175, 135, 175), + RGBColor(175, 135, 215), + RGBColor(175, 135, 255), + RGBColor(175, 175, 0), + RGBColor(175, 175, 95), + RGBColor(175, 175, 135), + RGBColor(175, 175, 175), + RGBColor(175, 175, 215), + RGBColor(175, 175, 255), + RGBColor(175, 215, 0), + RGBColor(175, 215, 95), + RGBColor(175, 215, 135), + RGBColor(175, 215, 175), + RGBColor(175, 215, 215), + RGBColor(175, 215, 255), + RGBColor(175, 255, 0), + RGBColor(175, 255, 95), + RGBColor(175, 255, 135), + RGBColor(175, 255, 175), + RGBColor(175, 255, 215), + RGBColor(175, 255, 255), + RGBColor(215, 0, 0), + RGBColor(215, 0, 95), + RGBColor(215, 0, 135), + RGBColor(215, 0, 175), + RGBColor(215, 0, 215), + RGBColor(215, 0, 255), + RGBColor(215, 95, 0), + RGBColor(215, 95, 95), + RGBColor(215, 95, 135), + RGBColor(215, 95, 175), + RGBColor(215, 95, 215), + RGBColor(215, 95, 255), + RGBColor(215, 135, 0), + RGBColor(215, 135, 95), + RGBColor(215, 135, 135), + RGBColor(215, 135, 175), + RGBColor(215, 135, 215), + RGBColor(215, 135, 255), + RGBColor(215, 175, 0), + RGBColor(215, 175, 95), + RGBColor(215, 175, 135), + RGBColor(215, 175, 175), + RGBColor(215, 175, 215), + RGBColor(215, 175, 255), + RGBColor(215, 215, 0), + RGBColor(215, 215, 95), + RGBColor(215, 215, 135), + RGBColor(215, 215, 175), + RGBColor(215, 215, 215), + RGBColor(215, 215, 255), + RGBColor(215, 255, 0), + RGBColor(215, 255, 95), + RGBColor(215, 255, 135), + RGBColor(215, 255, 175), + RGBColor(215, 255, 215), + RGBColor(215, 255, 255), + RGBColor(255, 0, 0), + RGBColor(255, 0, 135), + RGBColor(255, 0, 95), + RGBColor(255, 0, 175), + RGBColor(255, 0, 215), + RGBColor(255, 0, 255), + RGBColor(255, 95, 0), + RGBColor(255, 95, 95), + RGBColor(255, 95, 135), + RGBColor(255, 95, 175), + RGBColor(255, 95, 215), + RGBColor(255, 95, 255), + RGBColor(255, 135, 0), + RGBColor(255, 135, 95), + RGBColor(255, 135, 135), + RGBColor(255, 135, 175), + RGBColor(255, 135, 215), + RGBColor(255, 135, 255), + RGBColor(255, 175, 0), + RGBColor(255, 175, 95), + RGBColor(255, 175, 135), + RGBColor(255, 175, 175), + RGBColor(255, 175, 215), + RGBColor(255, 175, 255), + RGBColor(255, 215, 0), + RGBColor(255, 215, 95), + RGBColor(255, 215, 135), + RGBColor(255, 215, 175), + RGBColor(255, 215, 215), + RGBColor(255, 215, 255), + RGBColor(255, 255, 0), + RGBColor(255, 255, 95), + RGBColor(255, 255, 135), + RGBColor(255, 255, 175), + RGBColor(255, 255, 215), + RGBColor(255, 255, 255), + RGBColor(8, 8, 8), + RGBColor(18, 18, 18), + RGBColor(28, 28, 28), + RGBColor(38, 38, 38), + RGBColor(48, 48, 48), + RGBColor(58, 58, 58), + RGBColor(68, 68, 68), + RGBColor(78, 78, 78), + RGBColor(88, 88, 88), + RGBColor(98, 98, 98), + RGBColor(108, 108, 108), + RGBColor(118, 118, 118), + RGBColor(128, 128, 128), + RGBColor(138, 138, 138), + RGBColor(148, 148, 148), + RGBColor(158, 158, 158), + RGBColor(168, 168, 168), + RGBColor(178, 178, 178), + RGBColor(188, 188, 188), + RGBColor(198, 198, 198), + RGBColor(208, 208, 208), + RGBColor(218, 218, 218), + RGBColor(228, 228, 228), + RGBColor(238, 238, 238), +) diff --git a/third_party/python/blessed/blessed/colorspace.pyi b/third_party/python/blessed/blessed/colorspace.pyi new file mode 100644 index 0000000000..a799cd01cf --- /dev/null +++ b/third_party/python/blessed/blessed/colorspace.pyi @@ -0,0 +1,12 @@ +# std imports +from typing import Set, Dict, Tuple, NamedTuple + +CGA_COLORS: Set[str] + +class RGBColor(NamedTuple): + red: int + green: int + blue: int + +X11_COLORNAMES_TO_RGB: Dict[str, RGBColor] +RGB_256TABLE: Tuple[RGBColor, ...] diff --git a/third_party/python/blessed/blessed/formatters.py b/third_party/python/blessed/blessed/formatters.py new file mode 100644 index 0000000000..ed1badc5ad --- /dev/null +++ b/third_party/python/blessed/blessed/formatters.py @@ -0,0 +1,498 @@ +"""Sub-module providing sequence-formatting functions.""" +# std imports +import platform + +# 3rd party +import six + +# local +from blessed.colorspace import CGA_COLORS, X11_COLORNAMES_TO_RGB + +# isort: off +# curses +if platform.system() == 'Windows': + import jinxed as curses # pylint: disable=import-error +else: + import curses + + +def _make_colors(): + """ + Return set of valid colors and their derivatives. + + :rtype: set + :returns: Color names with prefixes + """ + colors = set() + # basic CGA foreground color, background, high intensity, and bold + # background ('iCE colors' in my day). + for cga_color in CGA_COLORS: + colors.add(cga_color) + colors.add('on_' + cga_color) + colors.add('bright_' + cga_color) + colors.add('on_bright_' + cga_color) + + # foreground and background VGA color + for vga_color in X11_COLORNAMES_TO_RGB: + colors.add(vga_color) + colors.add('on_' + vga_color) + return colors + + +#: Valid colors and their background (on), bright, and bright-background +#: derivatives. +COLORS = _make_colors() + +#: Attributes that may be compounded with colors, by underscore, such as +#: 'reverse_indigo'. +COMPOUNDABLES = set('bold underline reverse blink italic standout'.split()) + + +class ParameterizingString(six.text_type): + r""" + A Unicode string which can be called as a parameterizing termcap. + + For example:: + + >>> from blessed import Terminal + >>> term = Terminal() + >>> color = ParameterizingString(term.color, term.normal, 'color') + >>> color(9)('color #9') + u'\x1b[91mcolor #9\x1b(B\x1b[m' + """ + + def __new__(cls, cap, normal=u'', name=u'<not specified>'): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 3 positional arguments. + + :arg str cap: parameterized string suitable for curses.tparm() + :arg str normal: terminating sequence for this capability (optional). + :arg str name: name of this terminal capability (optional). + """ + new = six.text_type.__new__(cls, cap) + new._normal = normal + new._name = name + return new + + def __call__(self, *args): + """ + Returning :class:`FormattingString` instance for given parameters. + + Return evaluated terminal capability (self), receiving arguments + ``*args``, followed by the terminating sequence (self.normal) into + a :class:`FormattingString` capable of being called. + + :raises TypeError: Mismatch between capability and arguments + :raises curses.error: :func:`curses.tparm` raised an exception + :rtype: :class:`FormattingString` or :class:`NullCallableString` + :returns: Callable string for given parameters + """ + try: + # Re-encode the cap, because tparm() takes a bytestring in Python + # 3. However, appear to be a plain Unicode string otherwise so + # concats work. + attr = curses.tparm(self.encode('latin1'), *args).decode('latin1') + return FormattingString(attr, self._normal) + except TypeError as err: + # If the first non-int (i.e. incorrect) arg was a string, suggest + # something intelligent: + if args and isinstance(args[0], six.string_types): + raise TypeError( + "Unknown terminal capability, %r, or, TypeError " + "for arguments %r: %s" % (self._name, args, err)) + # Somebody passed a non-string; I don't feel confident + # guessing what they were trying to do. + raise + except curses.error as err: + # ignore 'tparm() returned NULL', you won't get any styling, + # even if does_styling is True. This happens on win32 platforms + # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed + if "tparm() returned NULL" not in six.text_type(err): + raise + return NullCallableString() + + +class ParameterizingProxyString(six.text_type): + r""" + A Unicode string which can be called to proxy missing termcap entries. + + This class supports the function :func:`get_proxy_string`, and mirrors + the behavior of :class:`ParameterizingString`, except that instead of + a capability name, receives a format string, and callable to filter the + given positional ``*args`` of :meth:`ParameterizingProxyString.__call__` + into a terminal sequence. + + For example:: + + >>> from blessed import Terminal + >>> term = Terminal('screen') + >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa') + >>> hpa(9) + u'' + >>> fmt = u'\x1b[{0}G' + >>> fmt_arg = lambda *arg: (arg[0] + 1,) + >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa') + >>> hpa(9) + u'\x1b[10G' + """ + + def __new__(cls, fmt_pair, normal=u'', name=u'<not specified>'): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 4 positional arguments. + + :arg tuple fmt_pair: Two element tuple containing: + - format string suitable for displaying terminal sequences + - callable suitable for receiving __call__ arguments for formatting string + :arg str normal: terminating sequence for this capability (optional). + :arg str name: name of this terminal capability (optional). + """ + assert isinstance(fmt_pair, tuple), fmt_pair + assert callable(fmt_pair[1]), fmt_pair[1] + new = six.text_type.__new__(cls, fmt_pair[0]) + new._fmt_args = fmt_pair[1] + new._normal = normal + new._name = name + return new + + def __call__(self, *args): + """ + Returning :class:`FormattingString` instance for given parameters. + + Arguments are determined by the capability. For example, ``hpa`` + (move_x) receives only a single integer, whereas ``cup`` (move) + receives two integers. See documentation in terminfo(5) for the + given capability. + + :rtype: FormattingString + :returns: Callable string for given parameters + """ + return FormattingString(self.format(*self._fmt_args(*args)), + self._normal) + + +class FormattingString(six.text_type): + r""" + A Unicode string which doubles as a callable. + + This is used for terminal attributes, so that it may be used both + directly, or as a callable. When used directly, it simply emits + the given terminal sequence. When used as a callable, it wraps the + given (string) argument with the 2nd argument used by the class + constructor:: + + >>> from blessed import Terminal + >>> term = Terminal() + >>> style = FormattingString(term.bright_blue, term.normal) + >>> print(repr(style)) + u'\x1b[94m' + >>> style('Big Blue') + u'\x1b[94mBig Blue\x1b(B\x1b[m' + """ + + def __new__(cls, sequence, normal=u''): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 2 positional arguments. + + :arg str sequence: terminal attribute sequence. + :arg str normal: terminating sequence for this attribute (optional). + """ + new = six.text_type.__new__(cls, sequence) + new._normal = normal + return new + + def __call__(self, *args): + """ + Return ``text`` joined by ``sequence`` and ``normal``. + + :raises TypeError: Not a string type + :rtype: str + :returns: Arguments wrapped in sequence and normal + """ + # Jim Allman brings us this convenience of allowing existing + # unicode strings to be joined as a call parameter to a formatting + # string result, allowing nestation: + # + # >>> t.red('This is ', t.bold('extremely'), ' dangerous!') + for idx, ucs_part in enumerate(args): + if not isinstance(ucs_part, six.string_types): + expected_types = ', '.join(_type.__name__ for _type in six.string_types) + raise TypeError( + "TypeError for FormattingString argument, " + "%r, at position %s: expected type %s, " + "got %s" % (ucs_part, idx, expected_types, + type(ucs_part).__name__)) + postfix = u'' + if self and self._normal: + postfix = self._normal + _refresh = self._normal + self + args = [_refresh.join(ucs_part.split(self._normal)) + for ucs_part in args] + + return self + u''.join(args) + postfix + + +class FormattingOtherString(six.text_type): + r""" + A Unicode string which doubles as a callable for another sequence when called. + + This is used for the :meth:`~.Terminal.move_up`, ``down``, ``left``, and ``right()`` + family of functions:: + + >>> from blessed import Terminal + >>> term = Terminal() + >>> move_right = FormattingOtherString(term.cuf1, term.cuf) + >>> print(repr(move_right)) + u'\x1b[C' + >>> print(repr(move_right(666))) + u'\x1b[666C' + >>> print(repr(move_right())) + u'\x1b[C' + """ + + def __new__(cls, direct, target): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor accepting 2 positional arguments. + + :arg str direct: capability name for direct formatting, eg ``('x' + term.right)``. + :arg str target: capability name for callable, eg ``('x' + term.right(99))``. + """ + new = six.text_type.__new__(cls, direct) + new._callable = target + return new + + def __getnewargs__(self): + # return arguments used for the __new__ method upon unpickling. + return six.text_type.__new__(six.text_type, self), self._callable + + def __call__(self, *args): + """Return ``text`` by ``target``.""" + if args: + return self._callable(*args) + return self + + +class NullCallableString(six.text_type): + """ + A dummy callable Unicode alternative to :class:`FormattingString`. + + This is used for colors on terminals that do not support colors, it is just a basic form of + unicode that may also act as a callable. + """ + + def __new__(cls): + """Class constructor.""" + return six.text_type.__new__(cls, u'') + + def __call__(self, *args): + """ + Allow empty string to be callable, returning given string, if any. + + When called with an int as the first arg, return an empty Unicode. An + int is a good hint that I am a :class:`ParameterizingString`, as there + are only about half a dozen string-returning capabilities listed in + terminfo(5) which accept non-int arguments, they are seldom used. + + When called with a non-int as the first arg (no no args at all), return + the first arg, acting in place of :class:`FormattingString` without + any attributes. + """ + if not args or isinstance(args[0], int): + # As a NullCallableString, even when provided with a parameter, + # such as t.color(5), we must also still be callable, fe: + # + # >>> t.color(5)('shmoo') + # + # is actually simplified result of NullCallable()() on terminals + # without color support, so turtles all the way down: we return + # another instance. + return NullCallableString() + return u''.join(args) + + +def get_proxy_string(term, attr): + """ + Proxy and return callable string for proxied attributes. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: terminal capability name that may be proxied. + :rtype: None or :class:`ParameterizingProxyString`. + :returns: :class:`ParameterizingProxyString` for some attributes + of some terminal types that support it, where the terminfo(5) + database would otherwise come up empty, such as ``move_x`` + attribute for ``term.kind`` of ``screen``. Otherwise, None. + """ + # normalize 'screen-256color', or 'ansi.sys' to its basic names + term_kind = next(iter(_kind for _kind in ('screen', 'ansi',) + if term.kind.startswith(_kind)), term) + _proxy_table = { # pragma: no cover + 'screen': { + # proxy move_x/move_y for 'screen' terminal type, used by tmux(1). + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + }, + 'ansi': { + # proxy show/hide cursor for 'ansi' terminal type. There is some + # demand for a richly working ANSI terminal type for some reason. + 'civis': ParameterizingProxyString( + (u'\x1b[?25l', lambda *arg: ()), term.normal, attr), + 'cnorm': ParameterizingProxyString( + (u'\x1b[?25h', lambda *arg: ()), term.normal, attr), + 'hpa': ParameterizingProxyString( + (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'vpa': ParameterizingProxyString( + (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr), + 'sc': '\x1b[s', + 'rc': '\x1b[u', + } + } + return _proxy_table.get(term_kind, {}).get(attr, None) + + +def split_compound(compound): + """ + Split compound formating string into segments. + + >>> split_compound('bold_underline_bright_blue_on_red') + ['bold', 'underline', 'bright_blue', 'on_red'] + + :arg str compound: a string that may contain compounds, separated by + underline (``_``). + :rtype: list + :returns: List of formating string segments + """ + merged_segs = [] + # These occur only as prefixes, so they can always be merged: + mergeable_prefixes = ['on', 'bright', 'on_bright'] + for segment in compound.split('_'): + if merged_segs and merged_segs[-1] in mergeable_prefixes: + merged_segs[-1] += '_' + segment + else: + merged_segs.append(segment) + return merged_segs + + +def resolve_capability(term, attr): + """ + Resolve a raw terminal capability using :func:`tigetstr`. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: terminal capability name. + :returns: string of the given terminal capability named by ``attr``, + which may be empty (u'') if not found or not supported by the + given :attr:`~.Terminal.kind`. + :rtype: str + """ + if not term.does_styling: + return u'' + val = curses.tigetstr(term._sugar.get(attr, attr)) # pylint: disable=protected-access + # Decode sequences as latin1, as they are always 8-bit bytes, so when + # b'\xff' is returned, this is decoded as u'\xff'. + return u'' if val is None else val.decode('latin1') + + +def resolve_color(term, color): + """ + Resolve a simple color name to a callable capability. + + This function supports :func:`resolve_attribute`. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str color: any string found in set :const:`COLORS`. + :returns: a string class instance which emits the terminal sequence + for the given color, and may be used as a callable to wrap the + given string with such sequence. + :returns: :class:`NullCallableString` when + :attr:`~.Terminal.number_of_colors` is 0, + otherwise :class:`FormattingString`. + :rtype: :class:`NullCallableString` or :class:`FormattingString` + """ + # pylint: disable=protected-access + if term.number_of_colors == 0: + return NullCallableString() + + # fg/bg capabilities terminals that support 0-256+ colors. + vga_color_cap = (term._background_color if 'on_' in color else + term._foreground_color) + + base_color = color.rsplit('_', 1)[-1] + if base_color in CGA_COLORS: + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + attr = 'COLOR_%s' % (base_color.upper(),) + fmt_attr = vga_color_cap(getattr(curses, attr) + offset) + return FormattingString(fmt_attr, term.normal) + + assert base_color in X11_COLORNAMES_TO_RGB, ( + 'color not known', base_color) + rgb = X11_COLORNAMES_TO_RGB[base_color] + + # downconvert X11 colors to CGA, EGA, or VGA color spaces + if term.number_of_colors <= 256: + fmt_attr = vga_color_cap(term.rgb_downconvert(*rgb)) + return FormattingString(fmt_attr, term.normal) + + # Modern 24-bit color terminals are written pretty basically. The + # foreground and background sequences are: + # - ^[38;2;<r>;<g>;<b>m + # - ^[48;2;<r>;<g>;<b>m + fgbg_seq = ('48' if 'on_' in color else '38') + assert term.number_of_colors == 1 << 24 + fmt_attr = u'\x1b[' + fgbg_seq + ';2;{0};{1};{2}m' + return FormattingString(fmt_attr.format(*rgb), term.normal) + + +def resolve_attribute(term, attr): + """ + Resolve a terminal attribute name into a capability class. + + :arg Terminal term: :class:`~.Terminal` instance. + :arg str attr: Sugary, ordinary, or compound formatted terminal + capability, such as "red_on_white", "normal", "red", or + "bold_on_black". + :returns: a string class instance which emits the terminal sequence + for the given terminal capability, or may be used as a callable to + wrap the given string with such sequence. + :returns: :class:`NullCallableString` when + :attr:`~.Terminal.number_of_colors` is 0, + otherwise :class:`FormattingString`. + :rtype: :class:`NullCallableString` or :class:`FormattingString` + """ + if attr in COLORS: + return resolve_color(term, attr) + + # A direct compoundable, such as `bold' or `on_red'. + if attr in COMPOUNDABLES: + sequence = resolve_capability(term, attr) + return FormattingString(sequence, term.normal) + + # Given `bold_on_red', resolve to ('bold', 'on_red'), RECURSIVE + # call for each compounding section, joined and returned as + # a completed completed FormattingString. + formatters = split_compound(attr) + if all((fmt in COLORS or fmt in COMPOUNDABLES) for fmt in formatters): + resolution = (resolve_attribute(term, fmt) for fmt in formatters) + return FormattingString(u''.join(resolution), term.normal) + + # otherwise, this is our end-game: given a sequence such as 'csr' + # (change scrolling region), return a ParameterizingString instance, + # that when called, performs and returns the final string after curses + # capability lookup is performed. + tparm_capseq = resolve_capability(term, attr) + if not tparm_capseq: + # and, for special terminals, such as 'screen', provide a Proxy + # ParameterizingString for attributes they do not claim to support, + # but actually do! (such as 'hpa' and 'vpa'). + proxy = get_proxy_string(term, + term._sugar.get(attr, attr)) # pylint: disable=protected-access + if proxy is not None: + return proxy + + return ParameterizingString(tparm_capseq, term.normal, attr) diff --git a/third_party/python/blessed/blessed/formatters.pyi b/third_party/python/blessed/blessed/formatters.pyi new file mode 100644 index 0000000000..32a3dc2df3 --- /dev/null +++ b/third_party/python/blessed/blessed/formatters.pyi @@ -0,0 +1,70 @@ +# std imports +from typing import (Any, + Set, + List, + Type, + Tuple, + Union, + TypeVar, + Callable, + NoReturn, + Optional, + overload) + +# local +from .terminal import Terminal + +COLORS: Set[str] +COMPOUNDABLES: Set[str] + +_T = TypeVar("_T") + +class ParameterizingString(str): + def __new__(cls: Type[_T], cap: str, normal: str = ..., name: str = ...) -> _T: ... + @overload + def __call__( + self, *args: int + ) -> Union["FormattingString", "NullCallableString"]: ... + @overload + def __call__(self, *args: str) -> NoReturn: ... + +class ParameterizingProxyString(str): + def __new__( + cls: Type[_T], + fmt_pair: Tuple[str, Callable[..., Tuple[object, ...]]], + normal: str = ..., + name: str = ..., + ) -> _T: ... + def __call__(self, *args: Any) -> "FormattingString": ... + +class FormattingString(str): + def __new__(cls: Type[_T], sequence: str, normal: str = ...) -> _T: ... + @overload + def __call__(self, *args: int) -> NoReturn: ... + @overload + def __call__(self, *args: str) -> str: ... + +class FormattingOtherString(str): + def __new__( + cls: Type[_T], direct: ParameterizingString, target: ParameterizingString = ... + ) -> _T: ... + def __call__(self, *args: Union[int, str]) -> str: ... + +class NullCallableString(str): + def __new__(cls: Type[_T]) -> _T: ... + @overload + def __call__(self, *args: int) -> "NullCallableString": ... + @overload + def __call__(self, *args: str) -> str: ... + +def get_proxy_string( + term: Terminal, attr: str +) -> Optional[ParameterizingProxyString]: ... +def split_compound(compound: str) -> List[str]: ... +def resolve_capability(term: Terminal, attr: str) -> str: ... +def resolve_color( + term: Terminal, color: str +) -> Union[NullCallableString, FormattingString]: ... +def resolve_attribute( + term: Terminal, attr: str +) -> Union[ParameterizingString, FormattingString]: ... diff --git a/third_party/python/blessed/blessed/keyboard.py b/third_party/python/blessed/blessed/keyboard.py new file mode 100644 index 0000000000..2736da160f --- /dev/null +++ b/third_party/python/blessed/blessed/keyboard.py @@ -0,0 +1,449 @@ +"""Sub-module providing 'keyboard awareness'.""" + +# std imports +import re +import time +import platform +from collections import OrderedDict + +# 3rd party +import six + +# isort: off +# curses +if platform.system() == 'Windows': + # pylint: disable=import-error + import jinxed as curses + from jinxed.has_key import _capability_names as capability_names +else: + import curses + from curses.has_key import _capability_names as capability_names + + +class Keystroke(six.text_type): + """ + A unicode-derived class for describing a single keystroke. + + A class instance describes a single keystroke received on input, + which may contain multiple characters as a multibyte sequence, + which is indicated by properties :attr:`is_sequence` returning + ``True``. + + When the string is a known sequence, :attr:`code` matches terminal + class attributes for comparison, such as ``term.KEY_LEFT``. + + The string-name of the sequence, such as ``u'KEY_LEFT'`` is accessed + by property :attr:`name`, and is used by the :meth:`__repr__` method + to display a human-readable form of the Keystroke this class + instance represents. It may otherwise by joined, split, or evaluated + just as as any other unicode string. + """ + + def __new__(cls, ucs='', code=None, name=None): + """Class constructor.""" + new = six.text_type.__new__(cls, ucs) + new._name = name + new._code = code + return new + + @property + def is_sequence(self): + """Whether the value represents a multibyte sequence (bool).""" + return self._code is not None + + def __repr__(self): + """Docstring overwritten.""" + return (six.text_type.__repr__(self) if self._name is None else + self._name) + __repr__.__doc__ = six.text_type.__doc__ + + @property + def name(self): + """String-name of key sequence, such as ``u'KEY_LEFT'`` (str).""" + return self._name + + @property + def code(self): + """Integer keycode value of multibyte sequence (int).""" + return self._code + + +def get_curses_keycodes(): + """ + Return mapping of curses key-names paired by their keycode integer value. + + :rtype: dict + :returns: Dictionary of (name, code) pairs for curses keyboard constant + values and their mnemonic name. Such as code ``260``, with the value of + its key-name identity, ``u'KEY_LEFT'``. + """ + _keynames = [attr for attr in dir(curses) + if attr.startswith('KEY_')] + return {keyname: getattr(curses, keyname) for keyname in _keynames} + + +def get_keyboard_codes(): + """ + Return mapping of keycode integer values paired by their curses key-name. + + :rtype: dict + :returns: Dictionary of (code, name) pairs for curses keyboard constant + values and their mnemonic name. Such as key ``260``, with the value of + its identity, ``u'KEY_LEFT'``. + + These keys are derived from the attributes by the same of the curses module, + with the following exceptions: + + * ``KEY_DELETE`` in place of ``KEY_DC`` + * ``KEY_INSERT`` in place of ``KEY_IC`` + * ``KEY_PGUP`` in place of ``KEY_PPAGE`` + * ``KEY_PGDOWN`` in place of ``KEY_NPAGE`` + * ``KEY_ESCAPE`` in place of ``KEY_EXIT`` + * ``KEY_SUP`` in place of ``KEY_SR`` + * ``KEY_SDOWN`` in place of ``KEY_SF`` + + This function is the inverse of :func:`get_curses_keycodes`. With the + given override "mixins" listed above, the keycode for the delete key will + map to our imaginary ``KEY_DELETE`` mnemonic, effectively erasing the + phrase ``KEY_DC`` from our code vocabulary for anyone that wishes to use + the return value to determine the key-name by keycode. + """ + keycodes = OrderedDict(get_curses_keycodes()) + keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN) + # merge _CURSES_KEYCODE_ADDINS added to our module space + keycodes.update((name, value) for name, value in globals().items() if name.startswith('KEY_')) + + # invert dictionary (key, values) => (values, key), preferring the + # last-most inserted value ('KEY_DELETE' over 'KEY_DC'). + return dict(zip(keycodes.values(), keycodes.keys())) + + +def _alternative_left_right(term): + r""" + Determine and return mapping of left and right arrow keys sequences. + + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :rtype: dict + :returns: Dictionary of sequences ``term._cuf1``, and ``term._cub1``, + valued as ``KEY_RIGHT``, ``KEY_LEFT`` (when appropriate). + + This function supports :func:`get_terminal_sequences` to discover + the preferred input sequence for the left and right application keys. + + It is necessary to check the value of these sequences to ensure we do not + use ``u' '`` and ``u'\b'`` for ``KEY_RIGHT`` and ``KEY_LEFT``, + preferring their true application key sequence, instead. + """ + # pylint: disable=protected-access + keymap = {} + if term._cuf1 and term._cuf1 != u' ': + keymap[term._cuf1] = curses.KEY_RIGHT + if term._cub1 and term._cub1 != u'\b': + keymap[term._cub1] = curses.KEY_LEFT + return keymap + + +def get_keyboard_sequences(term): + r""" + Return mapping of keyboard sequences paired by keycodes. + + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :returns: mapping of keyboard unicode sequences paired by keycodes + as integer. This is used as the argument ``mapper`` to + the supporting function :func:`resolve_sequence`. + :rtype: OrderedDict + + Initialize and return a keyboard map and sequence lookup table, + (sequence, keycode) from :class:`~.Terminal` instance ``term``, + where ``sequence`` is a multibyte input sequence of unicode + characters, such as ``u'\x1b[D'``, and ``keycode`` is an integer + value, matching curses constant such as term.KEY_LEFT. + + The return value is an OrderedDict instance, with their keys + sorted longest-first. + """ + # A small gem from curses.has_key that makes this all possible, + # _capability_names: a lookup table of terminal capability names for + # keyboard sequences (fe. kcub1, key_left), keyed by the values of + # constants found beginning with KEY_ in the main curses module + # (such as KEY_LEFT). + # + # latin1 encoding is used so that bytes in 8-bit range of 127-255 + # have equivalent chr() and unichr() values, so that the sequence + # of a kermit or avatar terminal, for example, remains unchanged + # in its byte sequence values even when represented by unicode. + # + sequence_map = dict(( + (seq.decode('latin1'), val) + for (seq, val) in ( + (curses.tigetstr(cap), val) + for (val, cap) in capability_names.items() + ) if seq + ) if term.does_styling else ()) + + sequence_map.update(_alternative_left_right(term)) + sequence_map.update(DEFAULT_SEQUENCE_MIXIN) + + # This is for fast lookup matching of sequences, preferring + # full-length sequence such as ('\x1b[D', KEY_LEFT) + # over simple sequences such as ('\x1b', KEY_EXIT). + return OrderedDict(( + (seq, sequence_map[seq]) for seq in sorted( + sequence_map.keys(), key=len, reverse=True))) + + +def get_leading_prefixes(sequences): + """ + Return a set of proper prefixes for given sequence of strings. + + :arg iterable sequences + :rtype: set + :return: Set of all string prefixes + + Given an iterable of strings, all textparts leading up to the final + string is returned as a unique set. This function supports the + :meth:`~.Terminal.inkey` method by determining whether the given + input is a sequence that **may** lead to a final matching pattern. + + >>> prefixes(['abc', 'abdf', 'e', 'jkl']) + set([u'a', u'ab', u'abd', u'j', u'jk']) + """ + return {seq[:i] for seq in sequences for i in range(1, len(seq))} + + +def resolve_sequence(text, mapper, codes): + r""" + Return a single :class:`Keystroke` instance for given sequence ``text``. + + :arg str text: string of characters received from terminal input stream. + :arg OrderedDict mapper: unicode multibyte sequences, such as ``u'\x1b[D'`` + paired by their integer value (260) + :arg dict codes: a :type:`dict` of integer values (such as 260) paired + by their mnemonic name, such as ``'KEY_LEFT'``. + :rtype: Keystroke + :returns: Keystroke instance for the given sequence + + The given ``text`` may extend beyond a matching sequence, such as + ``u\x1b[Dxxx`` returns a :class:`Keystroke` instance of attribute + :attr:`Keystroke.sequence` valued only ``u\x1b[D``. It is up to + calls to determine that ``xxx`` remains unresolved. + """ + for sequence, code in mapper.items(): + if text.startswith(sequence): + return Keystroke(ucs=sequence, code=code, name=codes[code]) + return Keystroke(ucs=text and text[0] or u'') + + +def _time_left(stime, timeout): + """ + Return time remaining since ``stime`` before given ``timeout``. + + This function assists determining the value of ``timeout`` for + class method :meth:`~.Terminal.kbhit` and similar functions. + + :arg float stime: starting time for measurement + :arg float timeout: timeout period, may be set to None to + indicate no timeout (where None is always returned). + :rtype: float or int + :returns: time remaining as float. If no time is remaining, + then the integer ``0`` is returned. + """ + return max(0, timeout - (time.time() - stime)) if timeout else timeout + + +def _read_until(term, pattern, timeout): + """ + Convenience read-until-pattern function, supporting :meth:`~.get_location`. + + :arg blessed.Terminal term: :class:`~.Terminal` instance. + :arg float timeout: timeout period, may be set to None to indicate no + timeout (where 0 is always returned). + :arg str pattern: target regular expression pattern to seek. + :rtype: tuple + :returns: tuple in form of ``(match, str)``, *match* + may be :class:`re.MatchObject` if pattern is discovered + in input stream before timeout has elapsed, otherwise + None. ``str`` is any remaining text received exclusive + of the matching pattern). + + The reason a tuple containing non-matching data is returned, is that the + consumer should push such data back into the input buffer by + :meth:`~.Terminal.ungetch` if any was received. + + For example, when a user is performing rapid input keystrokes while its + terminal emulator surreptitiously responds to this in-band sequence, we + must ensure any such keyboard data is well-received by the next call to + term.inkey() without delay. + """ + stime = time.time() + match, buf = None, u'' + + # first, buffer all pending data. pexpect library provides a + # 'searchwindowsize' attribute that limits this memory region. We're not + # concerned about OOM conditions: only (human) keyboard input and terminal + # response sequences are expected. + + while True: # pragma: no branch + # block as long as necessary to ensure at least one character is + # received on input or remaining timeout has elapsed. + ucs = term.inkey(timeout=_time_left(stime, timeout)) + # while the keyboard buffer is "hot" (has input), we continue to + # aggregate all awaiting data. We do this to ensure slow I/O + # calls do not unnecessarily give up within the first 'while' loop + # for short timeout periods. + while ucs: + buf += ucs + ucs = term.inkey(timeout=0) + + match = re.search(pattern=pattern, string=buf) + if match is not None: + # match + break + + if timeout is not None and not _time_left(stime, timeout): + # timeout + break + + return match, buf + + +#: Though we may determine *keynames* and codes for keyboard input that +#: generate multibyte sequences, it is also especially useful to aliases +#: a few basic ASCII characters such as ``KEY_TAB`` instead of ``u'\t'`` for +#: uniformity. +#: +#: Furthermore, many key-names for application keys enabled only by context +#: manager :meth:`~.Terminal.keypad` are surprisingly absent. We inject them +#: here directly into the curses module. +_CURSES_KEYCODE_ADDINS = ( + 'TAB', + 'KP_MULTIPLY', + 'KP_ADD', + 'KP_SEPARATOR', + 'KP_SUBTRACT', + 'KP_DECIMAL', + 'KP_DIVIDE', + 'KP_EQUAL', + 'KP_0', + 'KP_1', + 'KP_2', + 'KP_3', + 'KP_4', + 'KP_5', + 'KP_6', + 'KP_7', + 'KP_8', + 'KP_9') + +_LASTVAL = max(get_curses_keycodes().values()) +for keycode_name in _CURSES_KEYCODE_ADDINS: + _LASTVAL += 1 + globals()['KEY_' + keycode_name] = _LASTVAL + +#: In a perfect world, terminal emulators would always send exactly what +#: the terminfo(5) capability database plans for them, accordingly by the +#: value of the ``TERM`` name they declare. +#: +#: But this isn't a perfect world. Many vt220-derived terminals, such as +#: those declaring 'xterm', will continue to send vt220 codes instead of +#: their native-declared codes, for backwards-compatibility. +#: +#: This goes for many: rxvt, putty, iTerm. +#: +#: These "mixins" are used for *all* terminals, regardless of their type. +#: +#: Furthermore, curses does not provide sequences sent by the keypad, +#: at least, it does not provide a way to distinguish between keypad 0 +#: and numeric 0. +DEFAULT_SEQUENCE_MIXIN = ( + # these common control characters (and 127, ctrl+'?') mapped to + # an application key definition. + (six.unichr(10), curses.KEY_ENTER), + (six.unichr(13), curses.KEY_ENTER), + (six.unichr(8), curses.KEY_BACKSPACE), + (six.unichr(9), KEY_TAB), # noqa # pylint: disable=undefined-variable + (six.unichr(27), curses.KEY_EXIT), + (six.unichr(127), curses.KEY_BACKSPACE), + + (u"\x1b[A", curses.KEY_UP), + (u"\x1b[B", curses.KEY_DOWN), + (u"\x1b[C", curses.KEY_RIGHT), + (u"\x1b[D", curses.KEY_LEFT), + (u"\x1b[1;2A", curses.KEY_SR), + (u"\x1b[1;2B", curses.KEY_SF), + (u"\x1b[1;2C", curses.KEY_SRIGHT), + (u"\x1b[1;2D", curses.KEY_SLEFT), + (u"\x1b[F", curses.KEY_END), + (u"\x1b[H", curses.KEY_HOME), + # not sure where these are from .. please report + (u"\x1b[K", curses.KEY_END), + (u"\x1b[U", curses.KEY_NPAGE), + (u"\x1b[V", curses.KEY_PPAGE), + + # keys sent after term.smkx (keypad_xmit) is emitted, source: + # http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys + # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes + # + # keypad, numlock on + (u"\x1bOM", curses.KEY_ENTER), # noqa return + (u"\x1bOj", KEY_KP_MULTIPLY), # noqa * # pylint: disable=undefined-variable + (u"\x1bOk", KEY_KP_ADD), # noqa + # pylint: disable=undefined-variable + (u"\x1bOl", KEY_KP_SEPARATOR), # noqa , # pylint: disable=undefined-variable + (u"\x1bOm", KEY_KP_SUBTRACT), # noqa - # pylint: disable=undefined-variable + (u"\x1bOn", KEY_KP_DECIMAL), # noqa . # pylint: disable=undefined-variable + (u"\x1bOo", KEY_KP_DIVIDE), # noqa / # pylint: disable=undefined-variable + (u"\x1bOX", KEY_KP_EQUAL), # noqa = # pylint: disable=undefined-variable + (u"\x1bOp", KEY_KP_0), # noqa 0 # pylint: disable=undefined-variable + (u"\x1bOq", KEY_KP_1), # noqa 1 # pylint: disable=undefined-variable + (u"\x1bOr", KEY_KP_2), # noqa 2 # pylint: disable=undefined-variable + (u"\x1bOs", KEY_KP_3), # noqa 3 # pylint: disable=undefined-variable + (u"\x1bOt", KEY_KP_4), # noqa 4 # pylint: disable=undefined-variable + (u"\x1bOu", KEY_KP_5), # noqa 5 # pylint: disable=undefined-variable + (u"\x1bOv", KEY_KP_6), # noqa 6 # pylint: disable=undefined-variable + (u"\x1bOw", KEY_KP_7), # noqa 7 # pylint: disable=undefined-variable + (u"\x1bOx", KEY_KP_8), # noqa 8 # pylint: disable=undefined-variable + (u"\x1bOy", KEY_KP_9), # noqa 9 # pylint: disable=undefined-variable + + # keypad, numlock off + (u"\x1b[1~", curses.KEY_FIND), # find + (u"\x1b[2~", curses.KEY_IC), # insert (0) + (u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute" + (u"\x1b[4~", curses.KEY_SELECT), # select + (u"\x1b[5~", curses.KEY_PPAGE), # pgup (9) + (u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3) + (u"\x1b[7~", curses.KEY_HOME), # home + (u"\x1b[8~", curses.KEY_END), # end + (u"\x1b[OA", curses.KEY_UP), # up (8) + (u"\x1b[OB", curses.KEY_DOWN), # down (2) + (u"\x1b[OC", curses.KEY_RIGHT), # right (6) + (u"\x1b[OD", curses.KEY_LEFT), # left (4) + (u"\x1b[OF", curses.KEY_END), # end (1) + (u"\x1b[OH", curses.KEY_HOME), # home (7) + + # The vt220 placed F1-F4 above the keypad, in place of actual + # F1-F4 were local functions (hold screen, print screen, + # set up, data/talk, break). + (u"\x1bOP", curses.KEY_F1), + (u"\x1bOQ", curses.KEY_F2), + (u"\x1bOR", curses.KEY_F3), + (u"\x1bOS", curses.KEY_F4), +) + +#: Override mixins for a few curses constants with easier +#: mnemonics: there may only be a 1:1 mapping when only a +#: keycode (int) is given, where these phrases are preferred. +CURSES_KEYCODE_OVERRIDE_MIXIN = ( + ('KEY_DELETE', curses.KEY_DC), + ('KEY_INSERT', curses.KEY_IC), + ('KEY_PGUP', curses.KEY_PPAGE), + ('KEY_PGDOWN', curses.KEY_NPAGE), + ('KEY_ESCAPE', curses.KEY_EXIT), + ('KEY_SUP', curses.KEY_SR), + ('KEY_SDOWN', curses.KEY_SF), + ('KEY_UP_LEFT', curses.KEY_A1), + ('KEY_UP_RIGHT', curses.KEY_A3), + ('KEY_CENTER', curses.KEY_B2), + ('KEY_BEGIN', curses.KEY_BEG), +) + +__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',) diff --git a/third_party/python/blessed/blessed/keyboard.pyi b/third_party/python/blessed/blessed/keyboard.pyi new file mode 100644 index 0000000000..ae76393f14 --- /dev/null +++ b/third_party/python/blessed/blessed/keyboard.pyi @@ -0,0 +1,28 @@ +# std imports +from typing import Set, Dict, Type, Mapping, TypeVar, Iterable, Optional, OrderedDict + +# local +from .terminal import Terminal + +_T = TypeVar("_T") + +class Keystroke(str): + def __new__( + cls: Type[_T], + ucs: str = ..., + code: Optional[int] = ..., + name: Optional[str] = ..., + ) -> _T: ... + @property + def is_sequence(self) -> bool: ... + @property + def name(self) -> Optional[str]: ... + @property + def code(self) -> Optional[int]: ... + +def get_keyboard_codes() -> Dict[int, str]: ... +def get_keyboard_sequences(term: Terminal) -> OrderedDict[str, int]: ... +def get_leading_prefixes(sequences: Iterable[str]) -> Set[str]: ... +def resolve_sequence( + text: str, mapper: Mapping[str, int], codes: Mapping[int, str] +) -> Keystroke: ... diff --git a/third_party/python/blessed/blessed/py.typed b/third_party/python/blessed/blessed/py.typed new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/third_party/python/blessed/blessed/py.typed diff --git a/third_party/python/blessed/blessed/sequences.py b/third_party/python/blessed/blessed/sequences.py new file mode 100644 index 0000000000..6735ab8504 --- /dev/null +++ b/third_party/python/blessed/blessed/sequences.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- +"""Module providing 'sequence awareness'.""" +# std imports +import re +import math +import textwrap + +# 3rd party +import six +from wcwidth import wcwidth + +# local +from blessed._capabilities import CAPABILITIES_CAUSE_MOVEMENT + +__all__ = ('Sequence', 'SequenceTextWrapper', 'iter_parse', 'measure_length') + + +class Termcap(object): + """Terminal capability of given variable name and pattern.""" + + def __init__(self, name, pattern, attribute): + """ + Class initializer. + + :arg str name: name describing capability. + :arg str pattern: regular expression string. + :arg str attribute: :class:`~.Terminal` attribute used to build + this terminal capability. + """ + self.name = name + self.pattern = pattern + self.attribute = attribute + self._re_compiled = None + + def __repr__(self): + # pylint: disable=redundant-keyword-arg + return '<Termcap {self.name}:{self.pattern!r}>'.format(self=self) + + @property + def named_pattern(self): + """Regular expression pattern for capability with named group.""" + # pylint: disable=redundant-keyword-arg + return '(?P<{self.name}>{self.pattern})'.format(self=self) + + @property + def re_compiled(self): + """Compiled regular expression pattern for capability.""" + if self._re_compiled is None: + self._re_compiled = re.compile(self.pattern) + return self._re_compiled + + @property + def will_move(self): + """Whether capability causes cursor movement.""" + return self.name in CAPABILITIES_CAUSE_MOVEMENT + + def horizontal_distance(self, text): + """ + Horizontal carriage adjusted by capability, may be negative. + + :rtype: int + :arg str text: for capabilities *parm_left_cursor*, + *parm_right_cursor*, provide the matching sequence + text, its interpreted distance is returned. + + :returns: 0 except for matching ' + """ + value = { + 'cursor_left': -1, + 'backspace': -1, + 'cursor_right': 1, + 'tab': 8, + 'ascii_tab': 8, + }.get(self.name, None) + if value is not None: + return value + + unit = { + 'parm_left_cursor': -1, + 'parm_right_cursor': 1 + }.get(self.name, None) + if unit is not None: + value = int(self.re_compiled.match(text).group(1)) + return unit * value + + return 0 + + # pylint: disable=too-many-arguments + @classmethod + def build(cls, name, capability, attribute, nparams=0, + numeric=99, match_grouped=False, match_any=False, + match_optional=False): + r""" + Class factory builder for given capability definition. + + :arg str name: Variable name given for this pattern. + :arg str capability: A unicode string representing a terminal + capability to build for. When ``nparams`` is non-zero, it + must be a callable unicode string (such as the result from + ``getattr(term, 'bold')``. + :arg str attribute: The terminfo(5) capability name by which this + pattern is known. + :arg int nparams: number of positional arguments for callable. + :arg int numeric: Value to substitute into capability to when generating pattern + :arg bool match_grouped: If the numeric pattern should be + grouped, ``(\d+)`` when ``True``, ``\d+`` default. + :arg bool match_any: When keyword argument ``nparams`` is given, + *any* numeric found in output is suitable for building as + pattern ``(\d+)``. Otherwise, only the first matching value of + range *(numeric - 1)* through *(numeric + 1)* will be replaced by + pattern ``(\d+)`` in builder. + :arg bool match_optional: When ``True``, building of numeric patterns + containing ``(\d+)`` will be built as optional, ``(\d+)?``. + :rtype: blessed.sequences.Termcap + :returns: Terminal capability instance for given capability definition + """ + _numeric_regex = r'\d+' + if match_grouped: + _numeric_regex = r'(\d+)' + if match_optional: + _numeric_regex = r'(\d+)?' + numeric = 99 if numeric is None else numeric + + # basic capability attribute, not used as a callable + if nparams == 0: + return cls(name, re.escape(capability), attribute) + + # a callable capability accepting numeric argument + _outp = re.escape(capability(*(numeric,) * nparams)) + if not match_any: + for num in range(numeric - 1, numeric + 2): + if str(num) in _outp: + pattern = _outp.replace(str(num), _numeric_regex) + return cls(name, pattern, attribute) + + if match_grouped: + pattern = re.sub(r'(\d+)', lambda x: _numeric_regex, _outp) + else: + pattern = re.sub(r'\d+', lambda x: _numeric_regex, _outp) + return cls(name, pattern, attribute) + + +class SequenceTextWrapper(textwrap.TextWrapper): + """Docstring overridden.""" + + def __init__(self, width, term, **kwargs): + """ + Class initializer. + + This class supports the :meth:`~.Terminal.wrap` method. + """ + self.term = term + textwrap.TextWrapper.__init__(self, width, **kwargs) + + def _wrap_chunks(self, chunks): + """ + Sequence-aware variant of :meth:`textwrap.TextWrapper._wrap_chunks`. + + :raises ValueError: ``self.width`` is not a positive integer + :rtype: list + :returns: text chunks adjusted for width + + This simply ensures that word boundaries are not broken mid-sequence, as standard python + textwrap would incorrectly determine the length of a string containing sequences, and may + also break consider sequences part of a "word" that may be broken by hyphen (``-``), where + this implementation corrects both. + """ + lines = [] + if self.width <= 0 or not isinstance(self.width, int): + raise ValueError( + "invalid width {0!r}({1!r}) (must be integer > 0)" + .format(self.width, type(self.width))) + + term = self.term + drop_whitespace = not hasattr(self, 'drop_whitespace' + ) or self.drop_whitespace + chunks.reverse() + while chunks: + cur_line = [] + cur_len = 0 + indent = self.subsequent_indent if lines else self.initial_indent + width = self.width - len(indent) + if drop_whitespace and ( + Sequence(chunks[-1], term).strip() == '' and lines): + del chunks[-1] + while chunks: + chunk_len = Sequence(chunks[-1], term).length() + if cur_len + chunk_len > width: + break + cur_line.append(chunks.pop()) + cur_len += chunk_len + if chunks and Sequence(chunks[-1], term).length() > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + if drop_whitespace and ( + cur_line and Sequence(cur_line[-1], term).strip() == ''): + del cur_line[-1] + if cur_line: + lines.append(indent + u''.join(cur_line)) + return lines + + def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + """ + Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`. + + This simply ensures that word boundaries are not broken mid-sequence, as standard python + textwrap would incorrectly determine the length of a string containing sequences, and may + also break consider sequences part of a "word" that may be broken by hyphen (``-``), where + this implementation corrects both. + """ + # Figure out when indent is larger than the specified width, and make + # sure at least one character is stripped off on every pass + space_left = 1 if width < 1 else width - cur_len + # If we're allowed to break long words, then do so: put as much + # of the next chunk onto the current line as will fit. + + if self.break_long_words: + term = self.term + chunk = reversed_chunks[-1] + idx = nxt = 0 + for text, _ in iter_parse(term, chunk): + nxt += len(text) + if Sequence(chunk[:nxt], term).length() > space_left: + break + idx = nxt + cur_line.append(chunk[:idx]) + reversed_chunks[-1] = chunk[idx:] + + # Otherwise, we have to preserve the long word intact. Only add + # it to the current line if there's nothing already there -- + # that minimizes how much we violate the width constraint. + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + # If we're not allowed to break long words, and there's already + # text on the current line, do nothing. Next time through the + # main loop of _wrap_chunks(), we'll wind up here again, but + # cur_len will be zero, so the next line will be entirely + # devoted to the long word that we can't handle right now. + + +SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__ + + +class Sequence(six.text_type): + """ + A "sequence-aware" version of the base :class:`str` class. + + This unicode-derived class understands the effect of escape sequences + of printable length, allowing a properly implemented :meth:`rjust`, + :meth:`ljust`, :meth:`center`, and :meth:`length`. + """ + + def __new__(cls, sequence_text, term): + # pylint: disable = missing-return-doc, missing-return-type-doc + """ + Class constructor. + + :arg str sequence_text: A string that may contain sequences. + :arg blessed.Terminal term: :class:`~.Terminal` instance. + """ + new = six.text_type.__new__(cls, sequence_text) + new._term = term + return new + + def ljust(self, width, fillchar=u' '): + """ + Return string containing sequences, left-adjusted. + + :arg int width: Total width given to left-adjust ``text``. If + unspecified, the width of the attached terminal is used (default). + :arg str fillchar: String for padding right-of ``text``. + :returns: String of ``text``, left-aligned by ``width``. + :rtype: str + """ + rightside = fillchar * int( + (max(0.0, float(width.__index__() - self.length()))) / float(len(fillchar))) + return u''.join((self, rightside)) + + def rjust(self, width, fillchar=u' '): + """ + Return string containing sequences, right-adjusted. + + :arg int width: Total width given to right-adjust ``text``. If + unspecified, the width of the attached terminal is used (default). + :arg str fillchar: String for padding left-of ``text``. + :returns: String of ``text``, right-aligned by ``width``. + :rtype: str + """ + leftside = fillchar * int( + (max(0.0, float(width.__index__() - self.length()))) / float(len(fillchar))) + return u''.join((leftside, self)) + + def center(self, width, fillchar=u' '): + """ + Return string containing sequences, centered. + + :arg int width: Total width given to center ``text``. If + unspecified, the width of the attached terminal is used (default). + :arg str fillchar: String for padding left and right-of ``text``. + :returns: String of ``text``, centered by ``width``. + :rtype: str + """ + split = max(0.0, float(width.__index__()) - self.length()) / 2 + leftside = fillchar * int( + (max(0.0, math.floor(split))) / float(len(fillchar))) + rightside = fillchar * int( + (max(0.0, math.ceil(split))) / float(len(fillchar))) + return u''.join((leftside, self, rightside)) + + def truncate(self, width): + """ + Truncate a string in a sequence-aware manner. + + Any printable characters beyond ``width`` are removed, while all + sequences remain in place. Horizontal Sequences are first expanded + by :meth:`padd`. + + :arg int width: The printable width to truncate the string to. + :rtype: str + :returns: String truncated to at most ``width`` printable characters. + """ + output = "" + current_width = 0 + target_width = width.__index__() + parsed_seq = iter_parse(self._term, self.padd()) + + # Retain all text until non-cap width reaches desired width + for text, cap in parsed_seq: + if not cap: + # use wcwidth clipped to 0 because it can sometimes return -1 + current_width += max(wcwidth(text), 0) + if current_width > target_width: + break + output += text + + # Return with remaining caps appended + return output + ''.join(text for text, cap in parsed_seq if cap) + + def length(self): + r""" + Return the printable length of string containing sequences. + + Strings containing ``term.left`` or ``\b`` will cause "overstrike", + but a length less than 0 is not ever returned. So ``_\b+`` is a + length of 1 (displays as ``+``), but ``\b`` alone is simply a + length of 0. + + Some characters may consume more than one cell, mainly those CJK + Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode + as half or full-width characters. + + For example: + + >>> from blessed import Terminal + >>> from blessed.sequences import Sequence + >>> term = Terminal() + >>> msg = term.clear + term.red(u'コンニチハ') + >>> Sequence(msg, term).length() + 10 + + .. note:: Although accounted for, strings containing sequences such + as ``term.clear`` will not give accurate returns, it is not + considered lengthy (a length of 0). + """ + # because control characters may return -1, "clip" their length to 0. + return sum(max(wcwidth(w_char), 0) for w_char in self.padd(strip=True)) + + def strip(self, chars=None): + """ + Return string of sequences, leading and trailing whitespace removed. + + :arg str chars: Remove characters in chars instead of whitespace. + :rtype: str + :returns: string of sequences with leading and trailing whitespace removed. + """ + return self.strip_seqs().strip(chars) + + def lstrip(self, chars=None): + """ + Return string of all sequences and leading whitespace removed. + + :arg str chars: Remove characters in chars instead of whitespace. + :rtype: str + :returns: string of sequences with leading removed. + """ + return self.strip_seqs().lstrip(chars) + + def rstrip(self, chars=None): + """ + Return string of all sequences and trailing whitespace removed. + + :arg str chars: Remove characters in chars instead of whitespace. + :rtype: str + :returns: string of sequences with trailing removed. + """ + return self.strip_seqs().rstrip(chars) + + def strip_seqs(self): + """ + Return ``text`` stripped of only its terminal sequences. + + :rtype: str + :returns: Text with terminal sequences removed + """ + return self.padd(strip=True) + + def padd(self, strip=False): + """ + Return non-destructive horizontal movement as destructive spacing. + + :arg bool strip: Strip terminal sequences + :rtype: str + :returns: Text adjusted for horizontal movement + """ + outp = '' + for text, cap in iter_parse(self._term, self): + if not cap: + outp += text + continue + + value = cap.horizontal_distance(text) + if value > 0: + outp += ' ' * value + elif value < 0: + outp = outp[:value] + elif not strip: + outp += text + return outp + + +def iter_parse(term, text): + """ + Generator yields (text, capability) for characters of ``text``. + + value for ``capability`` may be ``None``, where ``text`` is + :class:`str` of length 1. Otherwise, ``text`` is a full + matching sequence of given capability. + """ + for match in term._caps_compiled_any.finditer(text): # pylint: disable=protected-access + name = match.lastgroup + value = match.group(name) + if name == 'MISMATCH': + yield (value, None) + else: + yield value, term.caps[name] + + +def measure_length(text, term): + """ + .. deprecated:: 1.12.0. + + :rtype: int + :returns: Length of the first sequence in the string + """ + try: + text, capability = next(iter_parse(term, text)) + if capability: + return len(text) + except StopIteration: + return 0 + return 0 diff --git a/third_party/python/blessed/blessed/sequences.pyi b/third_party/python/blessed/blessed/sequences.pyi new file mode 100644 index 0000000000..4460b7a466 --- /dev/null +++ b/third_party/python/blessed/blessed/sequences.pyi @@ -0,0 +1,55 @@ +# std imports +import textwrap +from typing import Any, Type, Tuple, Pattern, TypeVar, Iterator, Optional, SupportsIndex + +# local +from .terminal import Terminal + +_T = TypeVar("_T") + +class Termcap: + name: str = ... + pattern: str = ... + attribute: str = ... + def __init__(self, name: str, pattern: str, attribute: str) -> None: ... + @property + def named_pattern(self) -> str: ... + @property + def re_compiled(self) -> Pattern[str]: ... + @property + def will_move(self) -> bool: ... + def horizontal_distance(self, text: str) -> int: ... + @classmethod + def build( + cls, + name: str, + capability: str, + attribute: str, + nparams: int = ..., + numeric: int = ..., + match_grouped: bool = ..., + match_any: bool = ..., + match_optional: bool = ..., + ) -> "Termcap": ... + +class SequenceTextWrapper(textwrap.TextWrapper): + term: Terminal = ... + def __init__(self, width: int, term: Terminal, **kwargs: Any) -> None: ... + +class Sequence(str): + def __new__(cls: Type[_T], sequence_text: str, term: Terminal) -> _T: ... + def ljust(self, width: SupportsIndex, fillchar: str = ...) -> str: ... + def rjust(self, width: SupportsIndex, fillchar: str = ...) -> str: ... + def center(self, width: SupportsIndex, fillchar: str = ...) -> str: ... + def truncate(self, width: SupportsIndex) -> str: ... + def length(self) -> int: ... + def strip(self, chars: Optional[str] = ...) -> str: ... + def lstrip(self, chars: Optional[str] = ...) -> str: ... + def rstrip(self, chars: Optional[str] = ...) -> str: ... + def strip_seqs(self) -> str: ... + def padd(self, strip: bool = ...) -> str: ... + +def iter_parse( + term: Terminal, text: str +) -> Iterator[Tuple[str, Optional[Termcap]]]: ... +def measure_length(text: str, term: Terminal) -> int: ... diff --git a/third_party/python/blessed/blessed/terminal.py b/third_party/python/blessed/blessed/terminal.py new file mode 100644 index 0000000000..38bd2bb66b --- /dev/null +++ b/third_party/python/blessed/blessed/terminal.py @@ -0,0 +1,1502 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-lines +"""Module containing :class:`Terminal`, the primary API entry point.""" +# std imports +import os +import re +import sys +import time +import codecs +import locale +import select +import struct +import platform +import warnings +import functools +import contextlib +import collections + +# local +from .color import COLOR_DISTANCE_ALGORITHMS +from .keyboard import (_time_left, + _read_until, + resolve_sequence, + get_keyboard_codes, + get_leading_prefixes, + get_keyboard_sequences) +from .sequences import Termcap, Sequence, SequenceTextWrapper +from .colorspace import RGB_256TABLE +from .formatters import (COLORS, + COMPOUNDABLES, + FormattingString, + NullCallableString, + ParameterizingString, + FormattingOtherString, + split_compound, + resolve_attribute, + resolve_capability) +from ._capabilities import CAPABILITY_DATABASE, CAPABILITIES_ADDITIVES, CAPABILITIES_RAW_MIXIN + +# isort: off + +try: + InterruptedError +except NameError: + # alias py2 exception to py3 + # pylint: disable=redefined-builtin + InterruptedError = select.error + + +HAS_TTY = True +if platform.system() == 'Windows': + IS_WINDOWS = True + import jinxed as curses # pylint: disable=import-error + from jinxed.win32 import get_console_input_encoding # pylint: disable=import-error +else: + IS_WINDOWS = False + import curses + + try: + import fcntl + import termios + import tty + except ImportError: + _TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width') + _MSG_NOSUPPORT = ( + "One or more of the modules: 'termios', 'fcntl', and 'tty' " + "are not found on your platform '{platform}'. " + "The following methods of Terminal are dummy/no-op " + "unless a deriving class overrides them: {tty_methods}." + .format(platform=platform.system(), + tty_methods=', '.join(_TTY_METHODS))) + warnings.warn(_MSG_NOSUPPORT) + HAS_TTY = False + +_CUR_TERM = None # See comments at end of file + + +class Terminal(object): + """ + An abstraction for color, style, positioning, and input in the terminal. + + This keeps the endless calls to ``tigetstr()`` and ``tparm()`` out of your code, acts + intelligently when somebody pipes your output to a non-terminal, and abstracts over the + complexity of unbuffered keyboard input. It uses the terminfo database to remain portable across + terminal types. + """ + # pylint: disable=too-many-instance-attributes,too-many-public-methods + # Too many public methods (28/20) + # Too many instance attributes (12/7) + + #: Sugary names for commonly-used capabilities + _sugar = dict( + save='sc', + restore='rc', + clear_eol='el', + clear_bol='el1', + clear_eos='ed', + enter_fullscreen='smcup', + exit_fullscreen='rmcup', + move='cup', + move_yx='cup', + move_x='hpa', + move_y='vpa', + hide_cursor='civis', + normal_cursor='cnorm', + reset_colors='op', + normal='sgr0', + reverse='rev', + italic='sitm', + no_italic='ritm', + shadow='sshm', + no_shadow='rshm', + standout='smso', + no_standout='rmso', + subscript='ssubm', + no_subscript='rsubm', + superscript='ssupm', + no_superscript='rsupm', + underline='smul', + no_underline='rmul', + cursor_report='u6', + cursor_request='u7', + terminal_answerback='u8', + terminal_enquire='u9', + ) + + def __init__(self, kind=None, stream=None, force_styling=False): + """ + Initialize the terminal. + + :arg str kind: A terminal string as taken by :func:`curses.setupterm`. + Defaults to the value of the ``TERM`` environment variable. + + .. note:: Terminals withing a single process must share a common + ``kind``. See :obj:`_CUR_TERM`. + + :arg file stream: A file-like object representing the Terminal output. + Defaults to the original value of :obj:`sys.__stdout__`, like + :func:`curses.initscr` does. + + If ``stream`` is not a tty, empty Unicode strings are returned for + all capability values, so things like piping your program output to + a pipe or file does not emit terminal sequences. + + :arg bool force_styling: Whether to force the emission of capabilities + even if :obj:`sys.__stdout__` does not seem to be connected to a + terminal. If you want to force styling to not happen, use + ``force_styling=None``. + + This comes in handy if users are trying to pipe your output through + something like ``less -r`` or build systems which support decoding + of terminal sequences. + """ + # pylint: disable=global-statement,too-many-branches + global _CUR_TERM + self.errors = ['parameters: kind=%r, stream=%r, force_styling=%r' % + (kind, stream, force_styling)] + self._normal = None # cache normal attr, preventing recursive lookups + # we assume our input stream to be line-buffered until either the + # cbreak of raw context manager methods are entered with an attached tty. + self._line_buffered = True + + self._stream = stream + self._keyboard_fd = None + self._init_descriptor = None + self._is_a_tty = False + self.__init__streams() + + if IS_WINDOWS and self._init_descriptor is not None: + self._kind = kind or curses.get_term(self._init_descriptor) + else: + self._kind = kind or os.environ.get('TERM', 'dumb') or 'dumb' + + self._does_styling = False + if force_styling is None and self.is_a_tty: + self.errors.append('force_styling is None') + elif force_styling or self.is_a_tty: + self._does_styling = True + + if self.does_styling: + # Initialize curses (call setupterm), so things like tigetstr() work. + try: + curses.setupterm(self._kind, self._init_descriptor) + except curses.error as err: + msg = 'Failed to setupterm(kind={0!r}): {1}'.format(self._kind, err) + warnings.warn(msg) + self.errors.append(msg) + self._kind = None + self._does_styling = False + else: + if _CUR_TERM is None or self._kind == _CUR_TERM: + _CUR_TERM = self._kind + else: + # termcap 'kind' is immutable in a python process! Once + # initialized by setupterm, it is unsupported by the + # 'curses' module to change the terminal type again. If you + # are a downstream developer and you need this + # functionality, consider sub-processing, instead. + warnings.warn( + 'A terminal of kind "%s" has been requested; due to an' + ' internal python curses bug, terminal capabilities' + ' for a terminal of kind "%s" will continue to be' + ' returned for the remainder of this process.' % ( + self._kind, _CUR_TERM,)) + + self.__init__color_capabilities() + self.__init__capabilities() + self.__init__keycodes() + + def __init__streams(self): + # pylint: disable=too-complex,too-many-branches + # Agree to disagree ! + stream_fd = None + + # Default stream is stdout + if self._stream is None: + self._stream = sys.__stdout__ + + if not hasattr(self._stream, 'fileno'): + self.errors.append('stream has no fileno method') + elif not callable(self._stream.fileno): + self.errors.append('stream.fileno is not callable') + else: + try: + stream_fd = self._stream.fileno() + except ValueError as err: + # The stream is not a file, such as the case of StringIO, or, when it has been + # "detached", such as might be the case of stdout in some test scenarios. + self.errors.append('Unable to determine output stream file descriptor: %s' % err) + else: + self._is_a_tty = os.isatty(stream_fd) + if not self._is_a_tty: + self.errors.append('stream not a TTY') + + # Keyboard valid as stdin only when output stream is stdout or stderr and is a tty. + if self._stream in (sys.__stdout__, sys.__stderr__): + try: + self._keyboard_fd = sys.__stdin__.fileno() + except (AttributeError, ValueError) as err: + self.errors.append('Unable to determine input stream file descriptor: %s' % err) + else: + # _keyboard_fd only non-None if both stdin and stdout is a tty. + if not self.is_a_tty: + self.errors.append('Output stream is not a TTY') + self._keyboard_fd = None + elif not os.isatty(self._keyboard_fd): + self.errors.append('Input stream is not a TTY') + self._keyboard_fd = None + else: + self.errors.append('Output stream is not a default stream') + + # The descriptor to direct terminal initialization sequences to. + self._init_descriptor = stream_fd + if stream_fd is None: + try: + self._init_descriptor = sys.__stdout__.fileno() + except ValueError as err: + self.errors.append('Unable to determine __stdout__ file descriptor: %s' % err) + + def __init__color_capabilities(self): + self._color_distance_algorithm = 'cie2000' + if not self.does_styling: + self.number_of_colors = 0 + elif IS_WINDOWS or os.environ.get('COLORTERM') in ('truecolor', '24bit'): + self.number_of_colors = 1 << 24 + else: + self.number_of_colors = max(0, curses.tigetnum('colors') or -1) + + def __clear_color_capabilities(self): + for cached_color_cap in set(dir(self)) & COLORS: + delattr(self, cached_color_cap) + + def __init__capabilities(self): + # important that we lay these in their ordered direction, so that our + # preferred, 'color' over 'set_a_attributes1', for example. + self.caps = collections.OrderedDict() + + # some static injected patterns, esp. without named attribute access. + for name, (attribute, pattern) in CAPABILITIES_ADDITIVES.items(): + self.caps[name] = Termcap(name, pattern, attribute) + + for name, (attribute, kwds) in CAPABILITY_DATABASE.items(): + if self.does_styling: + # attempt dynamic lookup + cap = getattr(self, attribute) + if cap: + self.caps[name] = Termcap.build( + name, cap, attribute, **kwds) + continue + + # fall-back + pattern = CAPABILITIES_RAW_MIXIN.get(name) + if pattern: + self.caps[name] = Termcap(name, pattern, attribute) + + # make a compiled named regular expression table + self.caps_compiled = re.compile( + '|'.join(cap.pattern for name, cap in self.caps.items())) + + # for tokenizer, the '.lastgroup' is the primary lookup key for + # 'self.caps', unless 'MISMATCH'; then it is an unmatched character. + self._caps_compiled_any = re.compile('|'.join( + cap.named_pattern for name, cap in self.caps.items() + ) + '|(?P<MISMATCH>.)') + self._caps_unnamed_any = re.compile('|'.join( + '({0})'.format(cap.pattern) for name, cap in self.caps.items() + ) + '|(.)') + + def __init__keycodes(self): + # Initialize keyboard data determined by capability. + # Build database of int code <=> KEY_NAME. + self._keycodes = get_keyboard_codes() + + # Store attributes as: self.KEY_NAME = code. + for key_code, key_name in self._keycodes.items(): + setattr(self, key_name, key_code) + + # Build database of sequence <=> KEY_NAME. + self._keymap = get_keyboard_sequences(self) + + # build set of prefixes of sequences + self._keymap_prefixes = get_leading_prefixes(self._keymap) + + # keyboard stream buffer + self._keyboard_buf = collections.deque() + + if self._keyboard_fd is not None: + # set input encoding and initialize incremental decoder + + if IS_WINDOWS: + self._encoding = get_console_input_encoding() \ + or locale.getpreferredencoding() or 'UTF-8' + else: + self._encoding = locale.getpreferredencoding() or 'UTF-8' + + try: + self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)() + except LookupError as err: + # encoding is illegal or unsupported, use 'UTF-8' + warnings.warn('LookupError: {0}, defaulting to UTF-8 for keyboard.'.format(err)) + self._encoding = 'UTF-8' + self._keyboard_decoder = codecs.getincrementaldecoder(self._encoding)() + + def __getattr__(self, attr): + r""" + Return a terminal capability as Unicode string. + + For example, ``term.bold`` is a unicode string that may be prepended + to text to set the video attribute for bold, which should also be + terminated with the pairing :attr:`normal`. This capability + returns a callable, so you can use ``term.bold("hi")`` which + results in the joining of ``(term.bold, "hi", term.normal)``. + + Compound formatters may also be used. For example:: + + >>> term.bold_blink_red_on_green("merry x-mas!") + + For a parameterized capability such as ``move`` (or ``cup``), pass the + parameters as positional arguments:: + + >>> term.move(line, column) + + See the manual page `terminfo(5) + <https://invisible-island.net/ncurses/man/terminfo.5.html>`_ for a + complete list of capabilities and their arguments. + """ + if not self._does_styling: + return NullCallableString() + # Fetch the missing 'attribute' into some kind of curses-resolved + # capability, and cache by attaching to this Terminal class instance. + # + # Note that this will prevent future calls to __getattr__(), but + # that's precisely the idea of the cache! + val = resolve_attribute(self, attr) + setattr(self, attr, val) + return val + + @property + def kind(self): + """ + Read-only property: Terminal kind determined on class initialization. + + :rtype: str + """ + return self._kind + + @property + def does_styling(self): + """ + Read-only property: Whether this class instance may emit sequences. + + :rtype: bool + """ + return self._does_styling + + @property + def is_a_tty(self): + """ + Read-only property: Whether :attr:`~.stream` is a terminal. + + :rtype: bool + """ + return self._is_a_tty + + @property + def height(self): + """ + Read-only property: Height of the terminal (in number of lines). + + :rtype: int + """ + return self._height_and_width().ws_row + + @property + def width(self): + """ + Read-only property: Width of the terminal (in number of columns). + + :rtype: int + """ + return self._height_and_width().ws_col + + @property + def pixel_height(self): + """ + Read-only property: Height ofthe terminal (in pixels). + + :rtype: int + """ + return self._height_and_width().ws_ypixel + + @property + def pixel_width(self): + """ + Read-only property: Width of terminal (in pixels). + + :rtype: int + """ + return self._height_and_width().ws_xpixel + + @staticmethod + def _winsize(fd): + """ + Return named tuple describing size of the terminal by ``fd``. + + If the given platform does not have modules :mod:`termios`, + :mod:`fcntl`, or :mod:`tty`, window size of 80 columns by 25 + rows is always returned. + + :arg int fd: file descriptor queries for its window size. + :raises IOError: the file descriptor ``fd`` is not a terminal. + :rtype: WINSZ + :returns: named tuple describing size of the terminal + + WINSZ is a :class:`collections.namedtuple` instance, whose structure + directly maps to the return value of the :const:`termios.TIOCGWINSZ` + ioctl return value. The return parameters are: + + - ``ws_row``: width of terminal by its number of character cells. + - ``ws_col``: height of terminal by its number of character cells. + - ``ws_xpixel``: width of terminal by pixels (not accurate). + - ``ws_ypixel``: height of terminal by pixels (not accurate). + """ + if HAS_TTY: + # pylint: disable=protected-access + data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF) + return WINSZ(*struct.unpack(WINSZ._FMT, data)) + return WINSZ(ws_row=25, ws_col=80, ws_xpixel=0, ws_ypixel=0) + + def _height_and_width(self): + """ + Return a tuple of (terminal height, terminal width). + + If :attr:`stream` or :obj:`sys.__stdout__` is not a tty or does not + support :func:`fcntl.ioctl` of :const:`termios.TIOCGWINSZ`, a window + size of 80 columns by 25 rows is returned for any values not + represented by environment variables ``LINES`` and ``COLUMNS``, which + is the default text mode of IBM PC compatibles. + + :rtype: WINSZ + :returns: Named tuple specifying the terminal size + + WINSZ is a :class:`collections.namedtuple` instance, whose structure + directly maps to the return value of the :const:`termios.TIOCGWINSZ` + ioctl return value. The return parameters are: + + - ``ws_row``: height of terminal by its number of cell rows. + - ``ws_col``: width of terminal by its number of cell columns. + - ``ws_xpixel``: width of terminal by pixels (not accurate). + - ``ws_ypixel``: height of terminal by pixels (not accurate). + + .. note:: the peculiar (height, width, width, height) order, which + matches the return order of TIOCGWINSZ! + """ + for fd in (self._init_descriptor, sys.__stdout__): + try: + if fd is not None: + return self._winsize(fd) + except (IOError, OSError, ValueError, TypeError): # pylint: disable=overlapping-except + pass + + return WINSZ(ws_row=int(os.getenv('LINES', '25')), + ws_col=int(os.getenv('COLUMNS', '80')), + ws_xpixel=None, + ws_ypixel=None) + + @contextlib.contextmanager + def location(self, x=None, y=None): + """ + Context manager for temporarily moving the cursor. + + :arg int x: horizontal position, from left, *0*, to right edge of screen, *self.width - 1*. + :arg int y: vertical position, from top, *0*, to bottom of screen, *self.height - 1*. + :return: a context manager. + :rtype: Iterator + + Move the cursor to a certain position on entry, do any kind of I/O, and upon exit + let you print stuff there, then return the cursor to its original position: + + + .. code-block:: python + + term = Terminal() + with term.location(y=0, x=0): + for row_num in range(term.height-1): + print('Row #{row_num}') + print(term.clear_eol + 'Back to original location.') + + Specify ``x`` to move to a certain column, ``y`` to move to a certain + row, both, or neither. If you specify neither, only the saving and + restoration of cursor position will happen. This can be useful if you + simply want to restore your place after doing some manual cursor + movement. + + Calls cannot be nested: only one should be entered at a time. + + .. note:: The argument order *(x, y)* differs from the return value order *(y, x)* + of :meth:`get_location`, or argument order *(y, x)* of :meth:`move`. This is + for API Compaibility with the blessings library, sorry for the trouble! + """ + # pylint: disable=invalid-name + # Invalid argument name "x" + + # Save position and move to the requested column, row, or both: + self.stream.write(self.save) + if x is not None and y is not None: + self.stream.write(self.move(y, x)) + elif x is not None: + self.stream.write(self.move_x(x)) + elif y is not None: + self.stream.write(self.move_y(y)) + try: + self.stream.flush() + yield + finally: + # Restore original cursor position: + self.stream.write(self.restore) + self.stream.flush() + + def get_location(self, timeout=None): + r""" + Return tuple (row, column) of cursor position. + + :arg float timeout: Return after time elapsed in seconds with value ``(-1, -1)`` indicating + that the remote end did not respond. + :rtype: tuple + :returns: cursor position as tuple in form of ``(y, x)``. When a timeout is specified, + always ensure the return value is checked for ``(-1, -1)``. + + The location of the cursor is determined by emitting the ``u7`` terminal capability, or + VT100 `Query Cursor Position + <https://www2.ccs.neu.edu/research/gpc/VonaUtils/vona/terminal/vtansi.htm#status>`_ + when such capability is undefined, which elicits a response from a reply string described by + capability ``u6``, or again VT100's definition of ``\x1b[%i%d;%dR`` when undefined. + + The ``(y, x)`` return value matches the parameter order of the :meth:`move_xy` capability. + The following sequence should cause the cursor to not move at all:: + + >>> term = Terminal() + >>> term.move_yx(*term.get_location())) + + And the following should assert True with a terminal: + + >>> term = Terminal() + >>> given_y, given_x = 10, 20 + >>> with term.location(y=given_y, x=given_x): + ... result_y, result_x = term.get_location() + ... + >>> assert given_x == result_x, (given_x, result_x) + >>> assert given_y == result_y, (given_y, result_y) + + """ + # Local lines attached by termios and remote login protocols such as + # ssh and telnet both provide a means to determine the window + # dimensions of a connected client, but **no means to determine the + # location of the cursor**. + # + # from https://invisible-island.net/ncurses/terminfo.src.html, + # + # > The System V Release 4 and XPG4 terminfo format defines ten string + # > capabilities for use by applications, <u0>...<u9>. In this file, + # > we use certain of these capabilities to describe functions which + # > are not covered by terminfo. The mapping is as follows: + # > + # > u9 terminal enquire string (equiv. to ANSI/ECMA-48 DA) + # > u8 terminal answerback description + # > u7 cursor position request (equiv. to VT100/ANSI/ECMA-48 DSR 6) + # > u6 cursor position report (equiv. to ANSI/ECMA-48 CPR) + query_str = self.u7 or u'\x1b[6n' + response_str = getattr(self, self.caps['cursor_report'].attribute) or u'\x1b[%i%d;%dR' + + # determine response format as a regular expression + response_re = self.caps['cursor_report'].re_compiled + + # Avoid changing user's desired raw or cbreak mode if already entered, + # by entering cbreak mode ourselves. This is necessary to receive user + # input without awaiting a human to press the return key. This mode + # also disables echo, which we should also hide, as our input is an + # sequence that is not meaningful for display as an output sequence. + + ctx = None + try: + if self._line_buffered: + ctx = self.cbreak() + ctx.__enter__() # pylint: disable=no-member + + # emit the 'query cursor position' sequence, + self.stream.write(query_str) + self.stream.flush() + + # expect a response, + match, data = _read_until(term=self, + pattern=response_re, + timeout=timeout) + + # ensure response sequence is excluded from subsequent input, + if match: + data = (data[:match.start()] + data[match.end():]) + + # re-buffer keyboard data, if any + self.ungetch(data) + + if match: + # return matching sequence response, the cursor location. + row, col = (int(val) for val in match.groups()) + + # Per https://invisible-island.net/ncurses/terminfo.src.html + # The cursor position report (<u6>) string must contain two + # scanf(3)-style %d format elements. The first of these must + # correspond to the Y coordinate and the second to the %d. + # If the string contains the sequence %i, it is taken as an + # instruction to decrement each value after reading it (this is + # the inverse sense from the cup string). + if u'%i' in response_str: + row -= 1 + col -= 1 + return row, col + + finally: + if ctx is not None: + ctx.__exit__(None, None, None) # pylint: disable=no-member + + # We chose to return an illegal value rather than an exception, + # favoring that users author function filters, such as max(0, y), + # rather than crowbarring such logic into an exception handler. + return -1, -1 + + @contextlib.contextmanager + def fullscreen(self): + """ + Context manager that switches to secondary screen, restoring on exit. + + Under the hood, this switches between the primary screen buffer and + the secondary one. The primary one is saved on entry and restored on + exit. Likewise, the secondary contents are also stable and are + faithfully restored on the next entry:: + + with term.fullscreen(): + main() + + .. note:: There is only one primary and one secondary screen buffer. + :meth:`fullscreen` calls cannot be nested, only one should be + entered at a time. + """ + self.stream.write(self.enter_fullscreen) + self.stream.flush() + try: + yield + finally: + self.stream.write(self.exit_fullscreen) + self.stream.flush() + + @contextlib.contextmanager + def hidden_cursor(self): + """ + Context manager that hides the cursor, setting visibility on exit. + + with term.hidden_cursor(): + main() + + .. note:: :meth:`hidden_cursor` calls cannot be nested: only one + should be entered at a time. + """ + self.stream.write(self.hide_cursor) + self.stream.flush() + try: + yield + finally: + self.stream.write(self.normal_cursor) + self.stream.flush() + + def move_xy(self, x, y): + """ + A callable string that moves the cursor to the given ``(x, y)`` screen coordinates. + + :arg int x: horizontal position, from left, *0*, to right edge of screen, *self.width - 1*. + :arg int y: vertical position, from top, *0*, to bottom of screen, *self.height - 1*. + :rtype: ParameterizingString + :returns: Callable string that moves the cursor to the given coordinates + """ + # this is just a convenience alias to the built-in, but hidden 'move' + # attribute -- we encourage folks to use only (x, y) positional + # arguments, or, if they must use (y, x), then use the 'move_yx' + # alias. + return self.move(y, x) + + def move_yx(self, y, x): + """ + A callable string that moves the cursor to the given ``(y, x)`` screen coordinates. + + :arg int y: vertical position, from top, *0*, to bottom of screen, *self.height - 1*. + :arg int x: horizontal position, from left, *0*, to right edge of screen, *self.width - 1*. + :rtype: ParameterizingString + :returns: Callable string that moves the cursor to the given coordinates + """ + return self.move(y, x) + + @property + def move_left(self): + """Move cursor 1 cells to the left, or callable string for n>1 cells.""" + return FormattingOtherString(self.cub1, ParameterizingString(self.cub)) + + @property + def move_right(self): + """Move cursor 1 or more cells to the right, or callable string for n>1 cells.""" + return FormattingOtherString(self.cuf1, ParameterizingString(self.cuf)) + + @property + def move_up(self): + """Move cursor 1 or more cells upwards, or callable string for n>1 cells.""" + return FormattingOtherString(self.cuu1, ParameterizingString(self.cuu)) + + @property + def move_down(self): + """Move cursor 1 or more cells downwards, or callable string for n>1 cells.""" + return FormattingOtherString(self.cud1, ParameterizingString(self.cud)) + + @property + def color(self): + """ + A callable string that sets the foreground color. + + :rtype: ParameterizingString + + The capability is unparameterized until called and passed a number, at which point it + returns another string which represents a specific color change. This second string can + further be called to color a piece of text and set everything back to normal afterward. + + This should not be used directly, but rather a specific color by name or + :meth:`~.Terminal.color_rgb` value. + """ + if not self.does_styling: + return NullCallableString() + return ParameterizingString(self._foreground_color, + self.normal, 'color') + + def color_rgb(self, red, green, blue): + """ + Provides callable formatting string to set foreground color to the specified RGB color. + + :arg int red: RGB value of Red. + :arg int green: RGB value of Green. + :arg int blue: RGB value of Blue. + :rtype: FormattingString + :returns: Callable string that sets the foreground color + + If the terminal does not support RGB color, the nearest supported + color will be determined using :py:attr:`color_distance_algorithm`. + """ + if self.number_of_colors == 1 << 24: + # "truecolor" 24-bit + fmt_attr = u'\x1b[38;2;{0};{1};{2}m'.format(red, green, blue) + return FormattingString(fmt_attr, self.normal) + + # color by approximation to 256 or 16-color terminals + color_idx = self.rgb_downconvert(red, green, blue) + return FormattingString(self._foreground_color(color_idx), self.normal) + + @property + def on_color(self): + """ + A callable capability that sets the background color. + + :rtype: ParameterizingString + """ + if not self.does_styling: + return NullCallableString() + return ParameterizingString(self._background_color, + self.normal, 'on_color') + + def on_color_rgb(self, red, green, blue): + """ + Provides callable formatting string to set background color to the specified RGB color. + + :arg int red: RGB value of Red. + :arg int green: RGB value of Green. + :arg int blue: RGB value of Blue. + :rtype: FormattingString + :returns: Callable string that sets the foreground color + + If the terminal does not support RGB color, the nearest supported + color will be determined using :py:attr:`color_distance_algorithm`. + """ + if self.number_of_colors == 1 << 24: + fmt_attr = u'\x1b[48;2;{0};{1};{2}m'.format(red, green, blue) + return FormattingString(fmt_attr, self.normal) + + color_idx = self.rgb_downconvert(red, green, blue) + return FormattingString(self._background_color(color_idx), self.normal) + + def formatter(self, value): + """ + Provides callable formatting string to set color and other text formatting options. + + :arg str value: Sugary, ordinary, or compound formatted terminal capability, + such as "red_on_white", "normal", "red", or "bold_on_black". + :rtype: :class:`FormattingString` or :class:`NullCallableString` + :returns: Callable string that sets color and other text formatting options + + Calling ``term.formatter('bold_on_red')`` is equivalent to ``term.bold_on_red``, but a + string that is not a valid text formatter will return a :class:`NullCallableString`. + This is intended to allow validation of text formatters without the possibility of + inadvertently returning another terminal capability. + """ + formatters = split_compound(value) + if all((fmt in COLORS or fmt in COMPOUNDABLES) for fmt in formatters): + return getattr(self, value) + + return NullCallableString() + + def rgb_downconvert(self, red, green, blue): + """ + Translate an RGB color to a color code of the terminal's color depth. + + :arg int red: RGB value of Red (0-255). + :arg int green: RGB value of Green (0-255). + :arg int blue: RGB value of Blue (0-255). + :rtype: int + :returns: Color code of downconverted RGB color + """ + # Though pre-computing all 1 << 24 options is memory-intensive, a pre-computed + # "k-d tree" of 256 (x,y,z) vectors of a colorspace in 3 dimensions, such as a + # cone of HSV, or simply 255x255x255 RGB square, any given rgb value is just a + # nearest-neighbor search of 256 points, which k-d should be much faster by + # sub-dividing / culling search points, rather than our "search all 256 points + # always" approach. + fn_distance = COLOR_DISTANCE_ALGORITHMS[self.color_distance_algorithm] + color_idx = 7 + shortest_distance = None + for cmp_depth, cmp_rgb in enumerate(RGB_256TABLE): + cmp_distance = fn_distance(cmp_rgb, (red, green, blue)) + if shortest_distance is None or cmp_distance < shortest_distance: + shortest_distance = cmp_distance + color_idx = cmp_depth + if cmp_depth >= self.number_of_colors: + break + return color_idx + + @property + def normal(self): + """ + A capability that resets all video attributes. + + :rtype: str + + ``normal`` is an alias for ``sgr0`` or ``exit_attribute_mode``. Any + styling attributes previously applied, such as foreground or + background colors, reverse video, or bold are reset to defaults. + """ + if self._normal: + return self._normal + self._normal = resolve_capability(self, 'normal') + return self._normal + + def link(self, url, text, url_id=''): + """ + Display ``text`` that when touched or clicked, navigates to ``url``. + + Optional ``url_id`` may be specified, so that non-adjacent cells can reference a single + target, all cells painted with the same "id" will highlight on hover, rather than any + individual one, as described in "Hovering and underlining the id parameter" of gist + https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda. + + :param str url: Hyperlink URL. + :param str text: Clickable text. + :param str url_id: Optional 'id'. + :rtype: str + :returns: String of ``text`` as a hyperlink to ``url``. + """ + assert len(url) < 2000, (len(url), url) + if url_id: + assert len(str(url_id)) < 250, (len(str(url_id)), url_id) + params = 'id={0}'.format(url_id) + else: + params = '' + if not self.does_styling: + return text + return ('\x1b]8;{0};{1}\x1b\\{2}' + '\x1b]8;;\x1b\\'.format(params, url, text)) + + @property + def stream(self): + """ + Read-only property: stream the terminal outputs to. + + This is a convenience attribute. It is used internally for implied + writes performed by context managers :meth:`~.hidden_cursor`, + :meth:`~.fullscreen`, :meth:`~.location`, and :meth:`~.keypad`. + """ + return self._stream + + @property + def number_of_colors(self): + """ + Number of colors supported by terminal. + + Common return values are 0, 8, 16, 256, or 1 << 24. + + This may be used to test whether the terminal supports colors, + and at what depth, if that's a concern. + + If this property is assigned a value of 88, the value 16 will be saved. This is due to the + the rarity of 88 color support and the inconsistency of behavior between implementations. + + Assigning this property to a value other than 0, 4, 8, 16, 88, 256, or 1 << 24 will + raise an :py:exc:`AssertionError`. + """ + return self._number_of_colors + + @number_of_colors.setter + def number_of_colors(self, value): + assert value in (0, 4, 8, 16, 88, 256, 1 << 24) + # Because 88 colors is rare and we can't guarantee consistent behavior, + # when 88 colors is detected, it is treated as 16 colors + self._number_of_colors = 16 if value == 88 else value + self.__clear_color_capabilities() + + @property + def color_distance_algorithm(self): + """ + Color distance algorithm used by :meth:`rgb_downconvert`. + + The slowest, but most accurate, 'cie2000', is default. Other available options are 'rgb', + 'rgb-weighted', 'cie76', and 'cie94'. + """ + return self._color_distance_algorithm + + @color_distance_algorithm.setter + def color_distance_algorithm(self, value): + assert value in COLOR_DISTANCE_ALGORITHMS + self._color_distance_algorithm = value + self.__clear_color_capabilities() + + @property + def _foreground_color(self): + """ + Convenience capability to support :attr:`~.on_color`. + + Prefers returning sequence for capability ``setaf``, "Set foreground color to #1, using ANSI + escape". If the given terminal does not support such sequence, fallback to returning + attribute ``setf``, "Set foreground color #1". + """ + return self.setaf or self.setf + + @property + def _background_color(self): + """ + Convenience capability to support :attr:`~.on_color`. + + Prefers returning sequence for capability ``setab``, "Set background color to #1, using ANSI + escape". If the given terminal does not support such sequence, fallback to returning + attribute ``setb``, "Set background color #1". + """ + return self.setab or self.setb + + def ljust(self, text, width=None, fillchar=u' '): + """ + Left-align ``text``, which may contain terminal sequences. + + :arg str text: String to be aligned + :arg int width: Total width to fill with aligned text. If + unspecified, the whole width of the terminal is filled. + :arg str fillchar: String for padding the right of ``text`` + :rtype: str + :returns: String of ``text``, left-aligned by ``width``. + """ + # Left justification is different from left alignment, but we continue + # the vocabulary error of the str method for polymorphism. + if width is None: + width = self.width + return Sequence(text, self).ljust(width, fillchar) + + def rjust(self, text, width=None, fillchar=u' '): + """ + Right-align ``text``, which may contain terminal sequences. + + :arg str text: String to be aligned + :arg int width: Total width to fill with aligned text. If + unspecified, the whole width of the terminal is used. + :arg str fillchar: String for padding the left of ``text`` + :rtype: str + :returns: String of ``text``, right-aligned by ``width``. + """ + if width is None: + width = self.width + return Sequence(text, self).rjust(width, fillchar) + + def center(self, text, width=None, fillchar=u' '): + """ + Center ``text``, which may contain terminal sequences. + + :arg str text: String to be centered + :arg int width: Total width in which to center text. If + unspecified, the whole width of the terminal is used. + :arg str fillchar: String for padding the left and right of ``text`` + :rtype: str + :returns: String of ``text``, centered by ``width`` + """ + if width is None: + width = self.width + return Sequence(text, self).center(width, fillchar) + + def truncate(self, text, width=None): + r""" + Truncate ``text`` to maximum ``width`` printable characters, retaining terminal sequences. + + :arg str text: Text to truncate + :arg int width: The maximum width to truncate it to + :rtype: str + :returns: ``text`` truncated to at most ``width`` printable characters + + >>> term.truncate(u'xyz\x1b[0;3m', 2) + u'xy\x1b[0;3m' + """ + if width is None: + width = self.width + return Sequence(text, self).truncate(width) + + def length(self, text): + u""" + Return printable length of a string containing sequences. + + :arg str text: String to measure. May contain terminal sequences. + :rtype: int + :returns: The number of terminal character cells the string will occupy + when printed + + Wide characters that consume 2 character cells are supported: + + >>> term = Terminal() + >>> term.length(term.clear + term.red(u'コンニチハ')) + 10 + + .. note:: Sequences such as 'clear', which is considered as a + "movement sequence" because it would move the cursor to + (y, x)(0, 0), are evaluated as a printable length of + *0*. + """ + return Sequence(text, self).length() + + def strip(self, text, chars=None): + r""" + Return ``text`` without sequences and leading or trailing whitespace. + + :rtype: str + :returns: Text with leading and trailing whitespace removed + + >>> term.strip(u' \x1b[0;3m xyz ') + u'xyz' + """ + return Sequence(text, self).strip(chars) + + def rstrip(self, text, chars=None): + r""" + Return ``text`` without terminal sequences or trailing whitespace. + + :rtype: str + :returns: Text with terminal sequences and trailing whitespace removed + + >>> term.rstrip(u' \x1b[0;3m xyz ') + u' xyz' + """ + return Sequence(text, self).rstrip(chars) + + def lstrip(self, text, chars=None): + r""" + Return ``text`` without terminal sequences or leading whitespace. + + :rtype: str + :returns: Text with terminal sequences and leading whitespace removed + + >>> term.lstrip(u' \x1b[0;3m xyz ') + u'xyz ' + """ + return Sequence(text, self).lstrip(chars) + + def strip_seqs(self, text): + r""" + Return ``text`` stripped of only its terminal sequences. + + :rtype: str + :returns: Text with terminal sequences removed + + >>> term.strip_seqs(u'\x1b[0;3mxyz') + u'xyz' + >>> term.strip_seqs(term.cuf(5) + term.red(u'test')) + u' test' + + .. note:: Non-destructive sequences that adjust horizontal distance + (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive + space or erasing. + """ + return Sequence(text, self).strip_seqs() + + def split_seqs(self, text, maxsplit=0): + r""" + Return ``text`` split by individual character elements and sequences. + + :arg str text: String containing sequences + :arg int maxsplit: When maxsplit is nonzero, at most maxsplit splits + occur, and the remainder of the string is returned as the final element + of the list (same meaning is argument for :func:`re.split`). + :rtype: list[str] + :returns: List of sequences and individual characters + + >>> term.split_seqs(term.underline(u'xyz')) + ['\x1b[4m', 'x', 'y', 'z', '\x1b(B', '\x1b[m'] + + >>> term.split_seqs(term.underline(u'xyz'), 1) + ['\x1b[4m', r'xyz\x1b(B\x1b[m'] + """ + pattern = self._caps_unnamed_any + result = [] + for idx, match in enumerate(re.finditer(pattern, text)): + result.append(match.group()) + if maxsplit and idx == maxsplit: + remaining = text[match.end():] + if remaining: + result[-1] += remaining + break + return result + + def wrap(self, text, width=None, **kwargs): + r""" + Text-wrap a string, returning a list of wrapped lines. + + :arg str text: Unlike :func:`textwrap.wrap`, ``text`` may contain + terminal sequences, such as colors, bold, or underline. By + default, tabs in ``text`` are expanded by + :func:`string.expandtabs`. + :arg int width: Unlike :func:`textwrap.wrap`, ``width`` will + default to the width of the attached terminal. + :arg \**kwargs: See :py:class:`textwrap.TextWrapper` + :rtype: list + :returns: List of wrapped lines + + See :class:`textwrap.TextWrapper` for keyword arguments that can + customize wrapping behaviour. + """ + width = self.width if width is None else width + wrapper = SequenceTextWrapper(width=width, term=self, **kwargs) + lines = [] + for line in text.splitlines(): + lines.extend(iter(wrapper.wrap(line)) if line.strip() else (u'',)) + + return lines + + def getch(self): + """ + Read, decode, and return the next byte from the keyboard stream. + + :rtype: unicode + :returns: a single unicode character, or ``u''`` if a multi-byte + sequence has not yet been fully received. + + This method name and behavior mimics curses ``getch(void)``, and + it supports :meth:`inkey`, reading only one byte from + the keyboard string at a time. This method should always return + without blocking if called after :meth:`kbhit` has returned True. + + Implementors of alternate input stream methods should override + this method. + """ + assert self._keyboard_fd is not None + byte = os.read(self._keyboard_fd, 1) + return self._keyboard_decoder.decode(byte, final=False) + + def ungetch(self, text): + """ + Buffer input data to be discovered by next call to :meth:`~.inkey`. + + :arg str text: String to be buffered as keyboard input. + """ + self._keyboard_buf.extendleft(text) + + def kbhit(self, timeout=None): + """ + Return whether a keypress has been detected on the keyboard. + + This method is used by :meth:`inkey` to determine if a byte may + be read using :meth:`getch` without blocking. The standard + implementation simply uses the :func:`select.select` call on stdin. + + :arg float timeout: When ``timeout`` is 0, this call is + non-blocking, otherwise blocking indefinitely until keypress + is detected when None (default). When ``timeout`` is a + positive number, returns after ``timeout`` seconds have + elapsed (float). + :rtype: bool + :returns: True if a keypress is awaiting to be read on the keyboard + attached to this terminal. When input is not a terminal, False is + always returned. + """ + stime = time.time() + ready_r = [None, ] + check_r = [self._keyboard_fd] if self._keyboard_fd is not None else [] + + while HAS_TTY: + try: + ready_r, _, _ = select.select(check_r, [], [], timeout) + except InterruptedError: + # Beginning with python3.5, IntrruptError is no longer thrown + # https://www.python.org/dev/peps/pep-0475/ + # + # For previous versions of python, we take special care to + # retry select on InterruptedError exception, namely to handle + # a custom SIGWINCH handler. When installed, it would cause + # select() to be interrupted with errno 4 (EAGAIN). + # + # Just as in python3.5, it is ignored, and a new timeout value + # is derived from the previous unless timeout becomes negative. + # because the signal handler has blocked beyond timeout, then + # False is returned. Otherwise, when timeout is None, we + # continue to block indefinitely (default). + if timeout is not None: + # subtract time already elapsed, + timeout -= time.time() - stime + if timeout > 0: + continue + # no time remains after handling exception (rare) + ready_r = [] # pragma: no cover + break # pragma: no cover + else: + break + + return False if self._keyboard_fd is None else check_r == ready_r + + @contextlib.contextmanager + def cbreak(self): + """ + Allow each keystroke to be read immediately after it is pressed. + + This is a context manager for :func:`tty.setcbreak`. + + This context manager activates 'rare' mode, the opposite of 'cooked' + mode: On entry, :func:`tty.setcbreak` mode is activated disabling + line-buffering of keyboard input and turning off automatic echo of + input as output. + + .. note:: You must explicitly print any user input you would like + displayed. If you provide any kind of editing, you must handle + backspace and other line-editing control functions in this mode + as well! + + **Normally**, characters received from the keyboard cannot be read + by Python until the *Return* key is pressed. Also known as *cooked* or + *canonical input* mode, it allows the tty driver to provide + line-editing before shuttling the input to your program and is the + (implicit) default terminal mode set by most unix shells before + executing programs. + + Technically, this context manager sets the :mod:`termios` attributes + of the terminal attached to :obj:`sys.__stdin__`. + + .. note:: :func:`tty.setcbreak` sets ``VMIN = 1`` and ``VTIME = 0``, + see http://www.unixwiz.net/techtips/termios-vmin-vtime.html + """ + if HAS_TTY and self._keyboard_fd is not None: + # Save current terminal mode: + save_mode = termios.tcgetattr(self._keyboard_fd) + save_line_buffered = self._line_buffered + tty.setcbreak(self._keyboard_fd, termios.TCSANOW) + try: + self._line_buffered = False + yield + finally: + # Restore prior mode: + termios.tcsetattr(self._keyboard_fd, + termios.TCSAFLUSH, + save_mode) + self._line_buffered = save_line_buffered + else: + yield + + @contextlib.contextmanager + def raw(self): + r""" + A context manager for :func:`tty.setraw`. + + Although both :meth:`break` and :meth:`raw` modes allow each keystroke + to be read immediately after it is pressed, Raw mode disables + processing of input and output. + + In cbreak mode, special input characters such as ``^C`` or ``^S`` are + interpreted by the terminal driver and excluded from the stdin stream. + In raw mode these values are receive by the :meth:`inkey` method. + + Because output processing is not done, the newline ``'\n'`` is not + enough, you must also print carriage return to ensure that the cursor + is returned to the first column:: + + with term.raw(): + print("printing in raw mode", end="\r\n") + """ + if HAS_TTY and self._keyboard_fd is not None: + # Save current terminal mode: + save_mode = termios.tcgetattr(self._keyboard_fd) + save_line_buffered = self._line_buffered + tty.setraw(self._keyboard_fd, termios.TCSANOW) + try: + self._line_buffered = False + yield + finally: + # Restore prior mode: + termios.tcsetattr(self._keyboard_fd, + termios.TCSAFLUSH, + save_mode) + self._line_buffered = save_line_buffered + else: + yield + + @contextlib.contextmanager + def keypad(self): + r""" + Context manager that enables directional keypad input. + + On entrying, this puts the terminal into "keyboard_transmit" mode by + emitting the keypad_xmit (smkx) capability. On exit, it emits + keypad_local (rmkx). + + On an IBM-PC keyboard with numeric keypad of terminal-type *xterm*, + with numlock off, the lower-left diagonal key transmits sequence + ``\\x1b[F``, translated to :class:`~.Terminal` attribute + ``KEY_END``. + + However, upon entering :meth:`keypad`, ``\\x1b[OF`` is transmitted, + translating to ``KEY_LL`` (lower-left key), allowing you to determine + diagonal direction keys. + """ + try: + self.stream.write(self.smkx) + self.stream.flush() + yield + finally: + self.stream.write(self.rmkx) + self.stream.flush() + + def inkey(self, timeout=None, esc_delay=0.35): + """ + Read and return the next keyboard event within given timeout. + + Generally, this should be used inside the :meth:`raw` context manager. + + :arg float timeout: Number of seconds to wait for a keystroke before + returning. When ``None`` (default), this method may block + indefinitely. + :arg float esc_delay: To distinguish between the keystroke of + ``KEY_ESCAPE``, and sequences beginning with escape, the parameter + ``esc_delay`` specifies the amount of time after receiving escape + (``chr(27)``) to seek for the completion of an application key + before returning a :class:`~.Keystroke` instance for + ``KEY_ESCAPE``. + :rtype: :class:`~.Keystroke`. + :returns: :class:`~.Keystroke`, which may be empty (``u''``) if + ``timeout`` is specified and keystroke is not received. + + .. note:: When used without the context manager :meth:`cbreak`, or + :meth:`raw`, :obj:`sys.__stdin__` remains line-buffered, and this + function will block until the return key is pressed! + + .. note:: On Windows, a 10 ms sleep is added to the key press detection loop to reduce CPU + load. Due to the behavior of :py:func:`time.sleep` on Windows, this will actually + result in a 15.6 ms delay when using the default `time resolution + <https://docs.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod>`_. + Decreasing the time resolution will reduce this to 10 ms, while increasing it, which + is rarely done, will have a perceptable impact on the behavior. + """ + resolve = functools.partial(resolve_sequence, + mapper=self._keymap, + codes=self._keycodes) + + stime = time.time() + + # re-buffer previously received keystrokes, + ucs = u'' + while self._keyboard_buf: + ucs += self._keyboard_buf.pop() + + # receive all immediately available bytes + while self.kbhit(timeout=0): + ucs += self.getch() + + # decode keystroke, if any + ks = resolve(text=ucs) + + # so long as the most immediately received or buffered keystroke is + # incomplete, (which may be a multibyte encoding), block until until + # one is received. + while not ks and self.kbhit(timeout=_time_left(stime, timeout)): + ucs += self.getch() + ks = resolve(text=ucs) + + # handle escape key (KEY_ESCAPE) vs. escape sequence (like those + # that begin with \x1b[ or \x1bO) up to esc_delay when + # received. This is not optimal, but causes least delay when + # "meta sends escape" is used, or when an unsupported sequence is + # sent. + # + # The statement, "ucs in self._keymap_prefixes" has an effect on + # keystrokes such as Alt + Z ("\x1b[z" with metaSendsEscape): because + # no known input sequences begin with such phrasing to allow it to be + # returned more quickly than esc_delay otherwise blocks for. + if ks.code == self.KEY_ESCAPE: + esctime = time.time() + while (ks.code == self.KEY_ESCAPE and + ucs in self._keymap_prefixes and + self.kbhit(timeout=_time_left(esctime, esc_delay))): + ucs += self.getch() + ks = resolve(text=ucs) + + # buffer any remaining text received + self.ungetch(ucs[len(ks):]) + return ks + + +class WINSZ(collections.namedtuple('WINSZ', ( + 'ws_row', 'ws_col', 'ws_xpixel', 'ws_ypixel'))): + """ + Structure represents return value of :const:`termios.TIOCGWINSZ`. + + .. py:attribute:: ws_row + + rows, in characters + + .. py:attribute:: ws_col + + columns, in characters + + .. py:attribute:: ws_xpixel + + horizontal size, pixels + + .. py:attribute:: ws_ypixel + + vertical size, pixels + """ + #: format of termios structure + _FMT = 'hhhh' + #: buffer of termios structure appropriate for ioctl argument + _BUF = '\x00' * struct.calcsize(_FMT) + + +#: _CUR_TERM = None +#: From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al):: +#: +#: "After the call to setupterm(), the global variable cur_term is set to +#: point to the current structure of terminal capabilities. By calling +#: setupterm() for each terminal, and saving and restoring cur_term, it +#: is possible for a program to use two or more terminals at once." +#: +#: However, if you study Python's ``./Modules/_cursesmodule.c``, you'll find:: +#: +#: if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) { +#: +#: Python - perhaps wrongly - will not allow for re-initialisation of new +#: terminals through :func:`curses.setupterm`, so the value of cur_term cannot +#: be changed once set: subsequent calls to :func:`curses.setupterm` have no +#: effect. +#: +#: Therefore, the :attr:`Terminal.kind` of each :class:`Terminal` is +#: essentially a singleton. This global variable reflects that, and a warning +#: is emitted if somebody expects otherwise. diff --git a/third_party/python/blessed/blessed/terminal.pyi b/third_party/python/blessed/blessed/terminal.pyi new file mode 100644 index 0000000000..3d8eea4db7 --- /dev/null +++ b/third_party/python/blessed/blessed/terminal.pyi @@ -0,0 +1,106 @@ +# std imports +from typing import IO, Any, List, Tuple, Union, Optional, OrderedDict, ContextManager + +# local +from .keyboard import Keystroke +from .sequences import Termcap +from .formatters import (FormattingString, + NullCallableString, + ParameterizingString, + FormattingOtherString) + +HAS_TTY: bool + +class Terminal: + caps: OrderedDict[str, Termcap] + errors: List[str] = ... + def __init__( + self, + kind: Optional[str] = ..., + stream: Optional[IO[str]] = ..., + force_styling: bool = ..., + ) -> None: ... + def __getattr__( + self, attr: str + ) -> Union[NullCallableString, ParameterizingString, FormattingString]: ... + @property + def kind(self) -> str: ... + @property + def does_styling(self) -> bool: ... + @property + def is_a_tty(self) -> bool: ... + @property + def height(self) -> int: ... + @property + def width(self) -> int: ... + @property + def pixel_height(self) -> int: ... + @property + def pixel_width(self) -> int: ... + def location( + self, x: Optional[int] = ..., y: Optional[int] = ... + ) -> ContextManager[None]: ... + def get_location(self, timeout: Optional[float] = ...) -> Tuple[int, int]: ... + def fullscreen(self) -> ContextManager[None]: ... + def hidden_cursor(self) -> ContextManager[None]: ... + def move_xy(self, x: int, y: int) -> ParameterizingString: ... + def move_yx(self, y: int, x: int) -> ParameterizingString: ... + @property + def move_left(self) -> FormattingOtherString: ... + @property + def move_right(self) -> FormattingOtherString: ... + @property + def move_up(self) -> FormattingOtherString: ... + @property + def move_down(self) -> FormattingOtherString: ... + @property + def color(self) -> Union[NullCallableString, ParameterizingString]: ... + def color_rgb(self, red: int, green: int, blue: int) -> FormattingString: ... + @property + def on_color(self) -> Union[NullCallableString, ParameterizingString]: ... + def on_color_rgb(self, red: int, green: int, blue: int) -> FormattingString: ... + def formatter(self, value: str) -> Union[NullCallableString, FormattingString]: ... + def rgb_downconvert(self, red: int, green: int, blue: int) -> int: ... + @property + def normal(self) -> str: ... + def link(self, url: str, text: str, url_id: str = ...) -> str: ... + @property + def stream(self) -> IO[str]: ... + @property + def number_of_colors(self) -> int: ... + @number_of_colors.setter + def number_of_colors(self, value: int) -> None: ... + @property + def color_distance_algorithm(self) -> str: ... + @color_distance_algorithm.setter + def color_distance_algorithm(self, value: str) -> None: ... + def ljust( + self, text: str, width: Optional[int] = ..., fillchar: str = ... + ) -> str: ... + def rjust( + self, text: str, width: Optional[int] = ..., fillchar: str = ... + ) -> str: ... + def center( + self, text: str, width: Optional[int] = ..., fillchar: str = ... + ) -> str: ... + def truncate(self, text: str, width: Optional[int] = ...) -> str: ... + def length(self, text: str) -> int: ... + def strip(self, text: str, chars: Optional[str] = ...) -> str: ... + def rstrip(self, text: str, chars: Optional[str] = ...) -> str: ... + def lstrip(self, text: str, chars: Optional[str] = ...) -> str: ... + def strip_seqs(self, text: str) -> str: ... + def split_seqs(self, text: str, maxsplit: int) -> List[str]: ... + def wrap( + self, text: str, width: Optional[int] = ..., **kwargs: Any + ) -> List[str]: ... + def getch(self) -> str: ... + def ungetch(self, text: str) -> None: ... + def kbhit(self, timeout: Optional[float] = ...) -> bool: ... + def cbreak(self) -> ContextManager[None]: ... + def raw(self) -> ContextManager[None]: ... + def keypad(self) -> ContextManager[None]: ... + def inkey( + self, timeout: Optional[float] = ..., esc_delay: float = ... + ) -> Keystroke: ... + +class WINSZ: ... diff --git a/third_party/python/blessed/blessed/win_terminal.py b/third_party/python/blessed/blessed/win_terminal.py new file mode 100644 index 0000000000..267e028b96 --- /dev/null +++ b/third_party/python/blessed/blessed/win_terminal.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +"""Module containing Windows version of :class:`Terminal`.""" + +from __future__ import absolute_import + +# std imports +import time +import msvcrt # pylint: disable=import-error +import contextlib + +# 3rd party +from jinxed import win32 # pylint: disable=import-error + +# local +from .terminal import WINSZ +from .terminal import Terminal as _Terminal + + +class Terminal(_Terminal): + """Windows subclass of :class:`Terminal`.""" + + def getch(self): + r""" + Read, decode, and return the next byte from the keyboard stream. + + :rtype: unicode + :returns: a single unicode character, or ``u''`` if a multi-byte + sequence has not yet been fully received. + + For versions of Windows 10.0.10586 and later, the console is expected + to be in ENABLE_VIRTUAL_TERMINAL_INPUT mode and the default method is + called. + + For older versions of Windows, msvcrt.getwch() is used. If the received + character is ``\x00`` or ``\xe0``, the next character is + automatically retrieved. + """ + if win32.VTMODE_SUPPORTED: + return super(Terminal, self).getch() + + rtn = msvcrt.getwch() + if rtn in ('\x00', '\xe0'): + rtn += msvcrt.getwch() + return rtn + + def kbhit(self, timeout=None): + """ + Return whether a keypress has been detected on the keyboard. + + This method is used by :meth:`inkey` to determine if a byte may + be read using :meth:`getch` without blocking. This is implemented + by wrapping msvcrt.kbhit() in a timeout. + + :arg float timeout: When ``timeout`` is 0, this call is + non-blocking, otherwise blocking indefinitely until keypress + is detected when None (default). When ``timeout`` is a + positive number, returns after ``timeout`` seconds have + elapsed (float). + :rtype: bool + :returns: True if a keypress is awaiting to be read on the keyboard + attached to this terminal. + """ + end = time.time() + (timeout or 0) + while True: + + if msvcrt.kbhit(): + return True + + if timeout is not None and end < time.time(): + break + + time.sleep(0.01) # Sleep to reduce CPU load + return False + + @staticmethod + def _winsize(fd): + """ + Return named tuple describing size of the terminal by ``fd``. + + :arg int fd: file descriptor queries for its window size. + :rtype: WINSZ + :returns: named tuple describing size of the terminal + + WINSZ is a :class:`collections.namedtuple` instance, whose structure + directly maps to the return value of the :const:`termios.TIOCGWINSZ` + ioctl return value. The return parameters are: + + - ``ws_row``: width of terminal by its number of character cells. + - ``ws_col``: height of terminal by its number of character cells. + - ``ws_xpixel``: width of terminal by pixels (not accurate). + - ``ws_ypixel``: height of terminal by pixels (not accurate). + """ + window = win32.get_terminal_size(fd) + return WINSZ(ws_row=window.lines, ws_col=window.columns, + ws_xpixel=0, ws_ypixel=0) + + @contextlib.contextmanager + def cbreak(self): + """ + Allow each keystroke to be read immediately after it is pressed. + + This is a context manager for ``jinxed.w32.setcbreak()``. + + .. note:: You must explicitly print any user input you would like + displayed. If you provide any kind of editing, you must handle + backspace and other line-editing control functions in this mode + as well! + + **Normally**, characters received from the keyboard cannot be read + by Python until the *Return* key is pressed. Also known as *cooked* or + *canonical input* mode, it allows the tty driver to provide + line-editing before shuttling the input to your program and is the + (implicit) default terminal mode set by most unix shells before + executing programs. + """ + if self._keyboard_fd is not None: + + filehandle = msvcrt.get_osfhandle(self._keyboard_fd) + + # Save current terminal mode: + save_mode = win32.get_console_mode(filehandle) + save_line_buffered = self._line_buffered + win32.setcbreak(filehandle) + try: + self._line_buffered = False + yield + finally: + win32.set_console_mode(filehandle, save_mode) + self._line_buffered = save_line_buffered + + else: + yield + + @contextlib.contextmanager + def raw(self): + """ + A context manager for ``jinxed.w32.setcbreak()``. + + Although both :meth:`break` and :meth:`raw` modes allow each keystroke + to be read immediately after it is pressed, Raw mode disables + processing of input and output. + + In cbreak mode, special input characters such as ``^C`` are + interpreted by the terminal driver and excluded from the stdin stream. + In raw mode these values are receive by the :meth:`inkey` method. + """ + if self._keyboard_fd is not None: + + filehandle = msvcrt.get_osfhandle(self._keyboard_fd) + + # Save current terminal mode: + save_mode = win32.get_console_mode(filehandle) + save_line_buffered = self._line_buffered + win32.setraw(filehandle) + try: + self._line_buffered = False + yield + finally: + win32.set_console_mode(filehandle, save_mode) + self._line_buffered = save_line_buffered + + else: + yield diff --git a/third_party/python/blessed/blessed/win_terminal.pyi b/third_party/python/blessed/blessed/win_terminal.pyi new file mode 100644 index 0000000000..275f16f9ee --- /dev/null +++ b/third_party/python/blessed/blessed/win_terminal.pyi @@ -0,0 +1,11 @@ +# std imports +from typing import Optional, ContextManager + +# local +from .terminal import Terminal as _Terminal + +class Terminal(_Terminal): + def getch(self) -> str: ... + def kbhit(self, timeout: Optional[float] = ...) -> bool: ... + def cbreak(self) -> ContextManager[None]: ... + def raw(self) -> ContextManager[None]: ... |