generated from mwc/lab_retro
When I run the game, it works for a bit but then when the first asteroid
gets to the bottom I get this:
Traceback (most recent call last):
File "/root/making_with_code/mwc1/unit3/lab_retro/nav_game.py", line 14, in <module>
game.play()
File "/root/making_with_code/mwc1/unit3/lab_retro/retro/game.py", line 80, in play
agent.play_turn(self)
File "/root/making_with_code/mwc1/unit3/lab_retro/asteroid.py", line 16, in play_turn
game.remove_agent_by_name(self.name)
AttributeError: 'Asteroid' object has no attribute 'name'
This commit is contained in:
BIN
__pycache__/asteroid.cpython-310.pyc
Normal file
BIN
__pycache__/asteroid.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/asteroid_spawner.cpython-310.pyc
Normal file
BIN
__pycache__/asteroid_spawner.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/retro.cpython-310.pyc
Normal file
BIN
__pycache__/retro.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/spaceship.cpython-310.pyc
Normal file
BIN
__pycache__/spaceship.cpython-310.pyc
Normal file
Binary file not shown.
19
asteroid.py
19
asteroid.py
@@ -2,3 +2,22 @@
|
|||||||
# ------------
|
# ------------
|
||||||
# By MWC Contributors
|
# By MWC Contributors
|
||||||
# This module defines an asteroid agent class.
|
# This module defines an asteroid agent class.
|
||||||
|
|
||||||
|
class Asteroid:
|
||||||
|
character = 'O'
|
||||||
|
|
||||||
|
def __init__(self, position):
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
if game.turn_number % 2 == 0:
|
||||||
|
x, y = self.position
|
||||||
|
if y == 25 - 1:
|
||||||
|
game.remove_agent_by_name(self.name)
|
||||||
|
else:
|
||||||
|
ship = game.get_agent_by_name('ship')
|
||||||
|
new_position = (x, y + 1)
|
||||||
|
if new_position == ship.position:
|
||||||
|
game.end()
|
||||||
|
else:
|
||||||
|
self.position = new_position
|
||||||
@@ -2,3 +2,22 @@
|
|||||||
# -------------------
|
# -------------------
|
||||||
# By MWC Contributors
|
# By MWC Contributors
|
||||||
# This module defines an AsteroidSpawner agent class.
|
# This module defines an AsteroidSpawner agent class.
|
||||||
|
|
||||||
|
from random import randint
|
||||||
|
from asteroid import Asteroid
|
||||||
|
|
||||||
|
class AsteroidSpawner:
|
||||||
|
display = False
|
||||||
|
|
||||||
|
def __init__(self, board_size):
|
||||||
|
width, height = board_size
|
||||||
|
self.board_width = width
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
game.state['score'] += 1
|
||||||
|
if self.should_spawn_asteroid(game.turn_number):
|
||||||
|
asteroid = Asteroid((randint(0, self.board_width - 1), 0))
|
||||||
|
game.add_agent(asteroid)
|
||||||
|
|
||||||
|
def should_spawn_asteroid(self, turn_number):
|
||||||
|
return randint(0, 1000) < turn_number
|
||||||
23
blessed/__init__.py
Normal file
23
blessed/__init__.py
Normal file
@@ -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.20.0"
|
||||||
0
blessed/__init__.py:Zone.Identifier
Normal file
0
blessed/__init__.py:Zone.Identifier
Normal file
BIN
blessed/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
blessed/__pycache__/_capabilities.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/_capabilities.cpython-310.pyc
Normal file
Binary file not shown.
BIN
blessed/__pycache__/color.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/color.cpython-310.pyc
Normal file
Binary file not shown.
BIN
blessed/__pycache__/colorspace.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/colorspace.cpython-310.pyc
Normal file
Binary file not shown.
BIN
blessed/__pycache__/formatters.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/formatters.cpython-310.pyc
Normal file
Binary file not shown.
BIN
blessed/__pycache__/keyboard.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/keyboard.cpython-310.pyc
Normal file
Binary file not shown.
BIN
blessed/__pycache__/sequences.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/sequences.cpython-310.pyc
Normal file
Binary file not shown.
BIN
blessed/__pycache__/terminal.cpython-310.pyc
Normal file
BIN
blessed/__pycache__/terminal.cpython-310.pyc
Normal file
Binary file not shown.
168
blessed/_capabilities.py
Normal file
168
blessed/_capabilities.py
Normal file
@@ -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',
|
||||||
|
)
|
||||||
0
blessed/_capabilities.py:Zone.Identifier
Normal file
0
blessed/_capabilities.py:Zone.Identifier
Normal file
7
blessed/_capabilities.pyi
Normal file
7
blessed/_capabilities.pyi
Normal file
@@ -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, ...]
|
||||||
0
blessed/_capabilities.pyi:Zone.Identifier
Normal file
0
blessed/_capabilities.pyi:Zone.Identifier
Normal file
258
blessed/color.py
Normal file
258
blessed/color.py
Normal file
@@ -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}
|
||||||
0
blessed/color.py:Zone.Identifier
Normal file
0
blessed/color.py:Zone.Identifier
Normal file
17
blessed/color.pyi
Normal file
17
blessed/color.pyi
Normal file
@@ -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]]
|
||||||
0
blessed/color.pyi:Zone.Identifier
Normal file
0
blessed/color.pyi:Zone.Identifier
Normal file
973
blessed/colorspace.py
Normal file
973
blessed/colorspace.py
Normal file
@@ -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 = {'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),
|
||||||
|
)
|
||||||
0
blessed/colorspace.py:Zone.Identifier
Normal file
0
blessed/colorspace.py:Zone.Identifier
Normal file
12
blessed/colorspace.pyi
Normal file
12
blessed/colorspace.pyi
Normal file
@@ -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, ...]
|
||||||
0
blessed/colorspace.pyi:Zone.Identifier
Normal file
0
blessed/colorspace.pyi:Zone.Identifier
Normal file
496
blessed/formatters.py
Normal file
496
blessed/formatters.py
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
"""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``."""
|
||||||
|
return self._callable(*args) if args else 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)
|
||||||
0
blessed/formatters.py:Zone.Identifier
Normal file
0
blessed/formatters.py:Zone.Identifier
Normal file
70
blessed/formatters.pyi
Normal file
70
blessed/formatters.pyi
Normal file
@@ -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]: ...
|
||||||
0
blessed/formatters.pyi:Zone.Identifier
Normal file
0
blessed/formatters.pyi:Zone.Identifier
Normal file
451
blessed/keyboard.py
Normal file
451
blessed/keyboard.py
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
"""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().copy().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',)
|
||||||
0
blessed/keyboard.py:Zone.Identifier
Normal file
0
blessed/keyboard.py:Zone.Identifier
Normal file
28
blessed/keyboard.pyi
Normal file
28
blessed/keyboard.pyi
Normal file
@@ -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: ...
|
||||||
0
blessed/keyboard.pyi:Zone.Identifier
Normal file
0
blessed/keyboard.pyi:Zone.Identifier
Normal file
0
blessed/py.typed
Normal file
0
blessed/py.typed
Normal file
0
blessed/py.typed:Zone.Identifier
Normal file
0
blessed/py.typed:Zone.Identifier
Normal file
461
blessed/sequences.py
Normal file
461
blessed/sequences.py
Normal file
@@ -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)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
unit = {
|
||||||
|
'parm_left_cursor': -1,
|
||||||
|
'parm_right_cursor': 1
|
||||||
|
}.get(self.name)
|
||||||
|
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
|
||||||
0
blessed/sequences.py:Zone.Identifier
Normal file
0
blessed/sequences.py:Zone.Identifier
Normal file
55
blessed/sequences.pyi
Normal file
55
blessed/sequences.pyi
Normal file
@@ -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: ...
|
||||||
0
blessed/sequences.pyi:Zone.Identifier
Normal file
0
blessed/sequences.pyi:Zone.Identifier
Normal file
1552
blessed/terminal.py
Normal file
1552
blessed/terminal.py
Normal file
File diff suppressed because it is too large
Load Diff
0
blessed/terminal.py:Zone.Identifier
Normal file
0
blessed/terminal.py:Zone.Identifier
Normal file
108
blessed/terminal.pyi
Normal file
108
blessed/terminal.pyi
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# std imports
|
||||||
|
from typing import IO, Any, List, Tuple, Union, Optional, OrderedDict, SupportsIndex, 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 get_fgcolor(self, timeout: Optional[float] = ...) -> Tuple[int, int, int]: ...
|
||||||
|
def get_bgcolor(self, timeout: Optional[float] = ...) -> Tuple[int, 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[SupportsIndex] = ..., fillchar: str = ...
|
||||||
|
) -> str: ...
|
||||||
|
def rjust(
|
||||||
|
self, text: str, width: Optional[SupportsIndex] = ..., fillchar: str = ...
|
||||||
|
) -> str: ...
|
||||||
|
def center(
|
||||||
|
self, text: str, width: Optional[SupportsIndex] = ..., fillchar: str = ...
|
||||||
|
) -> str: ...
|
||||||
|
def truncate(self, text: str, width: Optional[SupportsIndex] = ...) -> 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: ...
|
||||||
0
blessed/terminal.pyi:Zone.Identifier
Normal file
0
blessed/terminal.pyi:Zone.Identifier
Normal file
163
blessed/win_terminal.py
Normal file
163
blessed/win_terminal.py
Normal file
@@ -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
|
||||||
0
blessed/win_terminal.py:Zone.Identifier
Normal file
0
blessed/win_terminal.py:Zone.Identifier
Normal file
11
blessed/win_terminal.pyi
Normal file
11
blessed/win_terminal.pyi
Normal file
@@ -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]: ...
|
||||||
0
blessed/win_terminal.pyi:Zone.Identifier
Normal file
0
blessed/win_terminal.pyi:Zone.Identifier
Normal file
7
example_game.py
Normal file
7
example_game.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from retro.game import Game
|
||||||
|
from retro.agent import ArrowKeyAgent
|
||||||
|
|
||||||
|
agent = ArrowKeyAgent()
|
||||||
|
state = {}
|
||||||
|
game = Game([agent], state)
|
||||||
|
game.play()
|
||||||
10
nav_game.py
10
nav_game.py
@@ -2,3 +2,13 @@
|
|||||||
# ------------
|
# ------------
|
||||||
# By MWC Contributors
|
# By MWC Contributors
|
||||||
# This class implements a simple game where a spaceship avoids asteroids.
|
# This class implements a simple game where a spaceship avoids asteroids.
|
||||||
|
|
||||||
|
from retro.game import Game
|
||||||
|
from spaceship import Spaceship
|
||||||
|
from asteroid_spawner import AsteroidSpawner
|
||||||
|
|
||||||
|
board_size = (25, 25)
|
||||||
|
ship = Spaceship(board_size)
|
||||||
|
spawner = AsteroidSpawner(board_size)
|
||||||
|
game = Game([ship, spawner], {"score": 0}, board_size=board_size)
|
||||||
|
game.play()
|
||||||
BIN
retro/__pycache__/agent.cpython-310.pyc
Normal file
BIN
retro/__pycache__/agent.cpython-310.pyc
Normal file
Binary file not shown.
BIN
retro/__pycache__/errors.cpython-310.pyc
Normal file
BIN
retro/__pycache__/errors.cpython-310.pyc
Normal file
Binary file not shown.
BIN
retro/__pycache__/game.cpython-310.pyc
Normal file
BIN
retro/__pycache__/game.cpython-310.pyc
Normal file
Binary file not shown.
BIN
retro/__pycache__/graph.cpython-310.pyc
Normal file
BIN
retro/__pycache__/graph.cpython-310.pyc
Normal file
Binary file not shown.
BIN
retro/__pycache__/validation.cpython-310.pyc
Normal file
BIN
retro/__pycache__/validation.cpython-310.pyc
Normal file
Binary file not shown.
BIN
retro/__pycache__/view.cpython-310.pyc
Normal file
BIN
retro/__pycache__/view.cpython-310.pyc
Normal file
Binary file not shown.
99
retro/agent.py
Normal file
99
retro/agent.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
class Agent:
|
||||||
|
"""Represents a character in the game. To create an Agent, define a new
|
||||||
|
class with some of the attributes and methods below. You may change any of
|
||||||
|
the Agent's attributes at any time, and the result will immediately be
|
||||||
|
visible in the game.
|
||||||
|
|
||||||
|
After you create your Agents, add them to the ``Game``, either when it is created
|
||||||
|
or using ``Game.add_agent`` later on. Then the Game will take care of calling
|
||||||
|
the Agent's methods at the appropriate times.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
position: (Required) The character's ``(int, int)`` position on the game
|
||||||
|
board.
|
||||||
|
character: (Required unless display is ``False``.) A one-character string
|
||||||
|
which will be displayed at the Agent's position on the game board.
|
||||||
|
name: (Optional) If an agent has a name, it must be unique within the game.
|
||||||
|
Agent names can be used to look up agents with
|
||||||
|
:py:meth:`retro.game.Game.get_agent_by_name`.
|
||||||
|
color (str): (Optional) The agent's color.
|
||||||
|
`Available colors <https://blessed.readthedocs.io/en/latest/colors.html>`_.
|
||||||
|
display: (Optional) When ``False``, the Agent will not be displayed on the
|
||||||
|
board. This is useful when you want to create an agent which will be displayed
|
||||||
|
later, or when you want to create an agent which acts on the Game indirectly,
|
||||||
|
for example by spawning other Agents. Defaults to True.
|
||||||
|
z: (Optional) When multiple Agents have the same position on the board, the
|
||||||
|
Agent with the highest ``z`` value will be displayed.
|
||||||
|
The Game is played on a two-dimensional (x, y) board, but you can think of
|
||||||
|
``z`` as a third "up" dimension. Defaults to 0.
|
||||||
|
"""
|
||||||
|
character = "*"
|
||||||
|
position = (0, 0)
|
||||||
|
name = "agent"
|
||||||
|
color = "white_on_black"
|
||||||
|
display = True
|
||||||
|
z = 0
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""If an Agent has this method, it will be called once
|
||||||
|
each turn.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
game (Game): The game which is currently being played will be
|
||||||
|
passed to the Agent, in case it needs to check anything about
|
||||||
|
the game or make any changes.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""If an Agent has a this method, it will be called every
|
||||||
|
time a key is pressed in the game.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
keystroke (blessed.keyboard.Keystroke): The key which was pressed. You can
|
||||||
|
compare a Keystroke with a string (e.g. ``if keystroke == 'q'``) to check
|
||||||
|
whether it is a regular letter, number, or symbol on the keyboard. You can
|
||||||
|
check special keys using the keystroke's name
|
||||||
|
(e.g. ``if keystroke.name == "KEY_RIGHT"``). Run your game in debug mode to
|
||||||
|
see the names of keystrokes.
|
||||||
|
game (Game): The game which is currently being played will be
|
||||||
|
passed to the Agent, in case it needs to check anything about
|
||||||
|
the game or make any changes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ArrowKeyAgent:
|
||||||
|
"""A simple agent which can be moved around with the arrow keys.
|
||||||
|
"""
|
||||||
|
name = "ArrowKeyAgent"
|
||||||
|
character = "*"
|
||||||
|
position = (0,0)
|
||||||
|
display = True
|
||||||
|
z = 0
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""Moves the agent's position if the keystroke is one of the arrow keys.
|
||||||
|
One by one, checks the keystroke's name against each arrow key.
|
||||||
|
Then uses :py:meth:`try_to_move` to check whether the move is on the
|
||||||
|
game's board before moving.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name == "KEY_RIGHT":
|
||||||
|
self.try_to_move((x + 1, y), game)
|
||||||
|
elif keystroke.name == "KEY_UP":
|
||||||
|
self.try_to_move((x, y - 1), game)
|
||||||
|
elif keystroke.name == "KEY_LEFT":
|
||||||
|
self.try_to_move((x - 1, y), game)
|
||||||
|
elif keystroke.name == "KEY_DOWN":
|
||||||
|
self.try_to_move((x, y + 1), game)
|
||||||
|
|
||||||
|
def try_to_move(self, position, game):
|
||||||
|
"""Moves to the position if it is on the game board.
|
||||||
|
"""
|
||||||
|
if game.on_board(position):
|
||||||
|
self.position = position
|
||||||
|
game.log(f"Position: {self.position}")
|
||||||
0
retro/agent.py:Zone.Identifier
Normal file
0
retro/agent.py:Zone.Identifier
Normal file
40
retro/errors.py
Normal file
40
retro/errors.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
class GameError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AgentWithNameAlreadyExists(GameError):
|
||||||
|
def __init__(self, name):
|
||||||
|
message = f"There is already an agent named {agent.name} in the game"
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
class AgentNotFoundByName(GameError):
|
||||||
|
def __init__(self, name):
|
||||||
|
message = f"There is no agent named {agent.name} in the game"
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
class AgentNotInGame(GameError):
|
||||||
|
def __init__(self, agent):
|
||||||
|
name = agent.name or f"anonymous {agent.__class__.__name__}"
|
||||||
|
message = f"Agent {name} is not in the game"
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
class IllegalMove(GameError):
|
||||||
|
def __init__(self, agent, position):
|
||||||
|
message = f"Agent {agent.name} tried to move to {position}"
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
class GraphError(GameError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TerminalTooSmall(GameError):
|
||||||
|
BORDER_X = 2
|
||||||
|
BORDER_Y = 3
|
||||||
|
STATE_HEIGHT = 5
|
||||||
|
|
||||||
|
def __init__(self, width=None, width_needed=None, height=None, height_needed=None):
|
||||||
|
if width is not None and width_needed is not None and width_needed < width:
|
||||||
|
err = f"The terminal width ({width}) is less than the required {width_needed}."
|
||||||
|
super().__init__(err)
|
||||||
|
elif height is not None and height_needed is not None and height_needed < height:
|
||||||
|
err = f"The terminal height ({height}) is less than the required {height_needed}."
|
||||||
|
else:
|
||||||
|
raise ValueError(f"TerminalTooSmall called with illegal values.")
|
||||||
0
retro/errors.py:Zone.Identifier
Normal file
0
retro/errors.py:Zone.Identifier
Normal file
BIN
retro/examples/__pycache__/debug.cpython-310.pyc
Normal file
BIN
retro/examples/__pycache__/debug.cpython-310.pyc
Normal file
Binary file not shown.
6
retro/examples/debug.py
Normal file
6
retro/examples/debug.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from retro.game import Game
|
||||||
|
from retro.agent import ArrowKeyAgent
|
||||||
|
|
||||||
|
game = Game([ArrowKeyAgent()], {}, debug=True)
|
||||||
|
game.play()
|
||||||
|
|
||||||
0
retro/examples/debug.py:Zone.Identifier
Normal file
0
retro/examples/debug.py:Zone.Identifier
Normal file
139
retro/examples/nav.py
Normal file
139
retro/examples/nav.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
from random import randint
|
||||||
|
from retro.game import Game
|
||||||
|
|
||||||
|
HEIGHT = 25
|
||||||
|
WIDTH = 25
|
||||||
|
|
||||||
|
class Spaceship:
|
||||||
|
"""A player-controlled agent which moves left and right, dodging asteroids.
|
||||||
|
Spaceship is a pretty simple class. The ship's character is ``^``, and
|
||||||
|
its position starts at the bottom center of the screen.
|
||||||
|
"""
|
||||||
|
name = "ship"
|
||||||
|
character = '^'
|
||||||
|
position = (WIDTH // 2, HEIGHT - 1)
|
||||||
|
color = "black_on_skyblue1"
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""When the
|
||||||
|
left or arrow key is pressed, it moves left or right. If the ship's
|
||||||
|
new position is empty, it moves to that position. If the new position
|
||||||
|
is occupied (by an asteroid!) the game ends.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name in ("KEY_LEFT", "KEY_RIGHT"):
|
||||||
|
if keystroke.name == "KEY_LEFT":
|
||||||
|
new_position = (x - 1, y)
|
||||||
|
else:
|
||||||
|
new_position = (x + 1, y)
|
||||||
|
if game.on_board(new_position):
|
||||||
|
if game.is_empty(new_position):
|
||||||
|
self.position = new_position
|
||||||
|
else:
|
||||||
|
self.explode()
|
||||||
|
game.end()
|
||||||
|
|
||||||
|
def explode(self):
|
||||||
|
"""Sets the ship's character to ``*`` and its color to red.
|
||||||
|
"""
|
||||||
|
self.color = "crimson_on_skyblue1"
|
||||||
|
self.character = '*'
|
||||||
|
|
||||||
|
class Asteroid:
|
||||||
|
"""When Asteroids are spawned, they fall down the screen until they
|
||||||
|
reach the bottom row and are removed.
|
||||||
|
An Asteroid's position is set when it is created.
|
||||||
|
Whenever an asteroid moves, it
|
||||||
|
checks whether it has it the ship.
|
||||||
|
"""
|
||||||
|
character = 'O'
|
||||||
|
color = "deepskyblue1_on_skyblue1"
|
||||||
|
|
||||||
|
def __init__(self, position):
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""Nothing happens unless
|
||||||
|
``game.turn_number`` is divisible by 2. The result is that asteroids
|
||||||
|
only move on even-numbered turns. If the asteroid is at the bottom of
|
||||||
|
the screen, it has run its course and should be removed from the game.
|
||||||
|
Otherwise, the asteroid's new position is one space down from its old
|
||||||
|
position. If the asteroid's new position is the same as the ship's
|
||||||
|
position, the game ends.
|
||||||
|
"""
|
||||||
|
if game.turn_number % 2 == 0:
|
||||||
|
self.set_color()
|
||||||
|
x, y = self.position
|
||||||
|
if y == HEIGHT - 1:
|
||||||
|
game.remove_agent(self)
|
||||||
|
else:
|
||||||
|
ship = game.get_agent_by_name('ship')
|
||||||
|
new_position = (x, y + 1)
|
||||||
|
if new_position == ship.position:
|
||||||
|
ship.explode()
|
||||||
|
game.end()
|
||||||
|
else:
|
||||||
|
self.position = new_position
|
||||||
|
|
||||||
|
def set_color(self):
|
||||||
|
"""To add to the game's drama, asteroids gradually become visible as they
|
||||||
|
fall down the screen. This method calculates the ratio of the asteroid's
|
||||||
|
position compared to the screen height--0 is the top of the screen and 1 is
|
||||||
|
the bottom ot the screen. Then sets the asteroid's color depending on the
|
||||||
|
ratio. (`Available colors <https://blessed.readthedocs.io/en/latest/colors.html>`_)
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
ratio = y / HEIGHT
|
||||||
|
if ratio < 0.2:
|
||||||
|
self.color = "deepskyblue1_on_skyblue1"
|
||||||
|
elif ratio < 0.4:
|
||||||
|
self.color = "deepskyblue2_on_skyblue1"
|
||||||
|
elif ratio < 0.6:
|
||||||
|
self.color = "deepskyblue3_on_skyblue1"
|
||||||
|
else:
|
||||||
|
self.color = "deepskyblue4_on_skyblue1"
|
||||||
|
|
||||||
|
class AsteroidSpawner:
|
||||||
|
"""An agent which is not displayed on the board, but which constantly spawns
|
||||||
|
asteroids.
|
||||||
|
"""
|
||||||
|
display = False
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""Adds 1 to the game score and then uses
|
||||||
|
:py:meth:`~retro.examples.nav.should_spawn_asteroid` to decide whether to
|
||||||
|
spawn an asteroid. When :py:meth:`~retro.examples.nav.should_spawn_asteroid`
|
||||||
|
comes back ``True``, creates a new instance of
|
||||||
|
:py:class:`~retro.examples.nav.Asteroid` at a random position along the
|
||||||
|
top of the screen and adds the asteroid to the game.
|
||||||
|
"""
|
||||||
|
game.state['score'] += 1
|
||||||
|
if self.should_spawn_asteroid(game.turn_number):
|
||||||
|
asteroid = Asteroid((randint(0, WIDTH - 1), 0))
|
||||||
|
game.add_agent(asteroid)
|
||||||
|
|
||||||
|
def should_spawn_asteroid(self, turn_number):
|
||||||
|
"""Decides whether to spawn an asteroid.
|
||||||
|
Uses a simple but effective algorithm to make the game get
|
||||||
|
progressively more difficult: choose a random number and return
|
||||||
|
``True`` if the number is less than the current turn number. At
|
||||||
|
the beginning of the game, few asteroids will be spawned. As the
|
||||||
|
turn number climbs toward 1000, asteroids are spawned almost
|
||||||
|
every turn.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
turn_number (int): The current turn in the game.
|
||||||
|
"""
|
||||||
|
return randint(0, 1000) < turn_number
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ship = Spaceship()
|
||||||
|
spawner = AsteroidSpawner()
|
||||||
|
game = Game(
|
||||||
|
[ship, spawner],
|
||||||
|
{"score": 0},
|
||||||
|
board_size=(WIDTH, HEIGHT),
|
||||||
|
color="deepskyblue4_on_skyblue1",
|
||||||
|
)
|
||||||
|
game.play()
|
||||||
|
|
||||||
0
retro/examples/nav.py:Zone.Identifier
Normal file
0
retro/examples/nav.py:Zone.Identifier
Normal file
7
retro/examples/simple.py
Normal file
7
retro/examples/simple.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from retro.game import Game
|
||||||
|
from retro.agent import ArrowKeyAgent
|
||||||
|
|
||||||
|
agent = ArrowKeyAgent()
|
||||||
|
state = {}
|
||||||
|
game = Game([agent], state)
|
||||||
|
game.play()
|
||||||
0
retro/examples/simple.py:Zone.Identifier
Normal file
0
retro/examples/simple.py:Zone.Identifier
Normal file
198
retro/examples/snake.py
Normal file
198
retro/examples/snake.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
from random import randint
|
||||||
|
from retro.game import Game
|
||||||
|
|
||||||
|
class Apple:
|
||||||
|
"""An agent representing the Apple.
|
||||||
|
Note how Apple doesn't have ``play_turn`` or
|
||||||
|
``handle_keystroke`` methods: the Apple doesn't need to do
|
||||||
|
anything in this game. It just sits there waiting to get
|
||||||
|
eaten.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: "Apple"
|
||||||
|
character: '@'
|
||||||
|
color: "red_on_black" (`Here's documentation on how colors
|
||||||
|
work <https://blessed.readthedocs.io/en/latest/colors.html>`_
|
||||||
|
position: (0, 0). The Apple will choose a random position
|
||||||
|
as soon as the game starts, but it needs an initial
|
||||||
|
position to be assigned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = "Apple"
|
||||||
|
character = '@'
|
||||||
|
color = "red_on_black"
|
||||||
|
position = (0, 0)
|
||||||
|
|
||||||
|
def relocate(self, game):
|
||||||
|
"""Sets position to a random empty position. This method is
|
||||||
|
called whenever the snake's head touches the apple.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
game (Game): The current game.
|
||||||
|
"""
|
||||||
|
self.position = self.random_empty_position(game)
|
||||||
|
|
||||||
|
def random_empty_position(self, game):
|
||||||
|
"""Returns a randomly-selected empty position. Uses a very
|
||||||
|
simple algorithm: Get the game's board size, choose a
|
||||||
|
random x-value between 0 and the board width, and choose
|
||||||
|
a random y-value between 0 and the board height. Now use
|
||||||
|
the game to check whether any Agents are occupying this
|
||||||
|
position. If so, keep randomly choosing a new position
|
||||||
|
until the position is empty.
|
||||||
|
"""
|
||||||
|
bw, bh = game.board_size
|
||||||
|
occupied_positions = game.get_agents_by_position()
|
||||||
|
while True:
|
||||||
|
position = (randint(0, bw-1), randint(0, bh-1))
|
||||||
|
if position not in occupied_positions:
|
||||||
|
return position
|
||||||
|
|
||||||
|
class SnakeHead:
|
||||||
|
"""An Agent representing the snake's head. When the game starts, you control
|
||||||
|
the snake head using the arrow keys. The SnakeHead always has a direction, and
|
||||||
|
will keep moving in that direction every turn. When you press an arrow key,
|
||||||
|
you change the SnakeHead's direction.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: "Snake head"
|
||||||
|
position: (0,0)
|
||||||
|
character: ``'v'`` Depending on the snake head's direction, its character
|
||||||
|
changes to ``'<'``, ``'^'``, ``'>'``, or ``'v'``.
|
||||||
|
next_segment: Initially ``None``, this is a reference to a SnakeBodySegment.
|
||||||
|
growing: When set to True, the snake will grow a new segment on its next move.
|
||||||
|
"""
|
||||||
|
RIGHT = (1, 0)
|
||||||
|
UP = (0, -1)
|
||||||
|
LEFT = (-1, 0)
|
||||||
|
DOWN = (0, 1)
|
||||||
|
name = "Snake head"
|
||||||
|
position = (0, 0)
|
||||||
|
direction = DOWN
|
||||||
|
character = 'v'
|
||||||
|
next_segment = None
|
||||||
|
growing = False
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""On each turn, the snake head uses its position and direction to figure out
|
||||||
|
its next position. If the snake head is able to move there (it's on the board and
|
||||||
|
not occuppied by part of the snake's body), it moves.
|
||||||
|
|
||||||
|
Then, if the snake head is on the Apple, the Apple moves to a new random position
|
||||||
|
and ``growing`` is set to True.
|
||||||
|
|
||||||
|
Now we need to deal with two situations. First, if ``next_segment`` is not None, there is
|
||||||
|
a SnakeBodySegment attached to the head. We need the body to follow the head,
|
||||||
|
so we call ``self.next_segment.move``, passing the head's old position
|
||||||
|
(this will be the body's new position), a reference to the game, and a value for
|
||||||
|
``growing``. If the snake needs to grow, we need to pass this information along
|
||||||
|
the body until it reaches the tail--this is where the next segment will be attached.
|
||||||
|
|
||||||
|
If there is no ``next_segment`` but ``self.growing`` is True, it's time to add
|
||||||
|
a body! We set ``self.next_segment`` to a new SnakeBodySegment, set its
|
||||||
|
position to the head's old position, and add it to the game. We also add 1 to the
|
||||||
|
game's score.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
dx, dy = self.direction
|
||||||
|
if self.can_move((x+dx, y+dy), game):
|
||||||
|
self.position = (x+dx, y+dy)
|
||||||
|
if self.is_on_apple(self.position, game):
|
||||||
|
apple = game.get_agent_by_name("Apple")
|
||||||
|
apple.relocate(game)
|
||||||
|
self.growing = True
|
||||||
|
if self.next_segment:
|
||||||
|
self.next_segment.move((x, y), game, growing=self.growing)
|
||||||
|
elif self.growing:
|
||||||
|
self.next_segment = SnakeBodySegment(1, (x, y))
|
||||||
|
game.add_agent(self.next_segment)
|
||||||
|
game.state['score'] += 1
|
||||||
|
self.growing = False
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""Checks whether one of the arrow keys has been pressed.
|
||||||
|
If so, sets the SnakeHead's direction and character.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name == "KEY_RIGHT":
|
||||||
|
self.direction = self.RIGHT
|
||||||
|
self.character = '>'
|
||||||
|
elif keystroke.name == "KEY_UP":
|
||||||
|
self.direction = self.UP
|
||||||
|
self.character = '^'
|
||||||
|
elif keystroke.name == "KEY_LEFT":
|
||||||
|
self.direction = self.LEFT
|
||||||
|
self.character = '<'
|
||||||
|
elif keystroke.name == "KEY_DOWN":
|
||||||
|
self.direction = self.DOWN
|
||||||
|
self.character = 'v'
|
||||||
|
|
||||||
|
def can_move(self, position, game):
|
||||||
|
on_board = game.on_board(position)
|
||||||
|
empty = game.is_empty(position)
|
||||||
|
on_apple = self.is_on_apple(position, game)
|
||||||
|
return on_board and (empty or on_apple)
|
||||||
|
|
||||||
|
def is_on_apple(self, position, game):
|
||||||
|
apple = game.get_agent_by_name("Apple")
|
||||||
|
return apple.position == position
|
||||||
|
|
||||||
|
class SnakeBodySegment:
|
||||||
|
"""Finally, we need an Agent for the snake's body segments.
|
||||||
|
SnakeBodySegment doesn't have ``play_turn`` or ``handle_keystroke`` methods because
|
||||||
|
it never does anything on its own. It only moves when the SnakeHead, or the previous
|
||||||
|
segment, tells it to move.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
segment_id (int): Keeps track of how far back this segment is from the head.
|
||||||
|
This is used to give the segment a unique name, and also to keep track
|
||||||
|
of how many points the player earns for eating the next apple.
|
||||||
|
position (int, int): The initial position.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
character: '*'
|
||||||
|
next_segment: Initially ``None``, this is a reference to a SnakeBodySegment
|
||||||
|
when this segment is not the last one in the snake's body.
|
||||||
|
|
||||||
|
"""
|
||||||
|
character = '*'
|
||||||
|
next_segment = None
|
||||||
|
|
||||||
|
def __init__(self, segment_id, position):
|
||||||
|
self.segment_id = segment_id
|
||||||
|
self.name = f"Snake body segment {segment_id}"
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
def move(self, new_position, game, growing=False):
|
||||||
|
"""When SnakeHead moves, it sets off a chain reaction, moving all its
|
||||||
|
body segments. Whenever the head or a body segment has another segment
|
||||||
|
(``next_segment``), it calls that segment's ``move`` method.
|
||||||
|
|
||||||
|
This method updates the SnakeBodySegment's position. Then, if
|
||||||
|
``self.next_segment`` is not None, calls that segment's ``move`` method.
|
||||||
|
If there is no next segment and ``growing`` is True, then we set
|
||||||
|
``self.next_segment`` to a new SnakeBodySegment in this segment's old
|
||||||
|
position, and update the game's score.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
new_position (int, int): The new position.
|
||||||
|
game (Game): A reference to the current game.
|
||||||
|
growing (bool): (Default False) When True, the snake needs to
|
||||||
|
add a new segment.
|
||||||
|
"""
|
||||||
|
old_position = self.position
|
||||||
|
self.position = new_position
|
||||||
|
if self.next_segment:
|
||||||
|
self.next_segment.move(old_position, game, growing=growing)
|
||||||
|
elif growing:
|
||||||
|
self.next_segment = SnakeBodySegment(self.segment_id + 1, old_position)
|
||||||
|
game.add_agent(self.next_segment)
|
||||||
|
game.state['score'] += self.segment_id + 1
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
head = SnakeHead()
|
||||||
|
apple = Apple()
|
||||||
|
game = Game([head, apple], {'score': 0}, board_size=(32, 16), framerate=12)
|
||||||
|
apple.relocate(game)
|
||||||
|
game.play()
|
||||||
|
|
||||||
0
retro/examples/snake.py:Zone.Identifier
Normal file
0
retro/examples/snake.py:Zone.Identifier
Normal file
216
retro/game.py
Normal file
216
retro/game.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from signal import signal, SIGWINCH
|
||||||
|
from time import sleep, perf_counter
|
||||||
|
from blessed import Terminal
|
||||||
|
from retro.view import View
|
||||||
|
from retro.validation import (
|
||||||
|
validate_agent,
|
||||||
|
validate_state,
|
||||||
|
validate_agent_name,
|
||||||
|
validate_position,
|
||||||
|
)
|
||||||
|
from retro.errors import (
|
||||||
|
AgentWithNameAlreadyExists,
|
||||||
|
AgentNotFoundByName,
|
||||||
|
IllegalMove,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
"""
|
||||||
|
Creates a playable game.
|
||||||
|
You will use Game to create games, but don't need to read or understand how
|
||||||
|
this class works. The main work in creating a
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
agents (list): A list of agents to add to the game.
|
||||||
|
state (dict): A dict containing the game's initial state.
|
||||||
|
board_size (int, int): (Optional) The two-dimensional size of the game board. D
|
||||||
|
debug (bool): (Optional) Turn on debug mode, showing log messages while playing.
|
||||||
|
framerate (int): (Optional) The target number of frames per second at which the
|
||||||
|
game should run.
|
||||||
|
color (str): (Optional) The game's background color scheme. `Available colors <https://blessed.readthedocs.io/en/latest/colors.html>`_.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# This example will create a simple game.
|
||||||
|
from retro.game import Game
|
||||||
|
from retro.agent import ArrowKeyAgent
|
||||||
|
|
||||||
|
agents = [ArrowKeyAgent()]
|
||||||
|
state = {}
|
||||||
|
game = Game(agents, state)
|
||||||
|
game.play()
|
||||||
|
|
||||||
|
"""
|
||||||
|
STATE_HEIGHT = 5
|
||||||
|
EXIT_CHARACTERS = ("KEY_ENTER", "KEY_ESCAPE")
|
||||||
|
|
||||||
|
def __init__(self, agents, state, board_size=(64, 32), debug=False, framerate=24,
|
||||||
|
color="white_on_black"):
|
||||||
|
self.log_messages = []
|
||||||
|
self.agents_by_name = {}
|
||||||
|
self.agents = []
|
||||||
|
self.state = validate_state(state)
|
||||||
|
self.board_size = board_size
|
||||||
|
self.debug = debug
|
||||||
|
self.framerate = framerate
|
||||||
|
self.turn_number = 0
|
||||||
|
self.color = color
|
||||||
|
for agent in agents:
|
||||||
|
self.add_agent(agent)
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
"""Starts the game.
|
||||||
|
"""
|
||||||
|
self.playing = True
|
||||||
|
terminal = Terminal()
|
||||||
|
with terminal.fullscreen(), terminal.hidden_cursor(), terminal.cbreak():
|
||||||
|
view = View(terminal, color=self.color)
|
||||||
|
while self.playing:
|
||||||
|
turn_start_time = perf_counter()
|
||||||
|
self.turn_number += 1
|
||||||
|
self.keys_pressed = self.collect_keystrokes(terminal)
|
||||||
|
if self.debug and self.keys_pressed:
|
||||||
|
self.log("Keys: " + ', '.join(k.name or str(k) for k in self.keys_pressed))
|
||||||
|
for agent in self.agents:
|
||||||
|
if hasattr(agent, 'handle_keystroke'):
|
||||||
|
for key in self.keys_pressed:
|
||||||
|
agent.handle_keystroke(key, self)
|
||||||
|
if hasattr(agent, 'play_turn'):
|
||||||
|
agent.play_turn(self)
|
||||||
|
if getattr(agent, 'display', True):
|
||||||
|
if not self.on_board(agent.position):
|
||||||
|
raise IllegalMove(agent, agent.position)
|
||||||
|
view.render(self)
|
||||||
|
turn_end_time = perf_counter()
|
||||||
|
time_elapsed_in_turn = turn_end_time - turn_start_time
|
||||||
|
time_remaining_in_turn = max(0, 1/self.framerate - time_elapsed_in_turn)
|
||||||
|
sleep(time_remaining_in_turn)
|
||||||
|
while True:
|
||||||
|
if terminal.inkey().name in self.EXIT_CHARACTERS:
|
||||||
|
break
|
||||||
|
|
||||||
|
def collect_keystrokes(self, terminal):
|
||||||
|
keys = set()
|
||||||
|
while True:
|
||||||
|
key = terminal.inkey(0.001)
|
||||||
|
if key:
|
||||||
|
keys.add(key)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
"""Write a log message.
|
||||||
|
Log messages are only shown when debug mode is on.
|
||||||
|
They can be very useful for debugging.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
message (str): The message to log.
|
||||||
|
"""
|
||||||
|
self.log_messages.append((self.turn_number, message))
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
"""Ends the game. No more turns will run.
|
||||||
|
"""
|
||||||
|
self.playing = False
|
||||||
|
|
||||||
|
def add_agent(self, agent):
|
||||||
|
"""Adds an agent to the game.
|
||||||
|
Whenever you want to add a new agent during the game, you must add it to
|
||||||
|
the game using this method.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
agent: An instance of an agent class.
|
||||||
|
"""
|
||||||
|
validate_agent(agent)
|
||||||
|
if getattr(agent, "display", True) and not self.on_board(agent.position):
|
||||||
|
raise IllegalMove(agent, agent.position)
|
||||||
|
if hasattr(agent, "name"):
|
||||||
|
if agent.name in self.agents_by_name:
|
||||||
|
raise AgentWithNameAlreadyExists(agent.name)
|
||||||
|
self.agents_by_name[agent.name] = agent
|
||||||
|
self.agents.append(agent)
|
||||||
|
|
||||||
|
def get_agent_by_name(self, name):
|
||||||
|
"""Looks up an agent by name.
|
||||||
|
This is useful when one agent needs to interact with another agent.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name (str): The agent's name. If there is no agent with this name,
|
||||||
|
you will get an error.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An agent.
|
||||||
|
"""
|
||||||
|
validate_agent_name(name)
|
||||||
|
if name in self.agents_by_name:
|
||||||
|
return self.agents_by_name[name]
|
||||||
|
else:
|
||||||
|
raise AgentNotFoundByName(name)
|
||||||
|
|
||||||
|
def is_empty(self, position):
|
||||||
|
"""Checks whether a position is occupied by any agents.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
position (int, int): The position to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A bool
|
||||||
|
"""
|
||||||
|
return position not in self.get_agents_by_position()
|
||||||
|
|
||||||
|
def get_agents_by_position(self):
|
||||||
|
"""Returns a dict where each key is a position (e.g. (10, 20)) and
|
||||||
|
each value is a list containing all the agents at that position.
|
||||||
|
This is useful when an agent needs to find out which other agents are
|
||||||
|
on the same space or nearby.
|
||||||
|
"""
|
||||||
|
positions = defaultdict(list)
|
||||||
|
for agent in self.agents:
|
||||||
|
if getattr(agent, "display", True):
|
||||||
|
validate_position(agent.position)
|
||||||
|
positions[agent.position].append(agent)
|
||||||
|
return positions
|
||||||
|
|
||||||
|
def remove_agent(self, agent):
|
||||||
|
"""Removes an agent from the game.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
agent (Agent): the agent to remove.
|
||||||
|
"""
|
||||||
|
if agent not in self.agents:
|
||||||
|
raise AgentNotInGame(agent)
|
||||||
|
else:
|
||||||
|
self.agents.remove(agent)
|
||||||
|
if hasattr(agent, "name"):
|
||||||
|
self.agents_by_name.pop(agent.name)
|
||||||
|
|
||||||
|
def remove_agent_by_name(self, name):
|
||||||
|
"""Removes an agent from the game.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name (str): the agent's name.
|
||||||
|
"""
|
||||||
|
validate_agent_name(name)
|
||||||
|
if name not in self.agents_by_name:
|
||||||
|
raise AgentNotFoundByName(name)
|
||||||
|
agent = self.agents_by_name.pop(name)
|
||||||
|
self.agents.remove(agent)
|
||||||
|
|
||||||
|
def on_board(self, position):
|
||||||
|
"""Checks whether a position is on the game board.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
position (int, int): The position to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A bool
|
||||||
|
"""
|
||||||
|
validate_position(position)
|
||||||
|
x, y = position
|
||||||
|
bx, by = self.board_size
|
||||||
|
return x >= 0 and x < bx and y >= 0 and y < by
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
retro/game.py:Zone.Identifier
Normal file
0
retro/game.py:Zone.Identifier
Normal file
162
retro/graph.py
Normal file
162
retro/graph.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
from retro.errors import GraphError
|
||||||
|
|
||||||
|
class Graph:
|
||||||
|
def __init__(self, vertices=None, edges=None):
|
||||||
|
self.vertices = vertices or []
|
||||||
|
self.edges = edges or []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '\n'.join(str(e) for e in self.edges)
|
||||||
|
|
||||||
|
def get_or_create_vertex(self, x, y):
|
||||||
|
for v in self.vertices:
|
||||||
|
if x == v.x and y == v.y:
|
||||||
|
return v
|
||||||
|
for e in self.edges:
|
||||||
|
if e.crosses(x, y):
|
||||||
|
return self.split_edge(e, x, y)
|
||||||
|
v = Vertex(x, y)
|
||||||
|
self.vertices.append(v)
|
||||||
|
return v
|
||||||
|
|
||||||
|
def get_or_create_edge(self, x0, y0, x1, y1):
|
||||||
|
v0 = self.get_or_create_vertex(x0, y0)
|
||||||
|
v1 = self.get_or_create_vertex(x1, y1)
|
||||||
|
new_edge = Edge(v0, v1)
|
||||||
|
for e in self.edges:
|
||||||
|
if e == new_edge:
|
||||||
|
new_edge.remove()
|
||||||
|
return e
|
||||||
|
return new_edge
|
||||||
|
|
||||||
|
def split_edge(self, edge, x, y):
|
||||||
|
"""
|
||||||
|
Splits an edge by inserting a new vertex along the edge.
|
||||||
|
"""
|
||||||
|
if not edge.crosses(x, y):
|
||||||
|
raise GraphError(f"Can't split edge {edge} at ({x}, {y})")
|
||||||
|
self.remove_edge(edge)
|
||||||
|
v = Vertex(x, y)
|
||||||
|
self.vertices.append(v)
|
||||||
|
self.edges.append(Edge(edge.begin, v))
|
||||||
|
self.edges.append(Edge(v, edge.end))
|
||||||
|
|
||||||
|
def remove_edge(self, edge):
|
||||||
|
if edge not in self.edges:
|
||||||
|
raise GraphError(f"Edge {edge} is not in the graph")
|
||||||
|
self.edges.remove(edge)
|
||||||
|
edge.begin.edges.remove(edge)
|
||||||
|
edge.end.edges.remove(edge)
|
||||||
|
|
||||||
|
def render(self, terminal):
|
||||||
|
for v in self.vertices:
|
||||||
|
v.render(terminal)
|
||||||
|
for e in self.edges:
|
||||||
|
e.render(terminal)
|
||||||
|
|
||||||
|
class Vertex:
|
||||||
|
CHARACTERS = {
|
||||||
|
"0000": " ",
|
||||||
|
"0001": "═",
|
||||||
|
"0010": "║",
|
||||||
|
"0011": "╗",
|
||||||
|
"0100": "═",
|
||||||
|
"0101": "═",
|
||||||
|
"0110": "╔",
|
||||||
|
"0111": "╦",
|
||||||
|
"1000": "║",
|
||||||
|
"1001": "╝",
|
||||||
|
"1010": "║",
|
||||||
|
"1011": "╣",
|
||||||
|
"1100": "╚",
|
||||||
|
"1101": "╩",
|
||||||
|
"1110": "╠",
|
||||||
|
"1111": "╬",
|
||||||
|
}
|
||||||
|
def __init__(self, x, y):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.edges = []
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"({self.x}, {self.y})"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.x == other.x and self.y == other.y
|
||||||
|
|
||||||
|
def neighbors(self):
|
||||||
|
vertices = []
|
||||||
|
for edge in self.edges:
|
||||||
|
if self == edge.begin:
|
||||||
|
vertices.append(edge.end)
|
||||||
|
else:
|
||||||
|
vertices.append(edge.begin)
|
||||||
|
return vertices
|
||||||
|
|
||||||
|
def render(self, terminal):
|
||||||
|
print(terminal.move_xy(self.x, self.y) + self.get_character())
|
||||||
|
|
||||||
|
def get_character(self):
|
||||||
|
u = self.has_up_edge()
|
||||||
|
r = self.has_right_edge()
|
||||||
|
d = self.has_down_edge()
|
||||||
|
l = self.has_left_edge()
|
||||||
|
code = ''.join([str(int(direction)) for direction in [u, r, d, l]])
|
||||||
|
return self.CHARACTERS[code]
|
||||||
|
|
||||||
|
def has_up_edge(self):
|
||||||
|
return any([v.x == self.x and v.y < self.y for v in self.neighbors()])
|
||||||
|
|
||||||
|
def has_right_edge(self):
|
||||||
|
return any([v.y == self.y and self.x < v.x for v in self.neighbors()])
|
||||||
|
|
||||||
|
def has_down_edge(self):
|
||||||
|
return any([v.x == self.x and self.y < v.y for v in self.neighbors()])
|
||||||
|
|
||||||
|
def has_left_edge(self):
|
||||||
|
return any([v.y == self.y and v.x < self.x for v in self.neighbors()])
|
||||||
|
|
||||||
|
class Edge:
|
||||||
|
def __init__(self, begin, end):
|
||||||
|
if not isinstance(begin, Vertex) or not isinstance(end, Vertex):
|
||||||
|
raise ValueError("Tried to initialize an Edge with a non-vertex")
|
||||||
|
if begin.x < end.x or begin.y < end.y:
|
||||||
|
self.begin = begin
|
||||||
|
self.end = end
|
||||||
|
else:
|
||||||
|
self.begin = end
|
||||||
|
self.end = begin
|
||||||
|
if not (self.is_horizontal() or self.is_vertical()):
|
||||||
|
raise ValueError("Edges must be horizontal or vertical.")
|
||||||
|
if self.is_horizontal() and self.is_vertical():
|
||||||
|
raise ValueError("Self-edges are not allowed.")
|
||||||
|
self.begin.edges.append(self)
|
||||||
|
self.end.edges.append(self)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.begin} -> {self.end}"
|
||||||
|
|
||||||
|
def render(self, terminal):
|
||||||
|
if self.is_horizontal():
|
||||||
|
with terminal.location(self.begin.x + 1, self.begin.y):
|
||||||
|
line = "═" * (self.end.x - self.begin.x - 1)
|
||||||
|
print(line)
|
||||||
|
else:
|
||||||
|
for y in range(self.begin.y + 1, self.end.y):
|
||||||
|
print(terminal.move_xy(self.begin.x, y) + "║")
|
||||||
|
|
||||||
|
def is_horizontal(self):
|
||||||
|
return self.begin.y == self.end.y
|
||||||
|
|
||||||
|
def is_vertical(self):
|
||||||
|
return self.begin.x == self.end.x
|
||||||
|
|
||||||
|
def crosses(self, x, y):
|
||||||
|
if self.is_horizontal():
|
||||||
|
return self.begin.y == y and self.begin.x < x and x < self.end.x
|
||||||
|
else:
|
||||||
|
return self.begin.x == x and self.begin.y < y and y < self.end.y
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
self.begin.edges.remove(self)
|
||||||
|
self.end.edges.remove(self)
|
||||||
0
retro/graph.py:Zone.Identifier
Normal file
0
retro/graph.py:Zone.Identifier
Normal file
5
retro/grid.py
Normal file
5
retro/grid.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from retro.graph import Vertex, Edge, Graph
|
||||||
|
|
||||||
|
class Grid:
|
||||||
|
def __init__(self):
|
||||||
|
self.graph = Graph
|
||||||
0
retro/grid.py:Zone.Identifier
Normal file
0
retro/grid.py:Zone.Identifier
Normal file
44
retro/validation.py
Normal file
44
retro/validation.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
def validate_agent(agent):
|
||||||
|
if hasattr(agent, "name"):
|
||||||
|
validate_agent_name(agent.name)
|
||||||
|
if getattr(agent, 'display', True):
|
||||||
|
validate_position(agent.position)
|
||||||
|
if not hasattr(agent, "character"):
|
||||||
|
raise ValueError(f"Agent {agent.name} must have a character")
|
||||||
|
return agent
|
||||||
|
|
||||||
|
def validate_state(state):
|
||||||
|
if not isinstance(state, dict):
|
||||||
|
raise TypeError(f"State is {type(state)}, but must be a dict.")
|
||||||
|
for key, value in state.items():
|
||||||
|
if is_mutable(value):
|
||||||
|
raise ValueError(f"State must be immutable, but state[{key}] is {value}")
|
||||||
|
return state
|
||||||
|
|
||||||
|
def validate_agent_name(name):
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise TypeError(f"Agent names must be strings")
|
||||||
|
return name
|
||||||
|
|
||||||
|
def validate_position(position):
|
||||||
|
if not isinstance(position, tuple):
|
||||||
|
raise TypeError(f"Position is {type(position)}, but must be a tuple.")
|
||||||
|
if not len(position) == 2:
|
||||||
|
raise ValueError(f"Position is {position}. Must be a tuple of two integers.")
|
||||||
|
if not isinstance(position[0], int) and isinstance(position[1], int):
|
||||||
|
raise TypeError(f"Position is {position}. Must be a tuple of two integers.")
|
||||||
|
return position
|
||||||
|
|
||||||
|
def is_mutable(obj):
|
||||||
|
if isinstance(obj, (int, float, bool, str, None)):
|
||||||
|
return False
|
||||||
|
elif isinstance(obj, tuple):
|
||||||
|
return all(is_mutable(element) for element in obj)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
retro/validation.py:Zone.Identifier
Normal file
0
retro/validation.py:Zone.Identifier
Normal file
127
retro/view.py
Normal file
127
retro/view.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from retro.graph import Vertex, Edge, Graph
|
||||||
|
from retro.errors import TerminalTooSmall
|
||||||
|
|
||||||
|
class View:
|
||||||
|
BORDER_X = 2
|
||||||
|
BORDER_Y = 3
|
||||||
|
STATE_HEIGHT = 5
|
||||||
|
DEBUG_WIDTH = 60
|
||||||
|
|
||||||
|
def __init__(self, terminal, color='white_on_black'):
|
||||||
|
self.terminal = terminal
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
def render(self, game):
|
||||||
|
self.render_layout(game)
|
||||||
|
ox, oy = self.get_board_origin_coords(game)
|
||||||
|
self.render_state(game)
|
||||||
|
if game.debug:
|
||||||
|
self.render_debug_log(game)
|
||||||
|
for agent in sorted(game.agents, key=lambda a: getattr(a, 'z', 0)):
|
||||||
|
if getattr(agent, 'display', True):
|
||||||
|
ax, ay = agent.position
|
||||||
|
if hasattr(agent, 'color'):
|
||||||
|
color = self.get_color(agent.color)
|
||||||
|
print(self.terminal.move_xy(ox + ax, oy + ay) + color(agent.character))
|
||||||
|
else:
|
||||||
|
print(self.terminal.move_xy(ox + ax, oy + ay) + agent.character)
|
||||||
|
|
||||||
|
def render_layout(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
self.check_terminal_size(game)
|
||||||
|
self.clear_screen()
|
||||||
|
layout_graph = self.get_layout_graph(game)
|
||||||
|
layout_graph.render(self.terminal)
|
||||||
|
|
||||||
|
def clear_screen(self):
|
||||||
|
print(self.terminal.home + self.get_color(self.color) + self.terminal.clear)
|
||||||
|
|
||||||
|
def get_color(self, color_string):
|
||||||
|
if not hasattr(self.terminal, color_string):
|
||||||
|
msg = (
|
||||||
|
f"{color_string} is not a supported color."
|
||||||
|
"See https://blessed.readthedocs.io/en/latest/colors.html"
|
||||||
|
)
|
||||||
|
raise ValueError(msg)
|
||||||
|
return getattr(self.terminal, color_string)
|
||||||
|
|
||||||
|
def render_state(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
ox, oy = self.get_state_origin_coords(game)
|
||||||
|
for i, key in enumerate(sorted(game.state.keys())):
|
||||||
|
msg = f"{key}: {game.state[key]}"[:bw]
|
||||||
|
print(self.terminal.move_xy(ox, oy + i) + msg)
|
||||||
|
|
||||||
|
def render_debug_log(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
debug_height = bh + self.STATE_HEIGHT
|
||||||
|
ox, oy = self.get_debug_origin_coords(game)
|
||||||
|
for i, (turn_number, message) in enumerate(game.log_messages[-debug_height:]):
|
||||||
|
msg = f"{turn_number}. {message}"[:self.DEBUG_WIDTH]
|
||||||
|
print(self.terminal.move_xy(ox, oy + i) + msg)
|
||||||
|
|
||||||
|
def get_layout_graph(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
sh = self.STATE_HEIGHT
|
||||||
|
ox, oy = self.get_board_origin_coords(game)
|
||||||
|
|
||||||
|
vertices = [
|
||||||
|
Vertex(ox - 1, oy - 1),
|
||||||
|
Vertex(ox + bw, oy - 1),
|
||||||
|
Vertex(ox + bw, oy + bh),
|
||||||
|
Vertex(ox + bw, oy + bh + sh),
|
||||||
|
Vertex(ox - 1, oy + bh + sh),
|
||||||
|
Vertex(ox - 1, oy + bh)
|
||||||
|
]
|
||||||
|
edges = [
|
||||||
|
Edge(vertices[0], vertices[1]),
|
||||||
|
Edge(vertices[1], vertices[2]),
|
||||||
|
Edge(vertices[2], vertices[3]),
|
||||||
|
Edge(vertices[3], vertices[4]),
|
||||||
|
Edge(vertices[4], vertices[5]),
|
||||||
|
Edge(vertices[5], vertices[0]),
|
||||||
|
Edge(vertices[5], vertices[2]),
|
||||||
|
]
|
||||||
|
graph = Graph(vertices, edges)
|
||||||
|
if game.debug:
|
||||||
|
dw = self.DEBUG_WIDTH
|
||||||
|
graph.vertices.append(Vertex(ox + bw + dw, oy - 1))
|
||||||
|
graph.vertices.append(Vertex(ox + bw + dw, oy + bh + sh))
|
||||||
|
graph.edges.append(Edge(graph.vertices[1], graph.vertices[6]))
|
||||||
|
graph.edges.append(Edge(graph.vertices[6], graph.vertices[7]))
|
||||||
|
graph.edges.append(Edge(graph.vertices[3], graph.vertices[7]))
|
||||||
|
return graph
|
||||||
|
|
||||||
|
def check_terminal_size(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
width_needed = bw + self.BORDER_X
|
||||||
|
height_needed = bh + self.BORDER_Y + self.STATE_HEIGHT
|
||||||
|
if self.terminal.width < width_needed:
|
||||||
|
raise TerminalTooSmall(width=self.terminal.width, width_needed=width_needed)
|
||||||
|
elif self.terminal.height < height_needed:
|
||||||
|
raise TerminalTooSmall(height=self.terminal.height, height_needed=height_needed)
|
||||||
|
|
||||||
|
def board_origin(self, game):
|
||||||
|
x, y = self.get_board_origin_coords(game)
|
||||||
|
return self.terminal.move_xy(x, y)
|
||||||
|
|
||||||
|
def get_board_origin_coords(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
margin_top = (self.terminal.height - bh - self.BORDER_Y) // 2
|
||||||
|
if game.debug:
|
||||||
|
margin_left = (self.terminal.width - bw - self.DEBUG_WIDTH - self.BORDER_X) // 2
|
||||||
|
else:
|
||||||
|
margin_left = (self.terminal.width - bw - self.BORDER_X) // 2
|
||||||
|
return margin_left, margin_top
|
||||||
|
|
||||||
|
def get_state_origin_coords(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
ox, oy = self.get_board_origin_coords(game)
|
||||||
|
return ox, oy + bh + 1
|
||||||
|
|
||||||
|
def get_debug_origin_coords(self, game):
|
||||||
|
bw, bh = game.board_size
|
||||||
|
ox, oy = self.get_board_origin_coords(game)
|
||||||
|
return ox + bw + 1, oy
|
||||||
|
|
||||||
|
|
||||||
0
retro/view.py:Zone.Identifier
Normal file
0
retro/view.py:Zone.Identifier
Normal file
21
spaceship.py
21
spaceship.py
@@ -2,3 +2,24 @@
|
|||||||
# ------------
|
# ------------
|
||||||
# By MWC Contributors
|
# By MWC Contributors
|
||||||
# This module defines a spaceship agent class.
|
# This module defines a spaceship agent class.
|
||||||
|
|
||||||
|
class Spaceship:
|
||||||
|
name = "ship"
|
||||||
|
character = '^'
|
||||||
|
|
||||||
|
def __init__(self, board_size):
|
||||||
|
board_width, board_height = board_size
|
||||||
|
self.position = (board_width // 2, board_height - 1)
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name in ("KEY_LEFT", "KEY_RIGHT"):
|
||||||
|
if keystroke.name == "KEY_LEFT":
|
||||||
|
new_position = (x - 1, y)
|
||||||
|
else:
|
||||||
|
new_position = (x + 1, y)
|
||||||
|
if game.on_board(new_position):
|
||||||
|
if game.is_empty(new_position):
|
||||||
|
self.position = new_position
|
||||||
|
else:
|
||||||
|
game.end()
|
||||||
Reference in New Issue
Block a user