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:
parent
2ce382cfb6
commit
52c1128ed4
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19
asteroid.py
19
asteroid.py
|
@ -2,3 +2,22 @@
|
|||
# ------------
|
||||
# By MWC Contributors
|
||||
# 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
|
||||
# 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
|
|
@ -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"
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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,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,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,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,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,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,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,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,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,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,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,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: ...
|
File diff suppressed because it is too large
Load Diff
|
@ -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,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,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,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
|
||||
# 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()
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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,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.")
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
from retro.game import Game
|
||||
from retro.agent import ArrowKeyAgent
|
||||
|
||||
game = Game([ArrowKeyAgent()], {}, debug=True)
|
||||
game.play()
|
||||
|
|
@ -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,0 +1,7 @@
|
|||
from retro.game import Game
|
||||
from retro.agent import ArrowKeyAgent
|
||||
|
||||
agent = ArrowKeyAgent()
|
||||
state = {}
|
||||
game = Game([agent], state)
|
||||
game.play()
|
|
@ -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,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,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,0 +1,5 @@
|
|||
from retro.graph import Vertex, Edge, Graph
|
||||
|
||||
class Grid:
|
||||
def __init__(self):
|
||||
self.graph = Graph
|
|
@ -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,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
|
||||
|
||||
|
21
spaceship.py
21
spaceship.py
|
@ -2,3 +2,24 @@
|
|||
# ------------
|
||||
# By MWC Contributors
|
||||
# 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()
|
Loading…
Reference in New Issue