pax_global_header 0000666 0000000 0000000 00000000064 15107137110 0014506 g ustar 00root root 0000000 0000000 52 comment=864a8f7d6de752d7fede2c030758d245f1bb8e21
jquast-blessed-864a8f7/ 0000775 0000000 0000000 00000000000 15107137110 0015005 5 ustar 00root root 0000000 0000000 jquast-blessed-864a8f7/.editorconfig 0000664 0000000 0000000 00000000440 15107137110 0017460 0 ustar 00root root 0000000 0000000 root = true
[*.json]
charset = utf-8
tab_width = 4
indent_size = tab
indent_space = space
trim_trailing_whitespace = true
insert_final_newline = true
[*.py]
charset = utf-8
tab_width = 4
indent_size = tab
indent_space = space
trim_trailing_whitespace = true
insert_final_newline = true
jquast-blessed-864a8f7/.github/ 0000775 0000000 0000000 00000000000 15107137110 0016345 5 ustar 00root root 0000000 0000000 jquast-blessed-864a8f7/.github/workflows/ 0000775 0000000 0000000 00000000000 15107137110 0020402 5 ustar 00root root 0000000 0000000 jquast-blessed-864a8f7/.github/workflows/tests.yml 0000664 0000000 0000000 00000004400 15107137110 0022265 0 ustar 00root root 0000000 0000000 name: Tests
on:
push:
pull_request:
release:
schedule:
# Every Thursday at 1 AM
- cron: '0 1 * * 4'
jobs:
Tests:
continue-on-error: ${{ matrix.optional || false }}
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
container: ${{ !startsWith(matrix.os, 'windows') && (matrix.container || format('python:{0}', matrix.python-version)) || null }}
name: ${{ matrix.label || matrix.python-version }} ${{ startsWith(matrix.os, 'windows') && '(Windows)' || '' }} ${{ matrix.optional && '[OPTIONAL]' }}
strategy:
fail-fast: false
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.14']
test_quick: [1]
include:
- python-version: '3.13'
label: Linting
toxenv: docformatter_check,flake8,flake8_tests,isort_check,mypy,sphinx,pydocstyle,pylint,pylint_tests,codespell
os-deps:
- enchant-2
- python-version: '3.13'
test_keyboard: 1
test_raw: 1
test_quick: 0
- python-version: '3.13'
os: windows-latest
test_quick: 0
env:
TOXENV: ${{ matrix.toxenv || format('py{0}', matrix.python-version) }}
TEST_QUICK: ${{ matrix.test_quick || 0 }}
TEST_KEYBOARD: ${{ matrix.test_keyboard || 0 }}
TEST_RAW: ${{ matrix.test_raw || 0 }}
TOXPYTHON: python${{ matrix.toxpython || matrix.python-version }}
steps:
- name: Install OS Dependencies
run: apt update && apt -y install ${{ join(matrix.os-deps, ' ') }}
if: ${{ matrix.os-deps }}
- uses: actions/checkout@v4
- name: Install tox
run: pip install tox
- name: Collect terminal information
run: tox -e about
- name: Run tox
run: tox
- name: Upload to Codecov
if: ${{ matrix.label != 'linting' }}
uses: codecov/codecov-action@v5
with:
verbose: true
name: ${{ matrix.label || matrix.python-version }} ${{ startsWith(matrix.os, 'windows') && '(Windows)' || '' }}
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
os: ${{ startsWith(matrix.os, 'windows') && 'windows' || 'linux' }}
env_vars: TOXENV,TEST_QUICK,TEST_KEYBOARD,TEST_RAW
jquast-blessed-864a8f7/.gitignore 0000664 0000000 0000000 00000000421 15107137110 0016772 0 ustar 00root root 0000000 0000000 .coverage
._coverage.*
.coverage.*
coverage.xml
.cache
.tox
*.egg-info
*.egg
*.pyc
results*.xml
build
dist
docs/_build
docs/all_the_colors.txt
docs/all_the_keys.txt
docs/_static/rgb
htmlcov
.coveralls.yml
.DS_Store
.*.sw?
.vscode
.python-version
.idea/
.venv
.pytest_cache
jquast-blessed-864a8f7/.readthedocs.yml 0000664 0000000 0000000 00000000422 15107137110 0020071 0 ustar 00root root 0000000 0000000 # https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
sphinx:
configuration: docs/conf.py
formats: all
build:
os: ubuntu-lts-latest
tools:
python: '3'
python:
install:
- method: pip
path: .
extra_requirements:
- docs
jquast-blessed-864a8f7/.spelling-ignore-words.txt 0000664 0000000 0000000 00000000034 15107137110 0022053 0 ustar 00root root 0000000 0000000 padd
parms
iTerm
THIRDPARTY
jquast-blessed-864a8f7/CONTRIBUTING.rst 0000664 0000000 0000000 00000002252 15107137110 0017447 0 ustar 00root root 0000000 0000000 Contributing
============
We welcome contributions via GitHub pull requests:
- `Fork a Repo `_
- `Creating a pull request
`_
Developing
----------
Prepare a developer environment. Then, from the blessed code folder::
pip install --editable .
Any changes made in this project folder are then made available to the python
interpreter as the 'blessed' package from any working directory.
Running Tests
~~~~~~~~~~~~~
Install and run tox
::
pip install --upgrade tox
tox
Py.test is used as the test runner, supporting positional arguments, you may
for example use `looponfailing
`_
with python 3.8, stopping at the first failing test case::
tox -epy38 -- -x
The test runner (``tox``) ensures all code and documentation complies with
standard python style guides, pep8 and pep257, as well as various static
analysis tools.
Test Coverage
~~~~~~~~~~~~~
When you contribute a new feature, make sure it is covered by tests.
Likewise, a bug fix should include a test demonstrating the bug.
jquast-blessed-864a8f7/LICENSE 0000664 0000000 0000000 00000002073 15107137110 0016014 0 ustar 00root root 0000000 0000000 Copyright (c) 2014 Jeff Quast
Copyright (c) 2011 Erik Rose
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
jquast-blessed-864a8f7/MANIFEST.in 0000664 0000000 0000000 00000000313 15107137110 0016540 0 ustar 00root root 0000000 0000000 graft docs
prune docs/_build
global-exclude *.py[cod] __pycache__
include LICENSE
include version.json
include *.txt
include tox.ini
recursive-include blessed py.typed
recursive-include tests *.py *.ans
jquast-blessed-864a8f7/README.rst 0000777 0000000 0000000 00000000000 15107137110 0021304 2docs/intro.rst ustar 00root root 0000000 0000000 jquast-blessed-864a8f7/bin/ 0000775 0000000 0000000 00000000000 15107137110 0015555 5 ustar 00root root 0000000 0000000 jquast-blessed-864a8f7/bin/bounce.py 0000775 0000000 0000000 00000001470 15107137110 0017407 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""Classic game of tennis."""
# std imports
from math import floor
# local
from blessed import Terminal
def roundxy(x, y):
return int(floor(x)), int(floor(y))
term = Terminal()
x, y, xs, ys = 2, 2, 0.4, 0.3
with term.cbreak(), term.hidden_cursor():
# clear the screen
print(term.home + term.black_on_olivedrab4 + term.clear)
# loop every 20ms
while term.inkey(timeout=0.02) != 'q':
# erase,
txt_erase = term.move_xy(*roundxy(x, y)) + ' '
# bounce,
if x >= (term.width - 1) or x <= 0:
xs *= -1
if y >= term.height or y <= 0:
ys *= -1
# move,
x, y = x + xs, y + ys
# draw !
txt_ball = term.move_xy(*roundxy(x, y)) + '█'
print(txt_erase + txt_ball, end='', flush=True)
jquast-blessed-864a8f7/bin/cnn.py 0000664 0000000 0000000 00000002657 15107137110 0016717 0 ustar 00root root 0000000 0000000 """Basic example of hyperlinks -- show CNN news site with clickable URL's."""
# std imports
import random
# 3rd party
import requests
# local
# 3rd-party
from bs4 import BeautifulSoup
# local imports
from blessed import Terminal
def embolden(phrase):
# bold some phrases
return phrase.isdigit() or phrase[:1].isupper()
def make_bold(term, text):
# embolden text
return ' '.join(term.bold(phrase) if embolden(phrase) else phrase
for phrase in text.split(' '))
def whitespace_only(term, line):
# return only left-hand whitespace of `line'.
return line[:term.length(line) - term.length(line.lstrip())]
def find_articles(soup):
return (a_link for a_link in soup.find_all('a') if '/article' in a_link.get('href'))
def main():
term = Terminal()
cnn_url = 'https://lite.cnn.io'
soup = BeautifulSoup(requests.get(cnn_url).content, 'html.parser')
textwrap_kwargs = {
'width': term.width - (term.width // 4),
'initial_indent': ' ' * (term.width // 6) + '* ',
'subsequent_indent': (' ' * (term.width // 6)) + ' ' * 2,
}
for a_href in find_articles(soup):
url_id = random.randrange(0, 1 << 24)
for line in term.wrap(make_bold(term, a_href.text), **textwrap_kwargs):
print(whitespace_only(term, line), end='')
print(term.link(cnn_url + a_href.get('href'), line.lstrip(), url_id))
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/color_query.py 0000775 0000000 0000000 00000001121 15107137110 0020470 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""Query terminal foreground and background colors."""
from blessed import Terminal
term = Terminal()
# Get foreground color
r, g, b = term.get_fgcolor()
if (r, g, b) != (-1, -1, -1):
print(f"Foreground color: RGB({r}, {g}, {b})")
print(f" Hex: #{r:04x}{g:04x}{b:04x}")
else:
print("Could not determine foreground color")
# Get background color
r, g, b = term.get_bgcolor()
if (r, g, b) != (-1, -1, -1):
print(f"Background color: RGB({r}, {g}, {b})")
print(f" Hex: #{r:04x}{g:04x}{b:04x}")
else:
print("Could not determine background color")
jquast-blessed-864a8f7/bin/colorchart.py 0000664 0000000 0000000 00000005025 15107137110 0020271 0 ustar 00root root 0000000 0000000 """
Utility to show X11 colors in 24-bit and downconverted to 256, 16, and 8 colors.
The time to generate the table is displayed to give an indication of how long each algorithm takes
compared to the others.
"""
# std imports
import sys
import timeit
import colorsys
# local
import blessed
from blessed.color import COLOR_DISTANCE_ALGORITHMS
from blessed.colorspace import X11_COLORNAMES_TO_RGB
def sort_colors():
"""Sort colors by HSV value and remove duplicates."""
colors = {}
for color_name, rgb_color in X11_COLORNAMES_TO_RGB.items():
if rgb_color not in colors:
colors[rgb_color] = color_name
return sorted(colors.items(),
key=lambda rgb: colorsys.rgb_to_hsv(*rgb[0]),
reverse=True)
ALGORITHMS = tuple(sorted(COLOR_DISTANCE_ALGORITHMS))
SORTED_COLORS = sort_colors()
def draw_chart(term):
"""Draw a chart of each color downconverted with selected distance algorithm."""
sys.stdout.write(term.home)
width = term.width
line = ''
line_len = 0
start = timeit.default_timer()
for color in SORTED_COLORS:
chart = ''
for noc in (1 << 24, 256, 16, 8):
term.number_of_colors = noc
chart += getattr(term, color[1])('█')
if line_len + 5 > width:
line += '\n'
line_len = 0
line += ' %s' % chart
line_len += 5
elapsed = round((timeit.default_timer() - start) * 1000)
print(line)
left_text = '[] to select, q to quit'
center_text = f'{term.color_distance_algorithm}'
right_text = f'{elapsed:d} ms\n'
sys.stdout.write(term.clear_eos + left_text +
term.center(center_text, term.width -
term.length(left_text) - term.length(right_text)) +
right_text)
def color_chart(term):
"""Main color chart application."""
term = blessed.Terminal()
algo_idx = 0
dirty = True
with term.cbreak(), term.hidden_cursor(), term.fullscreen():
while True:
if dirty:
draw_chart(term)
inp = term.inkey()
dirty = True
if inp in '[]':
algo_idx += 1 if inp == ']' else -1
algo_idx %= len(ALGORITHMS)
term.color_distance_algorithm = ALGORITHMS[algo_idx]
elif inp == '\x0c':
pass
elif inp in 'qQ':
break
else:
dirty = False
if __name__ == '__main__':
color_chart(blessed.Terminal())
jquast-blessed-864a8f7/bin/dec_modes_bracketed_paste.py 0000664 0000000 0000000 00000000745 15107137110 0023257 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
print("Paste some text (press 'q' to quit)...")
with term.bracketed_paste():
with term.cbreak():
while True:
ks = term.inkey()
if ks.name == 'BRACKETED_PASTE':
print(f"Pasted: {term.reverse(repr(ks.text))}")
elif ks == 'q':
print("Goodbye!")
break
elif ks:
print(f"Regular key: {ks!r}")
jquast-blessed-864a8f7/bin/dec_modes_focus.py 0000664 0000000 0000000 00000000675 15107137110 0021260 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
print("Switch focus to/from this terminal window, 'q' to stop.")
with term.focus_events():
with term.cbreak():
while True:
inp = term.inkey()
if inp.name == 'FOCUS_IN':
print("Focus gained")
elif inp.name == 'FOCUS_OUT':
print("Focus lost")
elif inp == 'q':
break
jquast-blessed-864a8f7/bin/dec_modes_query.py 0000664 0000000 0000000 00000001142 15107137110 0021274 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
# Query mouse support
mode = term.DecPrivateMode(term.DecPrivateMode.MOUSE_REPORT_CLICK)
response = term.get_dec_mode(mode)
print(f"Checking {mode.name} (mode {mode.value}) {mode.long_description}: ", end="")
if response.supported:
status = "enabled" if response.enabled else "disabled"
state = "permanently" if response.permanent else "temporarily"
print(f"Supported and {status} {state}")
elif response.failed:
print("Terminal does not support DEC mode queries")
else:
print("Mode not supported by this terminal")
jquast-blessed-864a8f7/bin/dec_modes_simple.py 0000664 0000000 0000000 00000000410 15107137110 0021415 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
print("Watch the cursor disappear, ")
with term.dec_modes_disabled(term.DecPrivateMode.DECTCEM):
print("Cursor is hidden - working...")
term.inkey(2)
print()
print("Cursor is back!")
jquast-blessed-864a8f7/bin/dec_modes_synchronized.py 0000664 0000000 0000000 00000001145 15107137110 0022651 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
fill = "█" * term.height * term.width
empty = " " * term.height * term.width
print(term.bold_red("Warning! Screen may blink rapidly!"))
print()
print("Press return to continue, 'q' to stop test")
term.inkey()
with term.fullscreen():
for step in range(300):
with term.synchronized_output():
print(term.home + empty, flush=True)
print(term.home + fill, flush=True)
print(term.home + f'step={step}')
if term.inkey(0.01) == 'q':
break
print(term.clear + "Test complete!")
jquast-blessed-864a8f7/bin/detect-multibyte.py 0000775 0000000 0000000 00000005421 15107137110 0021420 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Determines whether the attached terminal supports multibyte encodings.
Problem: A screen drawing application wants to detect whether the terminal
client is capable of rendering utf-8. Some transports, such as a serial link,
often cannot forward their ``LANG`` environment preference, or protocols such
as telnet and rlogin often assume mutual agreement by manual configuration.
We can interactively determine whether the connecting terminal emulator is
rendering in utf8 by making an inquiry of their cursor position:
- request cursor position (p0).
- display multibyte character.
- request cursor position (p1).
If the horizontal distance of (p0, p1) is 1 cell, we know the connecting
client is certainly matching our intended encoding.
As a (tough!) exercise, it may be possible to use this technique to accurately
determine the remote encoding without protocol negotiation using cursor
positioning alone through a complex state decision tree, as demonstrated
by the following diagram:
.. image:: _static/soulburner-ru-family-encodings.jpg
:alt: Cyrillic encodings flowchart
"""
# pylint: disable=invalid-name
# Invalid module name "detect-multibyte"
# std imports
import sys
import collections
# local
from blessed import Terminal
def get_pos(term):
"""Get cursor position, calling os.exit(2) if not determined."""
# pylint: disable=invalid-name
# Invalid variable name "Position"
Position = collections.namedtuple('Position', ('row', 'column'))
pos = Position(*term.get_location())
if -1 in pos:
print('stdin: not a human', file=sys.stderr)
exit(2)
return pos
def main():
"""Program entry point."""
term = Terminal()
# move to bottom of screen, temporarily, where we're likely to do
# the least damage, as we are performing something of a "destructive
# write and erase" onto this screen location.
with term.cbreak(), term.location(y=term.height - 1, x=0):
# store first position
pos0 = get_pos(term)
# display multibyte character
print('⦰', end='')
# store second position
pos1 = get_pos(term)
# determine distance
horizontal_distance = pos1.column - pos0.column
multibyte_capable = horizontal_distance == 1
# rubout character(s)
print('\b \b' * horizontal_distance, end='')
# returned to our original starting position,
if not multibyte_capable:
print(f'multibyte encoding failed, horizontal distance is {horizontal_distance}, '
'expected 1 for unicode point https://codepoints.net/U+29B0',
file=sys.stderr)
exit(1)
print(f"{term.bold_green('✓')} multibyte encoding supported!")
if __name__ == '__main__':
exit(main())
jquast-blessed-864a8f7/bin/display-fpathconf.py 0000775 0000000 0000000 00000004017 15107137110 0021547 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""Displays os.fpathconf values related to terminals."""
# pylint: disable=invalid-name
# Invalid module name "display-sighandlers"
# std imports
import os
import sys
def display_fpathconf():
"""Program entry point."""
if not hasattr(os, "pathconf_names"):
return
disp_values = (
('PC_MAX_CANON', ('Max no. of bytes in a '
'terminal canonical input line.')),
('PC_MAX_INPUT', ('Max no. of bytes for which '
'space is available in a terminal input queue.')),
('PC_PIPE_BUF', ('Max no. of bytes which will '
'be written atomically to a pipe.')),
# to explain in more detail: PC_VDISABLE is the reference character in
# the pairing output for bin/display-terminalinfo.py: if the value
# matches (\xff), then that special control character is disabled, fe:
#
# Index Name Special Character Default Value
# VEOF EOF ^D
# VEOL EOL _POSIX_VDISABLE
#
# regardless, this value is almost always \xff.
('PC_VDISABLE', 'Terminal character disabling value.')
)
fmt = '{name:<13} {value:<10} {description:<11}'
# column header
print(fmt.format(name='name', value='value', description='description'))
print(fmt.replace('<', '-<').format(name='-', value='-', description='-'))
fd = sys.stdin.fileno()
for name, description in disp_values:
key = os.pathconf_names.get(name, None)
if key is None:
value = 'UNDEF'
else:
try:
value = os.fpathconf(fd, name)
if name == 'PC_VDISABLE':
value = fr'\x{value:02x}'
except OSError as err:
value = f'OSErrno {err.errno}'
print(fmt.format(name=name, value=value, description=description))
print()
if __name__ == '__main__':
display_fpathconf()
jquast-blessed-864a8f7/bin/display-modes.py 0000775 0000000 0000000 00000012162 15107137110 0020706 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Display all supported DEC Private Modes and Device Attributes for the current terminal.
This utility queries the terminal for support of various DEC Private Modes and Device Attributes,
displaying the results in a formatted table.
Unsupported modes are not displayed.
"""
# std imports
import sys
# local
from blessed import Terminal
def display_device_attributes(term):
"""Query and display Device Attributes (DA1) information."""
print(term.bold("Device Attributes (DA1):"))
print("-" * 40)
# Query device attributes
da = term.get_device_attributes()
if da is None:
print(" " + term.bright_red("No response - terminal does NOT support DA1 queries"))
return
# Display service class
print(f" Service Class: {term.bright_cyan(str(da.service_class))}")
# Display extensions
if da.extensions:
print(f" Extensions: {term.bright_yellow(', '.join(map(str, sorted(da.extensions))))}")
# Describe notable extensions, we don't do this inside blessed itself
# because I don't think any of this stuff other than sixel matters
# anymore.
extension_desc = {
1: "132 columns",
2: "Printer port",
4: "Sixel graphics",
6: "Selective erase",
7: "DRCS (soft character set)",
8: "UDK (user-defined keys)",
9: "NRCS (national replacement character sets)",
12: "SCS extension (Serbian/Croatian/Slovakian)",
15: "Technical character set",
18: "Windowing capability",
21: "Horizontal scrolling",
23: "Greek extension",
24: "Turkish extension",
42: "ISO Latin-2 character set",
44: "PCTerm",
45: "Soft key map",
46: "ASCII emulation"
}
print(" Extension details:")
for ext in sorted(da.extensions):
desc = extension_desc.get(ext, "Unknown extension")
if ext == 4: # Highlight sixel
print(f" {term.bright_green(str(ext))}: {desc}")
else:
print(f" {str(ext)}: {desc}")
else:
print(" Extensions: None reported")
# Specifically highlight sixel support
sixel_status = term.bright_green("YES") if da.supports_sixel else term.bright_red("NO")
print(f" Sixel Graphics Support: {sixel_status}")
def display_dec_modes(term):
"""Query and display DEC Private Mode information."""
print(term.bold("DEC Private Modes:"))
print("-" * 40)
# Get all available DEC Private Modes
all_modes = {
k: getattr(Terminal.DecPrivateMode, k)
for k in dir(Terminal.DecPrivateMode)
if k.isupper() and not k.startswith('_')
}
supported_modes = {}
force_mode = '--force' in sys.argv
# Query each mode
for idx, (mode_name, mode_code) in enumerate(sorted(all_modes.items(), key=lambda x: x[1])):
print(f' Testing {mode_name}...' + term.clear_eol, end='\r', flush=True)
response = term.get_dec_mode(mode_code, force=force_mode)
if response.supported:
supported_modes[mode_name] = response
# Clear the testing line
print(term.move_x(0) + term.clear_eol, end='', flush=True)
if not supported_modes:
print(term.bright_red("DEC Private Mode not supported"))
return
# Display supported modes in a table
print(f"{len(supported_modes)} supported modes:")
print()
for mode_name, response in sorted(supported_modes.items(), key=lambda x: x[1].mode.value):
# Status with color coding
if response.enabled:
status = term.bright_green("Enabled")
else:
status = term.bright_red("Disabled")
# Permanence indicator
permanence = term.bold("permanently") if response.permanent else "temporarily"
# Mode info
mode_info = f"Mode {response.mode.value}"
print(f"{mode_info:<15} {status} {permanence}")
print(f"└─ {response.mode.long_description}")
print()
def main():
"""Main program entry point."""
term = Terminal()
print(term.home + term.clear)
print()
print(term.bold("Terminal Capability Report"))
print()
_kind = term.bright_cyan(term.kind or 'unknown')
print(f"Terminal.kind: {_kind}")
_yes = term.bright_green('YES')
_no = term.bright_red('NO')
print(f" .is_a_tty: {_yes if term.is_a_tty else _no}")
print(f" .does_styling: {_yes if term.does_styling else _no}")
print(f" .does_sixel: {_yes if term.does_sixel() else _no}")
_24bit = term.bright_green('24-bit')
_no_colors = term.bright_red(str(term.number_of_colors))
print(f" .number_of_colors: {_24bit if term.number_of_colors == 1 << 24 else _no_colors}")
print()
# Display Device Attributes
try:
display_device_attributes(term)
print()
except Exception as e:
print(f"Error querying device attributes: {e}")
print()
# Display DEC Private Modes
try:
display_dec_modes(term)
except Exception as e:
print(f"Error querying DEC modes: {e}")
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/display-sighandlers.py 0000775 0000000 0000000 00000002111 15107137110 0022073 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""Displays all signals, their values, and their handlers to stdout."""
# pylint: disable=invalid-name
# Invalid module name "display-sighandlers"
# std imports
import signal
def main():
"""Program entry point."""
fmt = '{name:<10} {value:<5} {description}'
# header
print(fmt.format(name='name', value='value', description='description'))
print('-' * (33))
for name, value in [(signal_name, getattr(signal, signal_name))
for signal_name in dir(signal)
if signal_name.startswith('SIG')
and not signal_name.startswith('SIG_')]:
try:
handler = signal.getsignal(value)
except ValueError:
# FreeBSD: signal number out of range
handler = 'out of range'
description = {
signal.SIG_IGN: "ignored(SIG_IGN)",
signal.SIG_DFL: "default(SIG_DFL)"
}.get(handler, handler)
print(fmt.format(name=name, value=value, description=description))
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/display-terminalinfo.py 0000775 0000000 0000000 00000015742 15107137110 0022275 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""Display known information about our terminal."""
# pylint: disable=invalid-name
# Invalid module name "display-terminalinfo"
# std imports
import os
import sys
import locale
import platform
BITMAP_IFLAG = {
'IGNBRK': 'ignore BREAK condition',
'BRKINT': 'map BREAK to SIGINTR',
'IGNPAR': 'ignore (discard) parity errors',
'PARMRK': 'mark parity and framing errors',
'INPCK': 'enable checking of parity errors',
'ISTRIP': 'strip 8th bit off chars',
'INLCR': 'map NL into CR',
'IGNCR': 'ignore CR',
'ICRNL': 'map CR to NL (ala CRMOD)',
'IXON': 'enable output flow control',
'IXOFF': 'enable input flow control',
'IXANY': 'any char will restart after stop',
'IMAXBEL': 'ring bell on input queue full',
'IUCLC': 'translate upper case to lower case',
}
BITMAP_OFLAG = {
'OPOST': 'enable following output processing',
'ONLCR': 'map NL to CR-NL (ala CRMOD)',
'OXTABS': 'expand tabs to spaces',
'ONOEOT': 'discard EOT\'s `^D\' on output)',
'OCRNL': 'map CR to NL',
'OLCUC': 'translate lower case to upper case',
'ONOCR': 'No CR output at column 0',
'ONLRET': 'NL performs CR function',
}
BITMAP_CFLAG = {
'CSIZE': 'character size mask',
'CS5': '5 bits (pseudo)',
'CS6': '6 bits',
'CS7': '7 bits',
'CS8': '8 bits',
'CSTOPB': 'send 2 stop bits',
'CREAD': 'enable receiver',
'PARENB': 'parity enable',
'PARODD': 'odd parity, else even',
'HUPCL': 'hang up on last close',
'CLOCAL': 'ignore modem status lines',
'CCTS_OFLOW': 'CTS flow control of output',
'CRTSCTS': 'same as CCTS_OFLOW',
'CRTS_IFLOW': 'RTS flow control of input',
'MDMBUF': 'flow control output via Carrier',
}
BITMAP_LFLAG = {
'ECHOKE': 'visual erase for line kill',
'ECHOE': 'visually erase chars',
'ECHO': 'enable echoing',
'ECHONL': 'echo NL even if ECHO is off',
'ECHOPRT': 'visual erase mode for hardcopy',
'ECHOCTL': 'echo control chars as ^(Char)',
'ISIG': 'enable signals INTR, QUIT, [D]SUSP',
'ICANON': 'canonicalize input lines',
'ALTWERASE': 'use alternate WERASE algorithm',
'IEXTEN': 'enable DISCARD and LNEXT',
'EXTPROC': 'external processing',
'TOSTOP': 'stop background jobs from output',
'FLUSHO': 'output being flushed (state)',
'NOKERNINFO': 'no kernel output from VSTATUS',
'PENDIN': 'XXX retype pending input (state)',
'NOFLSH': 'don\'t flush after interrupt',
}
CTLCHAR_INDEX = {
'VEOF': 'EOF',
'VEOL': 'EOL',
'VEOL2': 'EOL2',
'VERASE': 'ERASE',
'VWERASE': 'WERASE',
'VKILL': 'KILL',
'VREPRINT': 'REPRINT',
'VINTR': 'INTR',
'VQUIT': 'QUIT',
'VSUSP': 'SUSP',
'VDSUSP': 'DSUSP',
'VSTART': 'START',
'VSTOP': 'STOP',
'VLNEXT': 'LNEXT',
'VDISCARD': 'DISCARD',
'VMIN': '---',
'VTIME': '---',
'VSTATUS': 'STATUS',
}
def display_bitmask(kind, bitmap, value):
"""Display all matching bitmask values for ``value`` given ``bitmap``."""
import termios
col1_width = max(map(len, list(bitmap.keys()) + [kind]))
col2_width = 7
fmt = '{name:>{col1_width}} {value:>{col2_width}} {description}'
print(fmt.format(name=kind,
value='Value',
description='Description',
col1_width=col1_width,
col2_width=col2_width))
print(f"{'-' * col1_width} {'-' * col2_width} "
f"{'-' * max(map(len, bitmap.values()))}")
for flag_name, description in bitmap.items():
try:
bitmask = getattr(termios, flag_name)
bit_val = 'on' if bool(value & bitmask) else 'off'
except AttributeError:
bit_val = 'undef'
print(fmt.format(name=flag_name,
value=bit_val,
description=description,
col1_width=col1_width,
col2_width=col2_width))
print()
def display_ctl_chars(index, ctlc):
"""Display all control character indices, names, and values."""
import termios
title = 'Special Character'
col1_width = len(title)
col2_width = max(map(len, index.values()))
fmt = '{idx:<{col1_width}} {name:<{col2_width}} {value}'
print('Special line Characters'.center(40).rstrip())
print(fmt.format(idx='Index',
name='Name',
value='Value',
col1_width=col1_width,
col2_width=col2_width))
print(f"{'-' * col1_width} {'-' * col2_width} {'-' * 10}")
for index_name, name in index.items():
try:
index = getattr(termios, index_name)
value = ctlc[index]
value = '_POSIX_VDISABLE' if value == b'\xff' else repr(value)
except AttributeError:
value = 'undef'
print(fmt.format(idx=index_name,
name=name,
value=value,
col1_width=col1_width,
col2_width=col2_width))
print()
def display_pathconf(names, getter):
"""Helper displays results of os.pathconf_names values."""
col1_width = max(map(len, names))
fmt = '{name:>{col1_width}} {value}'
print(fmt.format(name='pathconf'.ljust(col1_width), value='value',
col1_width=col1_width))
print(f"{'-' * col1_width} {'-' * 27}")
for name in names:
try:
value = getter(name)
except OSError as err:
value = f'OSErrno {err.errno}'
print(fmt.format(name=name, value=value, col1_width=col1_width))
print()
def main():
"""Program entry point."""
if platform.system() == 'Windows':
print('No terminal on windows systems!')
exit(0)
import termios
fd = sys.stdin.fileno()
locale.setlocale(locale.LC_ALL, '')
encoding = locale.getpreferredencoding()
print(f'os.isatty({fd}) => {os.isatty(fd)}')
print(f'locale.getpreferredencoding() => {encoding}')
display_pathconf(names=os.pathconf_names,
getter=lambda name: os.fpathconf(fd, name))
try:
(iflag, oflag, cflag, lflag,
_, _, # input / output speed (bps macros)
ctlc) = termios.tcgetattr(fd)
except termios.error as err:
print(f'stdin is not a typewriter: {err}')
else:
display_bitmask(kind=' Input Mode',
bitmap=BITMAP_IFLAG,
value=iflag)
display_bitmask(kind=' Output Mode',
bitmap=BITMAP_OFLAG,
value=oflag)
display_bitmask(kind='Control Mode',
bitmap=BITMAP_CFLAG,
value=cflag)
display_bitmask(kind=' Local Mode',
bitmap=BITMAP_LFLAG,
value=lflag)
display_ctl_chars(index=CTLCHAR_INDEX,
ctlc=ctlc)
print(f'os.ttyname({fd}) => {os.ttyname(fd)}')
print(f'os.ctermid() => {os.ttyname(fd)}')
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/display-version.py 0000775 0000000 0000000 00000000652 15107137110 0021265 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from blessed import Terminal
term = Terminal()
print('Checking software version (XTVERSION) ...', end='', flush=True)
sv = term.get_software_version()
if sv is None:
print('No response.')
print(term.bright_red('This terminal does NOT support XTVERSION.'))
else:
print()
maybe_version = f', version {sv.version}' if sv.version else ''
print(f'Terminal: {sv.name}{maybe_version}')
jquast-blessed-864a8f7/bin/editor.py 0000775 0000000 0000000 00000020145 15107137110 0017422 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
A Dumb full-screen editor.
This example program makes use of many context manager methods:
:meth:`~.Terminal.hidden_cursor`, :meth:`~.Terminal.raw`,
:meth:`~.Terminal.location`, :meth:`~.Terminal.fullscreen`, and
:meth:`~.Terminal.keypad`.
Early curses work focused namely around writing screen editors, naturally
any serious editor would make liberal use of special modes.
Actions:
- ``Ctrl - L`` refresh
- ``F2`` quit
- ``F1`` save
- ``LEFT MOUSE BUTTON`` move cursor
"""
# std imports
import collections
# local
from blessed import Terminal
def echo(text):
"""Display ``text`` and flush output."""
print(text, end='', flush=True)
def input_filter(keystroke):
"""
For given keystroke, return whether it should be allowed as input.
This somewhat requires that the interface use special application keys to perform functions, as
alphanumeric input intended for persisting could otherwise be interpreted as a command sequence.
"""
if keystroke.is_sequence:
# Namely, deny multi-byte sequences (such as '\x1b[A'),
return False
if ord(keystroke) < ord(' '):
# or control characters (such as ^L),
return False
return True
def echo_yx(cursor, text):
"""Move to ``cursor`` and display ``text``."""
echo(cursor.term.move_yx(cursor.y, cursor.x) + text)
Cursor = collections.namedtuple('Cursor', ('y', 'x', 'term'))
def readline(term, width=20):
"""A rudimentary readline implementation."""
text = ''
while True:
inp = term.inkey()
if inp.code == term.KEY_ENTER:
break
elif inp.code == term.KEY_ESCAPE or inp == chr(3):
text = None
break
elif not inp.is_sequence and len(text) < width:
text += inp
echo(inp)
elif inp.code in (term.KEY_BACKSPACE, term.KEY_DELETE):
text = text[:-1]
# https://utcc.utoronto.ca/~cks/space/blog/unix/HowUnixBackspaces
#
# "When you hit backspace, the kernel tty line discipline rubs out
# your previous character by printing (in the simple case)
# Ctrl-H, a space, and then another Ctrl-H."
echo('\b \b')
return text
def save(screen, fname):
"""Save screen contents to file."""
if not fname:
return
with open(fname, 'w') as fout:
cur_row = cur_col = 0
for (row, col) in sorted(screen):
char = screen[(row, col)]
while row != cur_row:
cur_row += 1
cur_col = 0
fout.write('\n')
while col > cur_col:
cur_col += 1
fout.write(' ')
fout.write(char)
cur_col += 1
fout.write('\n')
def redraw(term, screen, start=None, end=None):
"""Redraw the screen."""
if start is None and end is None:
echo(term.clear)
start, end = (Cursor(y=min(y for (y, x) in screen or [(0, 0)]),
x=min(x for (y, x) in screen or [(0, 0)]),
term=term),
Cursor(y=max(y for (y, x) in screen or [(0, 0)]),
x=max(x for (y, x) in screen or [(0, 0)]),
term=term))
lastcol, lastrow = -1, -1
for row, col in sorted(screen):
if start.y <= row <= end.y and start.x <= col <= end.x:
if col >= term.width or row >= term.height:
# out of bounds
continue
if row != lastrow or col != lastcol + 1:
# use cursor movement
echo_yx(Cursor(row, col, term), screen[row, col])
else:
# just write past last one
echo(screen[row, col])
def main():
"""Program entry point."""
def above(csr, offset):
return Cursor(y=max(0, csr.y - offset),
x=csr.x,
term=csr.term)
def below(csr, offset):
return Cursor(y=min(csr.term.height - 1, csr.y + offset),
x=csr.x,
term=csr.term)
def right_of(csr, offset):
return Cursor(y=csr.y,
x=min(csr.term.width - 1, csr.x + offset),
term=csr.term)
def left_of(csr, offset):
return Cursor(y=csr.y,
x=max(0, csr.x - offset),
term=csr.term)
def home(csr):
return Cursor(y=csr.y,
x=0,
term=csr.term)
def end(csr):
return Cursor(y=csr.y,
x=csr.term.width - 1,
term=csr.term)
def bottom(csr):
return Cursor(y=csr.term.height - 1,
x=csr.x,
term=csr.term)
def center(csr):
return Cursor(csr.term.height // 2,
csr.term.width // 2,
csr.term)
def lookup_move(inp_code, csr):
return {
# arrows, including angled directionals
csr.term.KEY_END: below(left_of(csr, 1), 1),
csr.term.KEY_KP_1: below(left_of(csr, 1), 1),
csr.term.KEY_DOWN: below(csr, 1),
csr.term.KEY_KP_2: below(csr, 1),
csr.term.KEY_PGDOWN: below(right_of(csr, 1), 1),
csr.term.KEY_LR: below(right_of(csr, 1), 1),
csr.term.KEY_KP_3: below(right_of(csr, 1), 1),
csr.term.KEY_LEFT: left_of(csr, 1),
csr.term.KEY_KP_4: left_of(csr, 1),
csr.term.KEY_CENTER: center(csr),
csr.term.KEY_KP_5: center(csr),
csr.term.KEY_RIGHT: right_of(csr, 1),
csr.term.KEY_KP_6: right_of(csr, 1),
csr.term.KEY_HOME: above(left_of(csr, 1), 1),
csr.term.KEY_KP_7: above(left_of(csr, 1), 1),
csr.term.KEY_UP: above(csr, 1),
csr.term.KEY_KP_8: above(csr, 1),
csr.term.KEY_PGUP: above(right_of(csr, 1), 1),
csr.term.KEY_KP_9: above(right_of(csr, 1), 1),
# shift + arrows
csr.term.KEY_SLEFT: left_of(csr, 10),
csr.term.KEY_SRIGHT: right_of(csr, 10),
csr.term.KEY_SDOWN: below(csr, 10),
csr.term.KEY_SUP: above(csr, 10),
# carriage return
csr.term.KEY_ENTER: home(below(csr, 1)),
}.get(inp_code, csr)
term = Terminal()
csr = Cursor(0, 0, term)
screen = {}
with term.hidden_cursor(), \
term.raw(), \
term.location(), \
term.fullscreen(), \
term.keypad(), \
term.mouse_enabled():
inp = None
while True:
echo_yx(csr, term.reverse(screen.get((csr.y, csr.x), ' ')))
inp = term.inkey()
if inp.name == 'KEY_F2':
break
elif inp.name == 'KEY_F1':
# ^s saves
echo_yx(home(bottom(csr)),
term.ljust(term.bold_white('Filename: ')))
echo_yx(right_of(home(bottom(csr)), len('Filename: ')), '')
save(screen, readline(term))
echo_yx(home(bottom(csr)), term.clear_eol)
redraw(term=term, screen=screen,
start=home(bottom(csr)),
end=end(bottom(csr)))
continue
elif inp == chr(12):
# ^l refreshes
redraw(term=term, screen=screen)
elif inp.is_mouse_left():
# Handle left mouse button press
csr = Cursor(inp.y - 1, inp.x - 1, term)
continue
else:
n_csr = lookup_move(inp.code, csr)
if n_csr != csr:
# erase old cursor,
echo_yx(csr, screen.get((csr.y, csr.x), ' '))
csr = n_csr
elif input_filter(inp):
echo_yx(csr, inp)
screen[(csr.y, csr.x)] = inp.__str__()
n_csr = right_of(csr, 1)
if n_csr == csr:
# wrap around margin
n_csr = home(below(csr, 1))
csr = n_csr
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/generate-keycodes.py 0000664 0000000 0000000 00000003123 15107137110 0021524 0 ustar 00root root 0000000 0000000 # generate keycodes for the tables in docs/keyboard.rst
# std imports
import os
# local
from blessed.keyboard import DEFAULT_SEQUENCE_MIXIN, CURSES_KEYCODE_OVERRIDE_MIXIN
def is_override(key_attr_name, code):
return (code in [val for name, val in CURSES_KEYCODE_OVERRIDE_MIXIN] and
key_attr_name not in [name for name, val in CURSES_KEYCODE_OVERRIDE_MIXIN])
def main():
from blessed import Terminal
term = Terminal()
csv_header = """
.. csv-table:: All Terminal class attribute Keyboard codes, by name
:delim: |
:header: "Name"| "Value"| "Example Sequence(s)"
"""
fname = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, 'docs', 'all_the_keys.txt'))
with open(fname, 'w') as fout:
print(f"write: {fout.name}")
fout.write(csv_header)
for key_attr_name in sorted([
attr for attr in dir(term) if attr.startswith('KEY_')
]):
# filter away F23-F63 (lol)
if key_attr_name.startswith('KEY_F'):
maybe_digit = key_attr_name[len('KEY_F'):]
if maybe_digit.isdigit() and int(maybe_digit) > 23:
continue
code = getattr(term, key_attr_name)
repr_sequences = [repr(seq) for (seq, value) in DEFAULT_SEQUENCE_MIXIN if value == code]
txt_sequences = ', '.join(repr_sequences).replace('\\', '\\\\')
fout.write(f' {key_attr_name} | {code}')
if txt_sequences:
fout.write(f'| {txt_sequences}')
fout.write('\n')
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/generate-x11-colorchart.py 0000664 0000000 0000000 00000005116 15107137110 0022471 0 ustar 00root root 0000000 0000000 # generate images and tables for inclusion in docs/colors.rst
# std imports
import os
import re
import math
import colorsys
from functools import reduce
# 3rd party
from PIL import Image
# local
from blessed.colorspace import X11_COLORNAMES_TO_RGB
rgb_folder = os.path.abspath(
os.path.join(os.path.dirname(__file__), os.pardir, 'docs', '_static', 'rgb'))
color_alias_fmt = """
.. |{color_name}| image:: _static/rgb/{color_name}.png
:width: 48pt
:height: 12pt"""
csv_table = """.. csv-table:: All Terminal colors, by name
:header: "Name", "Image", "R", "G", "B", "H", "S", "V"
:name: Color chart
"""
def sort_colors():
colors = {}
for color_name, rgb_color in X11_COLORNAMES_TO_RGB.items():
if rgb_color in colors:
colors[rgb_color].append(color_name)
else:
colors[rgb_color] = [color_name]
def sortby_hv(rgb_item):
# sort by hue rounded to nearest %,
# then by color name & number
# except shades of grey -- by name & number, only
rgb, name = rgb_item
digit = 0
match = re.match(r'(.*)(\d+)', name[0])
if match is not None:
name = match.group(1)
digit = int(match.group(2))
else:
name = name[0]
hash_name = reduce(int.__mul__, map(ord, name))
hsv = colorsys.rgb_to_hsv(*rgb)
if rgb[0] == rgb[1] == rgb[2]:
return 100, hsv[2], hash_name, digit
return int(math.floor(hsv[0] * 100)), hash_name, digit, hsv[2]
return sorted(colors.items(), key=sortby_hv)
def main():
aliases, csv_rows = '', ''
for rgb, x11_colors in sort_colors():
x11_color = sorted(x11_colors)[0]
fname = os.path.join(rgb_folder, f'{x11_color}.png')
if not os.path.exists(os.path.join(fname)):
img = Image.new('RGB', (1, 1), color=rgb)
img.save(fname)
aliases += color_alias_fmt.format(color_name=x11_color)
hsv = colorsys.rgb_to_hsv(*rgb)
csv_rows += (' '
f'{x11_color}, |{x11_color}|, '
f'{rgb[0] / 255:0.1%}, {rgb[1] / 255:0.1%}, {rgb[2] / 255:0.1%}, '
f'{hsv[0]:0.1%}, {hsv[1]:0.1%}, {hsv[2] / 255:0.1%}\n')
output = aliases + '\n\n' + csv_table + '\n' + csv_rows
filepath_txt = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, 'docs',
'all_the_colors.txt'))
with open(filepath_txt, 'w') as fout:
print(f'write: {fout.name}')
fout.write(output.lstrip())
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/keyboard_animation.py 0000775 0000000 0000000 00000000643 15107137110 0021774 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
print("Cross animation, press any key to stop: ", end="", flush=True)
with term.cbreak(), term.hidden_cursor():
cross = '|'
while True:
key = term.inkey(timeout=0.1)
if key:
print(f'STOP by {key!r}')
break
cross = {'|': '-', '-': '|'}[cross]
print(f'{cross}\b', end='', flush=True)
jquast-blessed-864a8f7/bin/keyboard_arrow_paint.py 0000775 0000000 0000000 00000001557 15107137110 0022347 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
header_msg = "Press arrow keys (or 'q' to quit): "
term = Terminal()
position = [term.height // 2, term.width // 2]
with term.cbreak(), term.fullscreen(), term.hidden_cursor():
print(term.home + header_msg + term.clear_eos)
while True:
# show arrow-controlled block
print(term.move_yx(*position) + '█', end='', flush=True)
# get key,
key = term.inkey()
# take action,
if key == 'q':
break
if key.name == 'KEY_UP':
position[0] = max(0, position[0] - 1)
elif key.name == 'KEY_LEFT':
position[1] = max(0, position[1] - 1)
elif key.name == 'KEY_DOWN':
position[0] = min(term.height, position[0] + 1)
elif key.name == 'KEY_RIGHT':
position[1] = min(term.width, position[1] + 1)
jquast-blessed-864a8f7/bin/keyboard_kitty_simple.py 0000664 0000000 0000000 00000001202 15107137110 0022517 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
print("Press and hold keys to see raw kitty keystrokes and their names (press 'q' to quit)")
with term.enable_kitty_keyboard(report_events=True):
with term.cbreak():
while True:
key = term.inkey()
if key.pressed:
print(f"Key {key.name} pressed, value={key.value}, sequence={key!r}")
if key == 'q':
break
elif key.repeated:
print(f"Key repeating, sequence={key!r}")
elif key.released:
print(f"Key released, sequence={key!r}")
jquast-blessed-864a8f7/bin/keyboard_magic_methods.py 0000775 0000000 0000000 00000000702 15107137110 0022614 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
print("Press 'q' or F10 to exit! Press F1 for help")
with term.cbreak():
while True:
key = term.inkey()
# Check for specific character with modifier
if key.is_ctrl('q') or key.is_f10():
print(f"Exit by key named {key.name}")
break
# Check for function key
elif key.is_f1():
print("* don't panic")
jquast-blessed-864a8f7/bin/keyboard_simple.py 0000775 0000000 0000000 00000000225 15107137110 0021302 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
with term.cbreak():
key = term.inkey()
print(f"You pressed: {key!r}")
jquast-blessed-864a8f7/bin/keyboard_special_keys.py 0000775 0000000 0000000 00000000364 15107137110 0022470 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
with term.cbreak():
key = term.inkey()
if key.is_sequence:
print(f"Special key: {key.name} ({key!r})")
else:
print(f"Regular character: {key}")
jquast-blessed-864a8f7/bin/keymatrix.py 0000775 0000000 0000000 00000037634 15107137110 0020164 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Advanced keyboard and special modes interaction example.
Usage:
- F1-F11: Toggle DEC private modes (bracketed paste, mouse, etc.)
- Shift+F1-F5: Toggle Kitty keyboard protocol flags
- 'q': Exit
All modes that elicit responses are activated for demonstration.
"""
# std imports
import sys
from typing import Dict, List, Tuple, Optional, Any
import functools
from collections import deque
# local
from blessed import Terminal
# For convenience in type hints and initialization
DecPrivateMode = Terminal.DecPrivateMode
class DecModeManager:
"""Manages DEC Private Mode probing, tracking, and toggling."""
def __init__(self, term: Terminal, test_modes: Tuple[DecPrivateMode, ...]):
self.term = term
self.test_modes = test_modes
self.available_modes: Dict[DecPrivateMode, bool] = {}
self.active_contexts: Dict[DecPrivateMode, Any] = {}
def probe(self) -> List[str]:
"""Probe terminal for DEC mode support and return log messages."""
messages = ["Checking DEC Private Mode status:"]
for mode in self.test_modes:
mode = DecPrivateMode(mode)
response = self.term.get_dec_mode(mode)
if not response.supported:
messages.append(f'{mode}: no support')
continue
status = "enabled" if response.enabled else "disabled"
if response.permanent:
messages.append(f'{mode}: permanent, enabled={response.enabled}')
continue
messages.append(f'{mode}: {status}')
self.available_modes[mode] = response.enabled
if not self.available_modes:
messages.append("All DEC Private Modes fail support")
return messages
def entries(self) -> List[Tuple[DecPrivateMode, bool]]:
"""Return list of (mode, enabled) pairs for display."""
return [(mode, enabled) for mode, enabled in self.available_modes.items()]
def toggle_by_index(self, f_key_idx: int) -> str:
"""Toggle DEC mode by F-key index and return log message."""
if f_key_idx >= len(self.test_modes):
return ""
mode = self.test_modes[f_key_idx]
if mode not in self.available_modes:
return ""
old_enabled = self.available_modes[mode]
new_enabled = not old_enabled
self.available_modes[mode] = new_enabled
try:
if new_enabled and mode not in self.active_contexts:
cm = self.term.dec_modes_enabled(mode)
cm.__enter__()
self.active_contexts[mode] = cm
return f'Enabled {mode}'
elif not new_enabled and mode in self.active_contexts:
cm = self.active_contexts.pop(mode)
cm.__exit__(None, None, None)
return f'Disabled {mode}'
except Exception as e:
self.available_modes[mode] = old_enabled
return f'Failed to toggle {mode}: {e}'
return ""
def toggle_keynames(self) -> List[str]:
"""Return list of key names that toggle DEC modes."""
return [f'KEY_F{i}' for i in range(1, 12)]
def get_index_by_key(self, key_name: str) -> int:
"""Convert key name to toggle index."""
f_num = int(key_name.split('_')[-1][1:])
return f_num - 1
def cleanup(self) -> None:
"""Clean up all active context managers."""
for cm in self.active_contexts.values():
try:
cm.__exit__(None, None, None)
except BaseException:
pass
class KittyKeyboardManager:
"""Manages Kitty keyboard protocol probing and toggling."""
def __init__(self, term: Terminal):
self.term = term
self.kitty_flags: Optional[Any] = None
self.active_context: Optional[Any] = None
self.flag_masks = [1, 2, 4, 8, 16]
def probe(self) -> Tuple[bool, Optional[str], Optional[str]]:
"""Probe kitty keyboard support."""
self.kitty_flags = self.term.get_kitty_keyboard_state()
if self.kitty_flags is None:
return ["Kitty Keyboard Protocol not supported!"]
return [f'Kitty Keyboard Protocol is {self.kitty_flags!r}']
def toggle_by_index(self, shift_f_idx: int) -> str:
"""Toggle kitty flag by Shift+F index and return log message."""
if self.kitty_flags is None or shift_f_idx >= len(self.flag_masks):
return ""
mask = self.flag_masks[shift_f_idx]
self.kitty_flags.value ^= mask
try:
if self.active_context is not None:
self.active_context.__exit__(None, None, None)
self.active_context = None
args = self.kitty_flags.make_arguments()
if any(args.values()):
self.active_context = self.term.enable_kitty_keyboard(**args)
self.active_context.__enter__()
return f'Kitty: {self.kitty_flags!r}'
else:
return 'Kitty: disabled'
except Exception as e:
return f'Kitty error: {e}'
def header_msg(self) -> str:
return f"{self.repr_flags()} [Shift+F1..F5] to toggle"
def repr_flags(self) -> str:
"""Return string representation of current flags."""
return f"{self.kitty_flags!r}" if self.kitty_flags else ""
def toggle_keynames(self) -> List[str]:
"""Return list of key names that toggle Kitty keyboard flags."""
return [f'KEY_SHIFT_F{i}' for i in range(1, 6)]
def get_index_by_key(self, key_name: str) -> int:
"""Convert key name to toggle index."""
f_num = int(key_name.split('_')[-1][1:])
return f_num - 1
def cleanup(self) -> None:
"""Clean up active context manager."""
if self.active_context is not None:
try:
self.active_context.__exit__(None, None, None)
except BaseException:
pass
class MouseModeManager:
"""Manages mouse mode probing and toggling."""
def __init__(self, term: Terminal):
self.term = term
self.supported: bool = False
self.active_context: Optional[Any] = None
self.report_drag: bool = False
self.report_motion: bool = False
self.report_pixels: bool = False
self.mode_names = ['drag', 'motion', 'pixels']
def probe(self) -> List[str]:
"""Probe terminal for mouse support and return log messages."""
self.supported = self.term.does_mouse()
if self.supported:
return ["Mouse support detected!"]
return ["Mouse support not available!"]
def toggle_by_index(self, f_idx: int) -> str:
"""Toggle mouse mode by F-key index and return log message."""
if not self.supported or f_idx >= len(self.mode_names):
return ""
mode_name = self.mode_names[f_idx]
# Toggle the flag
if mode_name == 'drag':
self.report_drag = not self.report_drag
elif mode_name == 'motion':
self.report_motion = not self.report_motion
elif mode_name == 'pixels':
self.report_pixels = not self.report_pixels
# Clean up old context
if self.active_context is not None:
self.active_context.__exit__(None, None, None)
self.active_context = None
# Create new context if any mode is enabled
if self.report_drag or self.report_motion or self.report_pixels:
self.active_context = self.term.mouse_enabled(
report_drag=self.report_drag,
report_motion=self.report_motion,
report_pixels=self.report_pixels
)
self.active_context.__enter__() # pylint: disable=unnecessary-dunder-call
return (
f'Mouse: drag={self.report_drag} '
f'motion={self.report_motion} pixels={self.report_pixels}'
)
def header_msg(self) -> str:
"""Return header message showing current mouse modes."""
if not self.supported:
return "Mouse: not supported"
status = []
if self.report_drag:
status.append("drag")
if self.report_motion:
status.append("motion")
if self.report_pixels:
status.append("pixels")
status_str = "+".join(status) if status else "disabled"
return f"Mouse: {status_str} [F9=drag F10=motion F11=pixels]"
def toggle_keynames(self) -> List[str]:
"""Return list of key names that toggle mouse modes."""
return ['KEY_F9', 'KEY_F10', 'KEY_F11']
def get_index_by_key(self, key_name: str) -> int:
"""Convert key name to toggle index."""
f_num = int(key_name.split('_')[-1][1:])
return f_num - 9 # F9 -> index 0, F10 -> index 1, F11 -> index 2
def cleanup(self) -> None:
"""Clean up active context manager."""
if self.active_context is not None:
self.active_context.__exit__(None, None, None)
def get_test_modes() -> Tuple[DecPrivateMode, ...]:
"""Return the tuple of DEC private modes to test."""
return (
DecPrivateMode.DECCKM,
DecPrivateMode.DECSCNM,
DecPrivateMode.DECKANAM,
DecPrivateMode.FOCUS_IN_OUT_EVENTS,
DecPrivateMode.META_SENDS_ESC,
DecPrivateMode.ALT_SENDS_ESC,
DecPrivateMode.BRACKETED_PASTE
)
def render_header(term: Terminal, dec_manager: DecModeManager, kitty_manager:
KittyKeyboardManager, mouse_manager: MouseModeManager) -> int:
"""
Render the header section.
Returns number of rows used.
"""
header = ["Press ^C to quit."]
if kitty_manager.kitty_flags is not None:
header.append(f"{kitty_manager.repr_flags()} [Shift+F1..F5] to toggle")
if mouse_manager.supported:
header.append(mouse_manager.header_msg())
# Display DEC modes table
if dec_manager.entries():
maxlen = max(len(repr(m)) for m, _ in dec_manager.entries())
for mode, enabled in dec_manager.entries():
idx = dec_manager.test_modes.index(mode)
status = " IS " if enabled else "IS NOT"
f_key = f"F{idx + 1}"
mode_description = (
f"{repr(mode):<{maxlen}} "
f"{term.reverse(status)} Enabled, "
f"[{term.reverse(f_key)}] toggles")
header.append(mode_description)
# Display, Separators, headers, return row count
echo = functools.partial(print, end=term.clear_eol + '\r\n', flush=False)
echo(term.home, end='')
echo('-' * term.width)
row_count = 1
for line in header:
echo(line)
row_count += 1
echo('-' * term.width, flush=True)
row_count += 1
return row_count
def render_keymatrix(term: Terminal, n_header_rows: int, raw_sequences: deque,
formatted_events: deque) -> None:
"""Render the key matrix display with raw sequences bar and formatted table."""
# Calculate bar width (1/3 of terminal width)
bar_width = term.width // 3
bar_y = n_header_rows + 3
# remove raw sequences tracked until they fit
def _fmt(i, sequence):
if sequence.is_sequence:
rs = repr(str(sequence))
else:
rs = repr(sequence)
if rs.startswith("'") and rs.endswith("'"):
rs = rs.strip("'")
elif rs.startswith('"') and rs.endswith('"'):
rs = rs.strip('"')
if i % 2 == 0:
return term.reverse(rs)
return rs
while True:
bar_content = ''.join(_fmt(len(raw_sequences) - i, sequence)
for i, sequence in
enumerate(raw_sequences))
if term.length(bar_content) < bar_width:
break
raw_sequences.popleft()
echo = functools.partial(print, end=term.clear_eol + '\r\n', flush=False)
bar_line = ' ' * ((term.width // 3) - 3) + f'[ {bar_content} ]'
echo(term.move_yx(bar_y - 3, 0))
echo()
echo(bar_line)
echo()
# Calculate available space for formatted events table
max_event_rows = term.height - bar_y - 5
# Render formatted events table
events_to_display = list(formatted_events)[-max_event_rows:]
echo()
echo(f"{'value':<6} {'repr':<20} {'Name':<25} extra:")
echo()
for event_line in events_to_display:
echo(event_line)
echo('', end=term.clear_eos, flush=True)
def format_key_event(term, keystroke) -> str:
"""Format a key event for columnar display."""
# Build columns: sequence | value | name | modifiers/mode_values
value_repr = repr(keystroke.value)[:6]
seq_repr = repr(str(keystroke))[:20]
name_repr = repr(keystroke.name)[:25]
if keystroke.mode and int(keystroke.mode) > 0:
extra = f'{keystroke.mode}:{keystroke._mode_values!r}'
else:
events = []
for event_name in ('pressed', 'released', 'repeated'):
if getattr(keystroke, event_name):
events.append(event_name)
assert len(events) == 1, events
modifiers = []
for modifier_name in (
# possible with most terminals
'shift', 'alt', 'ctrl',
# kitty, only
'super', 'hyper', 'meta', 'caps_lock', 'num_lock'):
if getattr(keystroke, f'_{modifier_name}'):
modifiers.append(modifier_name.upper())
extra = f'{events[0]} {"+".join(modifiers)}'
trim_mode = max(10, term.width - 25 - 20 - 6 - 3)
return f"{value_repr:<6} {seq_repr:<20} {name_repr:<25} {extra[:trim_mode]}"
def main():
"""Main application orchestrator."""
term = Terminal()
test_modes = get_test_modes()
# Initialize managers
# Key event storage
raw_sequences = deque(maxlen=100) # Store raw sequences
formatted_events = deque(maxlen=50) # Store formatted event lines
# Probe terminal capabilities
dec_manager = DecModeManager(term, test_modes)
formatted_events.extend(dec_manager.probe())
kitty_manager = KittyKeyboardManager(term)
formatted_events.extend(kitty_manager.probe())
mouse_manager = MouseModeManager(term)
formatted_events.extend(mouse_manager.probe())
# Ensure clean input state
inp = term.flushinp(0.1)
assert not inp, "Expected no input after automatic sequence negotiation"
# Main interaction loop
input_mode = term.cbreak if '--cbreak' in sys.argv else term.raw
oldsize = (term.height, term.width)
with input_mode(), term.fullscreen():
message = None
n_header_rows = 0
# Initial full render
n_header_rows = render_header(term, dec_manager, kitty_manager, mouse_manager)
render_keymatrix(term, n_header_rows, raw_sequences, formatted_events)
do_exit = False
while not do_exit:
# Handle user input
inp = term.inkey()
for mgr in (dec_manager, kitty_manager, mouse_manager):
if inp.name in mgr.toggle_keynames():
index = mgr.get_index_by_key(inp.name)
message = mgr.toggle_by_index(index)
break
if inp == 'q' or inp.name == 'KEY_CTRL_C':
do_exit = True
if inp:
raw_sequences.append(inp)
formatted_events.append(format_key_event(term, inp))
# If mode was toggled, screen was resized, or CTRL^L pressed,
# re-render header
if (message
or oldsize != (term.height, term.width)
or inp.name == 'KEY_CTRL_L'):
if message:
formatted_events.append(f">> {message}")
message = None
n_header_rows = render_header(term, dec_manager, kitty_manager, mouse_manager)
oldsize = (term.height, term.width)
# Always render key matrix (efficient, only updates changed area)
render_keymatrix(term, n_header_rows, raw_sequences, formatted_events)
dec_manager.cleanup()
kitty_manager.cleanup()
mouse_manager.cleanup()
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/mouse_coords.py 0000664 0000000 0000000 00000000727 15107137110 0020636 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
if not term.does_mouse():
print("This example won't work on your terminal!")
else:
with term.cbreak(), term.fullscreen(), term.mouse_enabled(report_drag=True):
print("Click to move cursor! ^C to quit")
while True:
inp = term.inkey()
if inp.name and inp.name.startswith('MOUSE_'):
print(term.move_yx(*inp.mouse_yx), end='', flush=True)
jquast-blessed-864a8f7/bin/mouse_drag.py 0000664 0000000 0000000 00000000625 15107137110 0020257 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
if not term.does_mouse(report_drag=True):
print("This example won't work on your terminal!")
else:
with term.cbreak(), term.mouse_enabled(report_drag=True):
while True:
inp = term.inkey()
if inp.name and inp.name.endswith('_MOTION'):
print(f"Drag event at ({inp.y}, {inp.x})")
jquast-blessed-864a8f7/bin/mouse_modifiers.py 0000775 0000000 0000000 00000001664 15107137110 0021332 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from blessed import Terminal
term = Terminal()
counts = dict()
with term.fullscreen(), term.cbreak(), term.mouse_enabled():
while True:
inp = term.inkey(timeout=None)
# Check if this is a mouse event
if inp.name and inp.name.startswith('MOUSE_'):
# Use the keystroke name for button identification
counts[inp.name] = counts.get(inp.name, 0) + 1
with term.synchronized_output():
print(term.home + term.clear)
print(term.bold("Mouse Modifier Example, press Ctrl+C to quit"))
print()
# Display the most recent event
print(f"button={inp.name} at (y={inp.y}, x={inp.x})")
print()
# Display totals
print("Totals: ")
for button_name, count in sorted(counts.items()):
print(f"{button_name}: {count}")
jquast-blessed-864a8f7/bin/mouse_paint.py 0000775 0000000 0000000 00000002577 15107137110 0020470 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from blessed import Terminal
term = Terminal()
if not term.does_mouse(report_motion=True):
print("This terminal does not support mouse motion tracking!")
else:
# Track current color for painting
color_idx = 7
num_colors = min(256, term.number_of_colors)
header = "Mouse wheel sets color=[{0}], LEFT button paints, RIGHT erases, ^C:quit"
def make_header():
return term.home + term.center(header.format(term.color(color_idx)('█')))
text = make_header()
with term.cbreak(), term.fullscreen(), term.mouse_enabled(report_motion=True):
while True:
print(text, end='', flush=True)
inp = term.inkey()
if inp.name and inp.name.startswith('MOUSE_'):
# process scroll wheel changes color
_offset = (1 if inp.name == 'MOUSE_SCROLL_UP' else
-1 if inp.name == 'MOUSE_SCROLL_DOWN' else 0)
color_idx = (color_idx + _offset) % num_colors
# and left mouse paints, right erases
char = (term.color(color_idx)('█')
if inp.name.startswith('MOUSE_LEFT')
else ' ' if inp.name.startswith('MOUSE_RIGHT')
else '')
# update draw text using mouse_yx
text = make_header() + term.move_yx(*inp.mouse_yx) + char
jquast-blessed-864a8f7/bin/mouse_pixels.py 0000664 0000000 0000000 00000000776 15107137110 0020655 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
if not term.does_mouse(report_pixels=True):
print("This terminal does not support pixel coordinate mouse tracking!")
else:
print("Click to display Pixel coordinates, ^C to quit:")
with term.cbreak(), term.mouse_enabled(report_pixels=True):
while True:
event = term.inkey()
if event.name and event.name.startswith('MOUSE_'):
print(f"Pixel position: (y={event.y}, x={event.x})")
jquast-blessed-864a8f7/bin/mouse_query.py 0000775 0000000 0000000 00000001130 15107137110 0020502 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
# check basic mouse support
if not term.does_mouse():
print(f"mouse_enabled() {term.bright_red('not supported')} on this Terminal")
else:
# check for, enable, and report all supported advanced features
feature_kwargs = {mouse_feature: True
for mouse_feature in ('report_pixels', 'report_drag', 'report_motion')
if term.does_mouse(**{mouse_feature: True})}
with term.mouse_enabled(**feature_kwargs):
print(f"mouse_enabled({', '.join(feature_kwargs)}) enabled")
jquast-blessed-864a8f7/bin/mouse_simple.py 0000664 0000000 0000000 00000000651 15107137110 0020632 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from blessed import Terminal
term = Terminal()
if not term.does_mouse():
print("This example won't work on your terminal!")
else:
print("Click anywhere! ^C to quit")
with term.cbreak(), term.mouse_enabled():
while True:
inp = term.inkey()
if inp.name and inp.name.startswith('MOUSE_'):
print(f"button {inp.name} at (y={inp.y}, x={inp.x})")
jquast-blessed-864a8f7/bin/on_resize.py 0000775 0000000 0000000 00000002335 15107137110 0020132 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
import threading
from blessed import Terminal
term = Terminal()
_resize_pending = threading.Event()
def on_resize(*args):
_resize_pending.set()
def display_size(term):
# conditionally refresh sixel size when enabled
sixel_height, sixel_width = 0, 0
if term.does_sixel():
sixel_height, sixel_width = term.get_sixel_height_and_width(force=True)
print()
print(f'height={term.height}, width={term.width}, ' +
f'pixel_height={term.pixel_height}, pixel_width={term.pixel_width}, ' +
f'sixel_height={sixel_height}, sixel_width={sixel_width}',
end='', flush=True)
if not term.does_inband_resize():
print('In-band Window Resize not supported on this terminal')
import sys
if sys.platform != 'win32':
import signal
signal.signal(signal.SIGWINCH, on_resize)
with term.cbreak(), term.notify_on_resize():
print("press 'q' to quit.")
display_size(term)
while True:
inp = term.inkey(timeout=0.1)
if inp == 'q':
break
if inp.name == 'RESIZE_EVENT':
_resize_pending.set()
elif _resize_pending.is_set():
_resize_pending.clear()
display_size(term)
jquast-blessed-864a8f7/bin/plasma.py 0000775 0000000 0000000 00000007566 15107137110 0017425 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# std imports
import sys
import math
import time
import timeit
import colorsys
import contextlib
# local
import blessed
def scale_255(val): return int(round(val * 255))
def rgb_at_xy(term, x, y, t):
h, w = term.height, term.width
hue = 4.0 + (
math.sin(x / 16.0)
+ math.sin(y / 32.0)
+ math.sin(math.sqrt(
(x - w / 2.0) * (x - w / 2.0) +
(y - h / 2.0) * (y - h / 2.0)
) / 8.0 + t * 3)
) + math.sin(math.sqrt(x * x + y * y) / 8.0)
saturation = y / h
lightness = x / w
return tuple(map(scale_255, colorsys.hsv_to_rgb(hue / 8.0, saturation, lightness)))
def screen_plasma(term, plasma_fn, t):
result = ''
for y in range(term.height - 1):
for x in range(term.width):
result += term.on_color_rgb(*plasma_fn(term, x, y, t)) + ' '
return result
@contextlib.contextmanager
def elapsed_timer():
"""Timer pattern, from https://stackoverflow.com/a/30024601."""
start = timeit.default_timer()
def elapser():
return timeit.default_timer() - start
# pylint: disable=unnecessary-lambda
yield lambda: elapser()
def show_please_wait(term):
txt_wait = 'please wait ...'
outp = term.move_yx(term.height - 1, 0) + term.clear_eol + term.center(txt_wait)
print(outp, end='')
sys.stdout.flush()
def show_paused(term):
txt_paused = 'paused'
outp = term.move_yx(term.height - 1, int(term.width / 2 - len(txt_paused) / 2))
outp += txt_paused
print(outp, end='')
sys.stdout.flush()
def next_algo(algo, forward):
algos = tuple(sorted(blessed.color.COLOR_DISTANCE_ALGORITHMS))
next_index = algos.index(algo) + (1 if forward else -1)
if next_index == len(algos):
next_index = 0
return algos[next_index]
def next_color(color, forward):
colorspaces = (4, 8, 16, 256, 1 << 24)
next_index = colorspaces.index(color) + (1 if forward else -1)
if next_index == len(colorspaces):
next_index = 0
return colorspaces[next_index]
def status(term, elapsed):
left_txt = (f'{term.number_of_colors} colors - '
f'{term.color_distance_algorithm} - ?: help ')
right_txt = f'fps: {1 / elapsed:2.2f}'
return ('\n' + term.normal +
term.white_on_blue + term.clear_eol + left_txt +
term.rjust(right_txt, term.width - len(left_txt)))
def main(term):
with term.cbreak(), term.hidden_cursor(), term.fullscreen():
pause, dirty = False, True
t = time.time()
while True:
if dirty or not pause:
if not pause:
t = time.time()
with elapsed_timer() as elapsed:
outp = term.home + screen_plasma(term, rgb_at_xy, t)
outp += status(term, elapsed())
# Use synchronized output to reduce tearing and improve smoothness
with term.dec_modes_enabled(term.DecPrivateMode.SYNCHRONIZED_OUTPUT):
print(outp, end='')
sys.stdout.flush()
dirty = False
if pause:
show_paused(term)
inp = term.inkey(timeout=None if pause else 0.01)
if inp == '?':
assert False, "don't panic"
elif inp == '\x0c':
dirty = True
if inp in ('[', ']'):
term.color_distance_algorithm = next_algo(
term.color_distance_algorithm, inp == '[')
show_please_wait(term)
dirty = True
if inp == ' ':
pause = not pause
if inp.code in (term.KEY_TAB, term.KEY_BTAB):
term.number_of_colors = next_color(
term.number_of_colors, inp.code == term.KEY_TAB)
show_please_wait(term)
dirty = True
if __name__ == "__main__":
exit(main(blessed.Terminal()))
jquast-blessed-864a8f7/bin/progress_bar.py 0000775 0000000 0000000 00000002623 15107137110 0020625 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Example application for the 'blessed' Terminal library for python.
This isn't a real progress bar, just a sample "animated prompt" of sorts that demonstrates the
separate move_x() and move_yx() capabilities, made mainly to test the `hpa' compatibility for
'screen' terminal type which fails to provide one, but blessed recognizes that it actually does, and
provides a proxy.
"""
# std imports
import sys
# local
from blessed import Terminal
def main():
"""Program entry point."""
term = Terminal()
assert term.hpa(1) != '', (
'Terminal does not support hpa (Horizontal position absolute)')
col, offset = 1, 1
with term.cbreak():
inp = None
print("press 'X' to stop.")
sys.stderr.write(term.move_yx(term.height, 0) + '[')
sys.stderr.write(term.move_x(term.width - 1) + ']' + term.move_x(1))
while inp != 'X':
if col >= (term.width - 2):
offset = -1
elif col <= 1:
offset = 1
sys.stderr.write(term.move_x(col))
if offset == -1:
sys.stderr.write('.')
else:
sys.stderr.write('=')
col += offset
sys.stderr.write(term.move_x(col))
sys.stderr.write('|\b')
sys.stderr.flush()
inp = term.inkey(0.04)
print()
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/resize.py 0000775 0000000 0000000 00000006125 15107137110 0017437 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Determines and prints COLUMNS and LINES of the attached window width.
A strange problem: programs that perform screen addressing incorrectly
determine the screen margins. Calls to reset(1) do not resolve the
issue.
This may often happen because the transport is incapable of communicating
the terminal size, such as over a serial line. This demonstration program
determines true screen dimensions and produces output suitable for evaluation
by a bourne-like shell::
$ eval `./resize.py`
The following remote login protocols communicate window size:
- ssh: notifies on dedicated session channel, see for example,
``paramiko.ServerInterface.check_channel_window_change_request``.
- telnet: sends window size through NAWS (negotiate about window
size, RFC 1073), see for example,
``telnetlib3.TelnetServer.naws_receive``.
- rlogin: protocol sends only initial window size, and does not notify
about size changes.
This is a simplified version of `resize.c
`_ provided by the
xterm package.
"""
# std imports
import sys
import collections
# local
from blessed import Terminal
def main():
"""Program entry point."""
# pylint: disable=invalid-name
# Invalid variable name "Position"
Position = collections.namedtuple('Position', ('row', 'column'))
# particularly strange, we use sys.stderr as our output stream device,
# this 'stream' file descriptor is only used for side effects, of which
# this application uses two: the term.location() has an implied write,
# as does get_position().
#
# the reason we chose stderr is to ensure that the terminal emulator
# receives our bytes even when this program is wrapped by shell eval
# `resize.py`; backticks gather stdout but not stderr in this case.
term = Terminal(stream=sys.stderr)
# Move the cursor to the farthest lower-right hand corner that is
# reasonable. Due to word size limitations in older protocols, 999,999
# is our most reasonable and portable edge boundary. Telnet NAWS is just
# two unsigned shorts: ('!HH' in python struct module format).
with term.location(999, 999):
# We're not likely at (999, 999), but a well behaved terminal emulator
# will do its best to accommodate our request, positioning the cursor
# to the farthest lower-right corner. By requesting the current
# position, we may negotiate about the window size directly with the
# terminal emulator connected at the distant end.
pos = Position(*term.get_location())
if -1 not in pos:
# true size was determined
lines, columns = pos.row, pos.column
else:
# size could not be determined. Oh well, the built-in blessed
# properties will use termios if available, falling back to
# existing environment values if it has to.
lines, columns = term.height, term.width
print(f"COLUMNS={columns};\nLINES={lines};\nexport COLUMNS LINES;"
)
if __name__ == '__main__':
exit(main())
jquast-blessed-864a8f7/bin/sixel_query.py 0000775 0000000 0000000 00000001555 15107137110 0020511 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from blessed import Terminal
term = Terminal()
# Check for sixel support
if not term.does_sixel():
print("This terminal does not support sixel graphics")
else:
print("This terminal probably supports sixel graphics")
# Get display dimensions
height, width = term.get_sixel_height_and_width()
if (height, width) == (-1, -1):
print("Could not determine sixel dimensions")
else:
print(f"Sixel area: {width}x{height} (px)")
# Get color support
colors = term.get_sixel_colors()
if colors == -1:
print("Could not determine color support")
else:
print(f"Colors available: {colors}")
# Get cell dimensions for positioning
cell_height, cell_width = term.get_cell_height_and_width()
if (cell_height, cell_width) == (-1, -1):
print("Could not determine cell size")
else:
print(f"Character cells: {cell_width}x{cell_height} (px)")
jquast-blessed-864a8f7/bin/strip.py 0000775 0000000 0000000 00000000471 15107137110 0017275 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""Example script that strips input of terminal sequences."""
# std imports
import sys
# local
import blessed
def main():
"""Program entry point."""
term = blessed.Terminal()
for line in sys.stdin:
print(term.strip_seqs(line))
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/tprint.py 0000775 0000000 0000000 00000001433 15107137110 0017453 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
A simple cmd-line tool for displaying FormattingString capabilities.
For example:
$ python tprint.py bold A rather bold statement.
"""
# std
# std imports
import argparse
# local
from blessed import Terminal
def parse_args():
"""Parse sys.argv, returning dict suitable for main()."""
parser = argparse.ArgumentParser(
description='displays argument as specified style')
parser.add_argument('style', type=str, help='style formatter')
parser.add_argument('text', type=str, nargs='+')
return dict(parser.parse_args()._get_kwargs())
def main(style, text):
"""Program entry point."""
term = Terminal()
style = getattr(term, style)
print(style(' '.join(text)))
if __name__ == '__main__':
exit(main(**parse_args()))
jquast-blessed-864a8f7/bin/worms.py 0000775 0000000 0000000 00000020267 15107137110 0017310 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Example application for the 'blessed' Terminal library for python.
It is also an experiment in functional programming.
"""
# std imports
from random import randrange
from collections import namedtuple
# local
from blessed import Terminal
def echo(text):
"""Display ``text`` and flush output."""
print(text, end='', flush=True)
# a worm is a list of (y, x) segments Locations
Location = namedtuple('Point', ('y', 'x',))
# a nibble is a (x,y) Location and value
Nibble = namedtuple('Nibble', ('location', 'value'))
# A direction is a bearing, fe.
# y=0, x=-1 = move right
# y=1, x=0 = move down
Direction = namedtuple('Direction', ('y', 'x',))
# these functions return a new Location instance, given
# the direction indicated by their name.
LEFT = (0, -1)
RIGHT = (0, 1)
UP = (-1, 0)
DOWN = (1, 0)
def left_of(segment, term):
"""Return Location left-of given segment."""
# pylint: disable=unused-argument
# Unused argument 'term'
return Location(y=segment.y,
x=max(0, segment.x - 1))
def right_of(segment, term):
"""Return Location right-of given segment."""
return Location(y=segment.y,
x=min(term.width - 1, segment.x + 1))
def above(segment, term):
"""Return Location above given segment."""
# pylint: disable=unused-argument
# Unused argument 'term'
return Location(
y=max(0, segment.y - 1),
x=segment.x)
def below(segment, term):
"""Return Location below given segment."""
return Location(
y=min(term.height - 1, segment.y + 1),
x=segment.x)
def next_bearing(term, inp_code, bearing):
"""
Return direction function for new bearing by inp_code.
If no inp_code matches a bearing direction, return a function for the current bearing.
"""
return {
term.KEY_LEFT: left_of,
term.KEY_RIGHT: right_of,
term.KEY_UP: above,
term.KEY_DOWN: below,
}.get(inp_code,
# direction function given the current bearing
{LEFT: left_of,
RIGHT: right_of,
UP: above,
DOWN: below}[(bearing.y, bearing.x)])
def change_bearing(f_mov, segment, term):
"""Return new bearing given the movement f(x)."""
return Direction(
f_mov(segment, term).y - segment.y,
f_mov(segment, term).x - segment.x)
def bearing_flipped(dir1, dir2):
"""
Direction-flipped check.
Return true if dir2 travels in opposite direction of dir1.
"""
return (0, 0) == (dir1.y + dir2.y, dir1.x + dir2.x)
def hit_any(loc, segments):
"""Return True if `loc' matches any (y, x) coordinates within segments."""
# `segments' -- a list composing a worm.
return loc in segments
def hit_vany(locations, segments):
"""Return True if any locations are found within any segments."""
return any(hit_any(loc, segments)
for loc in locations)
def hit(src, dst):
"""Return True if segments are same position (hit detection)."""
return src.x == dst.x and src.y == dst.y
def next_wormlength(nibble, head, worm_length):
"""Return new worm_length if current nibble is hit."""
if hit(head, nibble.location):
return worm_length + nibble.value
return worm_length
def next_speed(nibble, head, speed, modifier):
"""Return new speed if current nibble is hit."""
return speed * modifier if hit(head, nibble.location) else speed
def head_glyph(direction):
"""Return character for worm head depending on horiz/vert orientation."""
return ':' if direction in (left_of, right_of) else '"'
def next_nibble(term, nibble, head, worm):
"""
Provide the next nibble.
continuously generate a random new nibble so long as the current nibble hits any location of the
worm. Otherwise, return a nibble of the same location and value as provided.
"""
loc, val = nibble.location, nibble.value
while hit_vany([head] + worm, nibble_locations(loc, val)):
loc = Location(x=randrange(1, term.width - 1),
y=randrange(1, term.height - 1))
val = nibble.value + 1
return Nibble(loc, val)
def nibble_locations(nibble_location, nibble_value):
"""Return array of locations for the current "nibble"."""
# generate an array of locations for the current nibble's location
# -- a digit such as '123' may be hit at 3 different (y, x) coordinates.
return [
Location(x=nibble_location.x + offset, y=nibble_location.y)
for offset in range(1 + len(f'{nibble_value}') - 1)
]
def main():
"""Program entry point."""
# pylint: disable=too-many-locals
# Too many local variables (20/15)
term = Terminal()
worm = [Location(x=term.width // 2, y=term.height // 2)]
worm_length = 2
bearing = Direction(*LEFT)
direction = left_of
nibble = Nibble(location=worm[0], value=0)
color_nibble = term.black_on_green
color_worm = term.yellow_reverse
color_head = term.red_reverse
color_bg = term.on_blue
echo(term.move_yx(1, 1))
echo(color_bg(term.clear))
# speed is actually a measure of time; the shorter, the faster.
speed = 0.1
modifier = 0.93
inp = None
echo(term.move_yx(term.height, 0))
with term.hidden_cursor(), term.cbreak(), term.location():
while inp not in ('q', 'Q'):
# delete the tail of the worm at worm_length
if len(worm) > worm_length:
echo(term.move_yx(*worm.pop(0)))
echo(color_bg(' '))
# compute head location
head = worm.pop()
# check for hit against self; hitting a wall results in the (y, x)
# location being clipped, -- and death by hitting self (not wall).
if hit_any(head, worm):
break
# get the next nibble, which may be equal to ours unless this
# nibble has been struck by any portion of our worm body.
n_nibble = next_nibble(term, nibble, head, worm)
# get the next worm_length and speed, unless unchanged.
worm_length = next_wormlength(nibble, head, worm_length)
speed = next_speed(nibble, head, speed, modifier)
if n_nibble != nibble:
# erase the old one, careful to redraw the nibble contents
# with a worm color for those portions that overlay.
for (yloc, xloc) in nibble_locations(*nibble):
echo(''.join((
term.move_yx(yloc, xloc),
(color_worm if (yloc, xloc) == head
else color_bg)(' '),
term.normal)))
# and draw the new,
echo(term.move_yx(*n_nibble.location) + (
color_nibble(f'{n_nibble.value}')))
# display new worm head
echo(term.move_yx(*head) + color_head(head_glyph(direction)))
# and its old head (now, a body piece)
if worm:
echo(term.move_yx(*(worm[-1])))
echo(color_worm(' '))
echo(term.move_yx(*head))
# wait for keyboard input, which may indicate
# a new direction (up/down/left/right)
inp = term.inkey(timeout=speed)
# discover new direction, given keyboard input and/or bearing.
nxt_direction = next_bearing(term, inp.code, bearing)
# discover new bearing, given new direction compared to prev
nxt_bearing = change_bearing(nxt_direction, head, term)
# disallow new bearing/direction when flipped: running into
# oneself, for example traveling left while traveling right.
if not bearing_flipped(bearing, nxt_bearing):
direction = nxt_direction
bearing = nxt_bearing
# append the prior `head' onto the worm, then
# a new `head' for the given direction.
worm.extend([head, direction(head, term)])
# re-assign new nibble,
nibble = n_nibble
echo(term.normal)
score = (worm_length - 1) * 100
echo(''.join((term.move_yx(term.height - 1, 1), term.normal)))
echo(''.join(('\r\n', f'score: {score}', '\r\n')))
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/bin/x11_colorpicker.py 0000664 0000000 0000000 00000007174 15107137110 0021145 0 ustar 00root root 0000000 0000000 # std imports
import re
import math
import colorsys
from functools import reduce
# local
import blessed
from blessed.colorspace import X11_COLORNAMES_TO_RGB
def sort_colors():
colors = {}
for color_name, rgb_color in X11_COLORNAMES_TO_RGB.items():
if rgb_color in colors:
colors[rgb_color].append(color_name)
else:
colors[rgb_color] = [color_name]
def sortby_hv(rgb_item):
# sort by hue rounded to nearest %,
# then by color name & number
# except shades of grey -- by name & number, only
rgb, name = rgb_item
digit = 0
match = re.match(r'(.*)(\d+)', name[0])
if match is not None:
name = match.group(1)
digit = int(match.group(2))
else:
name = name[0]
hash_name = reduce(int.__mul__, map(ord, name))
hsv = colorsys.rgb_to_hsv(*rgb)
if rgb[0] == rgb[1] == rgb[2]:
return 100, hsv[2], hash_name, digit
return int(math.floor(hsv[0] * 100)), hash_name, digit, hsv[2]
return sorted(colors.items(), key=sortby_hv)
HSV_SORTED_COLORS = sort_colors()
def render(term, idx):
rgb_color, color_names = HSV_SORTED_COLORS[idx]
result = term.home + term.normal + ''.join(
getattr(term, HSV_SORTED_COLORS[i][1][0]) + '◼'
for i in range(len(HSV_SORTED_COLORS))
)
result += term.clear_eos + '\n'
result += getattr(term, 'on_' + color_names[0]) + term.clear_eos + '\n'
result += term.normal + \
term.center(f'{" | ".join(color_names)}: {rgb_color}') + '\n'
result += term.normal + term.center(
f'{term.number_of_colors} colors - '
f'{term.color_distance_algorithm}')
result += term.move_yx(idx // term.width, idx % term.width)
result += term.on_color_rgb(*rgb_color)(' \b')
return result
def next_algo(algo, forward):
algos = tuple(sorted(blessed.color.COLOR_DISTANCE_ALGORITHMS))
next_index = algos.index(algo) + (1 if forward else -1)
if next_index == len(algos):
next_index = 0
return algos[next_index]
def next_color(color, forward):
colorspaces = (4, 8, 16, 256, 1 << 24)
next_index = colorspaces.index(color) + (1 if forward else -1)
if next_index == len(colorspaces):
next_index = 0
return colorspaces[next_index]
def main():
term = blessed.Terminal()
with term.cbreak(), term.hidden_cursor(), term.fullscreen():
idx = len(HSV_SORTED_COLORS) // 2
dirty = True
while True:
if dirty:
outp = render(term, idx)
print(outp, end='', flush=True)
with term.hidden_cursor():
inp = term.inkey()
dirty = True
if inp.code == term.KEY_LEFT or inp == 'h':
idx -= 1
elif inp.code == term.KEY_DOWN or inp == 'j':
idx += term.width
elif inp.code == term.KEY_UP or inp == 'k':
idx -= term.width
elif inp.code == term.KEY_RIGHT or inp == 'l':
idx += 1
elif inp.code in (term.KEY_TAB, term.KEY_BTAB):
term.number_of_colors = next_color(
term.number_of_colors, inp.code == term.KEY_TAB)
elif inp in ('[', ']'):
term.color_distance_algorithm = next_algo(
term.color_distance_algorithm, inp == '[')
elif inp != '\x0c':
dirty = False
while idx < 0:
idx += len(HSV_SORTED_COLORS)
while idx >= len(HSV_SORTED_COLORS):
idx -= len(HSV_SORTED_COLORS)
if __name__ == '__main__':
main()
jquast-blessed-864a8f7/blessed/ 0000775 0000000 0000000 00000000000 15107137110 0016426 5 ustar 00root root 0000000 0000000 jquast-blessed-864a8f7/blessed/__init__.py 0000664 0000000 0000000 00000000552 15107137110 0020541 0 ustar 00root root 0000000 0000000 """
A thin, practical wrapper around terminal capabilities in Python.
http://pypi.python.org/pypi/blessed
"""
# std imports
import platform as _platform
# isort: off
if _platform.system() == 'Windows':
from blessed.win_terminal import Terminal
else:
from blessed.terminal import Terminal # type: ignore
__all__ = ('Terminal',)
__version__ = "1.25.0"
jquast-blessed-864a8f7/blessed/_capabilities.py 0000664 0000000 0000000 00000014722 15107137110 0021576 0 ustar 00root root 0000000 0000000 """Terminal capability builder patterns."""
# std imports
import re
import typing
from collections import OrderedDict
__all__ = (
'CAPABILITY_DATABASE',
'CAPABILITIES_RAW_MIXIN',
'CAPABILITIES_ADDITIVES',
'CAPABILITIES_HORIZONTAL_DISTANCE',
'CAPABILITIES_CAUSE_MOVEMENT',
)
CAPABILITY_DATABASE: \
typing.OrderedDict[str, typing.Tuple[str, typing.Dict[str, typing.Any]]] = 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', {})),
))
_ESC = re.escape('\x1b')
_CSI = rf'{_ESC}\['
_ANY_NOTESC = rf'[^{_ESC}]*'
CAPABILITIES_RAW_MIXIN: typing.Dict[str, str] = {
'bell': re.escape('\a'),
'carriage_return': re.escape('\r'),
'cursor_left': re.escape('\b'),
'cursor_report': rf'{_CSI}(\d+)\;(\d+)R',
'cursor_right': rf'{_CSI}C',
'exit_attribute_mode': rf'{_CSI}m',
'parm_left_cursor': rf'{_CSI}(\d+)D',
'parm_right_cursor': rf'{_CSI}(\d+)C',
'restore_cursor': rf'{_CSI}u',
'save_cursor': rf'{_CSI}s',
'scroll_forward': re.escape('\n'),
'set0_des_seq': re.escape('\x1b(B'),
'tab': re.escape('\t'),
}
CAPABILITIES_ADDITIVES: typing.Dict[
str, typing.Union[typing.Tuple[str, str, int], typing.Tuple[str, str]]] = {
'link': (rf'{_ESC}\]8;{_ANY_NOTESC};{_ANY_NOTESC}{_ESC}\\', 'link', 1),
'color256': (rf'{_CSI}38;5;\d+m', 'color', 1),
'on_color256': (rf'{_CSI}48;5;\d+m', 'on_color', 1),
'color_rgb': (rf'{_CSI}38;2;\d+;\d+;\d+m', 'color_rgb', 3),
'on_color_rgb': (rf'{_CSI}48;2;\d+;\d+;\d+m', 'on_color_rgb', 3),
'shift_in': (re.escape('\x0f'), ''),
'shift_out': (re.escape('\x0e'), ''),
# sgr(...) outputs strangely, use the basic ANSI/EMCA-48 codes here.
'set_a_attributes1': (rf'{_CSI}\d+m', 'sgr', 1),
'set_a_attributes2': (rf'{_CSI}\d+\;\d+m', 'sgr', 2),
'set_a_attributes3': (rf'{_CSI}\d+\;\d+\;\d+m', 'sgr', 3),
'set_a_attributes4': (rf'{_CSI}\d+\;\d+\;\d+\;\d+m', 'sgr', 4),
# this helps where xterm's sgr0 includes set0_des_seq, we'd
# rather like to also match this immediate substring.
'sgr0': (rf'{_CSI}m', 'sgr0'),
'backspace': (re.escape('\b'), ''),
'ascii_tab': (CAPABILITIES_RAW_MIXIN['tab'], ''),
'clr_eol': (rf'{_CSI}K', ''),
'clr_eol0': (rf'{_CSI}0K', ''),
'clr_bol': (rf'{_CSI}1K', ''),
'clr_eosK': (rf'{_CSI}2K', ''),
}
CAPABILITIES_HORIZONTAL_DISTANCE: typing.Dict[str, int] = {
'ascii_tab': 8,
'backspace': -1,
'cursor_left': -1,
'cursor_right': 1,
'parm_left_cursor': -1,
'parm_right_cursor': 1,
'tab': 8,
}
CAPABILITIES_CAUSE_MOVEMENT: typing.Tuple[str, ...] = tuple(CAPABILITIES_HORIZONTAL_DISTANCE) + (
'carriage_return',
'clear_screen',
'column_address',
'cursor_address',
'cursor_down',
'cursor_home',
'cursor_up',
'enter_fullscreen',
'exit_fullscreen',
'parm_down_cursor',
'parm_up_cursor',
'restore_cursor',
'row_address',
'scroll_forward',
)
jquast-blessed-864a8f7/blessed/color.py 0000664 0000000 0000000 00000025051 15107137110 0020121 0 ustar 00root root 0000000 0000000 """
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
from typing import Dict, Tuple, Callable
from functools import lru_cache
_RGB = Tuple[int, int, int]
def rgb_to_xyz(red: int, green: int, blue: int) -> Tuple[float, float, float]:
"""
Convert standard RGB color to XYZ color.
D65/2° standard illuminant.
: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
"""
rgb = []
for int_val in red, green, blue:
val = float(int_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)
r_float, g_float, b_float = rgb # pylint: disable=unbalanced-tuple-unpacking
x_val = r_float * 0.4124 + g_float * 0.3576 + b_float * 0.1805
y_val = r_float * 0.2126 + g_float * 0.7152 + b_float * 0.0722
z_val = r_float * 0.0193 + g_float * 0.1192 + b_float * 0.9505
return x_val, y_val, z_val
def xyz_to_lab(x_val: float, y_val: float, z_val: float) -> Tuple[float, float, float]:
"""
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 float_val, ref in (x_val, 95.047), (y_val, 100.0), (z_val, 108.883):
val = float_val / ref
val = pow(val, 1 / 3.0) if val > 0.008856 else 7.787 * val + 16 / 116.0
xyz.append(val)
x_float, y_float, z_float = xyz # pylint: disable=unbalanced-tuple-unpacking
cie_l = 116 * y_float - 16
cie_a = 500 * (x_float - y_float)
cie_b = 200 * (y_float - z_float)
return cie_l, cie_a, cie_b
@lru_cache(maxsize=256)
def rgb_to_lab(red: int, green: int, blue: int) -> Tuple[float, float, float]:
"""
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: _RGB, rgb2: _RGB) -> float:
"""
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: _RGB, rgb2: _RGB) -> float:
"""
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: _RGB, rgb2: _RGB) -> float:
"""
Determine distance between two rgb colors using the CIE76 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: _RGB, rgb2: _RGB) -> float:
# 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 +
(delta_c / (k_c * s_c)) ** 2 +
(delta_h / (k_h * s_h)) ** 2)
def dist_cie2000(rgb1: _RGB, rgb2: _RGB) -> float:
# 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.0
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.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.0
else:
delta_h_prime -= 360.0
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: Dict[str,
Callable[[_RGB,
_RGB],
float]] = {'rgb': dist_rgb,
'rgb-weighted': dist_rgb_weighted,
'cie76': dist_cie76,
'cie94': dist_cie94,
'cie2000': dist_cie2000}
# Precomputed lookup tables for fast 256-color xterm cube mapping
# Based on xterm's 256colres.pl: levels [0, 95, 135, 175, 215, 255] for 6x6x6 cube
_CUBE_LEVELS = (0, 95, 135, 175, 215, 255)
# Precomputed RGB to cube index mapping "level", (0-5) for each RGB value (0-255)
# Uses xterm thresholds based on midpoints between cube levels [0,95,135,175,215,255]
# Thresholds: 48, 115, 155, 195, 235
_RGB_TO_CUBE_IDX = tuple(
0 if v < 48 else 1 if v < 115 else 2 if v < 155 else 3 if v < 195 else 4 if v < 235 else 5
for v in range(256)
)
# Precomputed RGB to cube value mapping for each RGB value (0-255)
_RGB_TO_CUBE_VAL = tuple(_CUBE_LEVELS[_RGB_TO_CUBE_IDX[v]] for v in range(256))
# Precomputed grayscale index mapping from brightness value (0-255) to gray index (0-23)
# Formula: 8 + 10*i gives gray values, so i = (v-8)/10, clamped to [0,23]
_GRAY_IDX_FROM_V = tuple(
0 if v < 8 else 23 if v > 238 else int(round((v - 8) / 10.0))
for v in range(256)
)
# Precomputed gray values for each gray index (0-23)
_GRAY_VAL_FROM_IDX = tuple(8 + 10 * i for i in range(24))
def xterm256color_from_rgb(red: int, green: int, blue: int) -> Tuple[int, _RGB]:
"""
Convert RGB values to xterm 256-color cube index and RGB approximation.
Uses the 6x6x6 color cube (indices 16-231) with levels [0,95,135,175,215,255].
:arg int red: RGB value of Red (0-255).
:arg int green: RGB value of Green (0-255).
:arg int blue: RGB value of Blue (0-255).
:returns: Tuple (cube_index, (r, g, b)) representing the xterm cube index and RGB approximation
:rtype: tuple
"""
# Find nearest candidate by "6x6x6 cube", (indices 16-231):
# 6x6x6 cube with levels [0,95,135,175,215,255]
r_idx = _RGB_TO_CUBE_IDX[red]
g_idx = _RGB_TO_CUBE_IDX[green]
b_idx = _RGB_TO_CUBE_IDX[blue]
cube_idx = 16 + 36 * r_idx + 6 * g_idx + b_idx
cube_rgb = (_RGB_TO_CUBE_VAL[red], _RGB_TO_CUBE_VAL[green], _RGB_TO_CUBE_VAL[blue])
return cube_idx, cube_rgb
def xterm256gray_from_rgb(red: int, green: int, blue: int) -> Tuple[int, _RGB]:
"""
Convert RGB values to xterm 256-color grayscale index and RGB approximation.
Uses the 24 grayscale entries (indices 232-255) with values 8+10*i.
:arg int red: RGB value of Red (0-255).
:arg int green: RGB value of Green (0-255).
:arg int blue: RGB value of Blue (0-255).
:returns: Tuple (gray_index, (r, g, b)) representing the xterm gray index and RGB approximation
:rtype: tuple
"""
# Grayscale candidate (indices 232-255):
# 24 grays with values 8+10*i
brightness = (red + green + blue) // 3
gray_idx_offset = _GRAY_IDX_FROM_V[brightness]
gray_idx = 232 + gray_idx_offset
gray_val = _GRAY_VAL_FROM_IDX[gray_idx_offset]
gray_rgb = (gray_val, gray_val, gray_val)
return gray_idx, gray_rgb
jquast-blessed-864a8f7/blessed/colorspace.py 0000664 0000000 0000000 00000105134 15107137110 0021136 0 ustar 00root root 0000000 0000000 """
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
from typing import Set, Dict, Tuple
__all__ = (
'CGA_COLORS',
'RGBColor',
'RGB_256TABLE',
'X11_COLORNAMES_TO_RGB',
)
CGA_COLORS: Set[str] = {'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) -> str:
return f'#{self.red:02x}{self.green:02x}{self.blue:02x}'
#: X11 Color names to (XTerm-defined) RGB values from xorg-rgb/rgb.txt
X11_COLORNAMES_TO_RGB: Dict[str, RGBColor] = {
'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: Tuple[RGBColor, ...] = (
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),
)
jquast-blessed-864a8f7/blessed/dec_modes.py 0000664 0000000 0000000 00000053651 15107137110 0020734 0 ustar 00root root 0000000 0000000 """Class definitions for DEC Private Modes and their Response values."""
# std imports
from typing import Any, Union
class DecModeResponse:
"""
Container for DEC Private Mode query response.
Use helper properties :attr:`~DecModeResponse.supported`,
:attr:`~DecModeResponse.enabled`, :attr:`~DecModeResponse.permanent`,
and :attr:`~DecModeResponse.failed` to interpret responses rather than
checking numeric values directly.
"""
# Response value constants. Values -1 and -2 are internal abstractions
# for timeout or query failure, functionally equivalent to NOT_RECOGNIZED.
NOT_QUERIED = -2
NO_RESPONSE = -1
NOT_RECOGNIZED = 0
SET = 1
RESET = 2
PERMANENTLY_SET = 3
PERMANENTLY_RESET = 4
def __init__(self, mode: Union[int, "DecPrivateMode"], value: int):
"""
Initialize response for a DEC private mode query.
:param mode: DEC private mode number
:type mode: int
:param value: Response value from terminal
:type value: int
:raises TypeError: If mode is not an integer
"""
if isinstance(mode, DecPrivateMode):
self._mode_value = mode.value
elif isinstance(mode, int):
self._mode_value = mode
else:
raise TypeError(f"Invalid mode got {mode!r}, DecPrivateMode or int expected")
self._value = value
@property
def mode(self) -> "DecPrivateMode":
"""
The :class:`DecPrivateMode` instance for this response.
:rtype: DecPrivateMode
"""
return DecPrivateMode(self._mode_value)
@property
def description(self) -> str:
"""
Description of what this mode controls.
:rtype: str
"""
if isinstance(self.mode, DecPrivateMode):
return self.mode.long_description
return "Unknown mode"
@property
def value(self) -> int:
"""
Numeric response value for compatibility.
Prefer using helper properties like :attr:`~DecModeResponse.enabled`
instead of checking this value directly.
:rtype: int
"""
return self._value
@property
def supported(self) -> bool:
"""
Check if the mode is supported by the terminal.
:rtype: bool
:returns: True if terminal recognizes and supports this mode
"""
return self.value > 0
@property
def enabled(self) -> bool:
"""
Check if the mode is currently enabled.
:rtype: bool
:returns: True if mode is set (temporarily or permanently)
"""
return self.value in {1, 3}
@property
def disabled(self) -> bool:
"""
Check if the mode is currently disabled.
:rtype: bool
:returns: True if mode is reset (temporarily or permanently)
"""
return self.value in {2, 4}
@property
def changeable(self) -> bool:
"""
Check if the mode setting can be changed.
:rtype: bool
:returns: True if mode can be toggled by applications
"""
return self.value in {1, 2}
@property
def permanent(self) -> bool:
"""
Check if the mode setting is permanent.
:rtype: bool
:returns: True if mode cannot be changed by applications
"""
return self.value in {3, 4}
@property
def failed(self) -> bool:
"""
Check if the query failed.
:rtype: bool
:returns: True if response indicates timeout or query failure
"""
return self.value < 0
def __str__(self) -> str:
"""
Return the constant name for the response value.
:rtype: str
"""
return {
self.NOT_QUERIED: "NOT_QUERIED",
self.NO_RESPONSE: "NO_RESPONSE",
self.NOT_RECOGNIZED: "NOT_RECOGNIZED",
self.SET: "SET",
self.RESET: "RESET",
self.PERMANENTLY_SET: "PERMANENTLY_SET",
self.PERMANENTLY_RESET: "PERMANENTLY_RESET"
}.get(self.value, "UNKNOWN")
def __repr__(self) -> str:
"""
Return full representation with mode and response details.
:rtype: str
"""
response_name = str(self)
response_value = f"({self.value})"
return f"{self.mode.name}({self._mode_value}) is {response_name}{response_value}"
class DecPrivateMode:
"""
DEC Private Mode with mnemonic name and description.
Each instance provides:
- :attr:`~DecPrivateMode.value`: Numeric private mode identifier
- :attr:`~DecPrivateMode.name`: Mnemonic name or "UNKNOWN" for unrecognized modes
- :attr:`~DecPrivateMode.long_description`: Full description of mode functionality
"""
# These are *not* DecPrivateModes, in that they are not negotiable using the
# DEC Private Mode sequences, but are carried in the same way, and attached
# to this class for type safety, as they carry meaning that they have a
# "special encoding" that is evaluated on-demand on evaluation of
# term.inkey().name as 'KEY_SHIFT_F1' or testing inkey().is_alt_shift('a').
# these key "events" have special late-binding evaluations depending on the
# 'mode' they were sent as.
SpecialInternalLegacyCSIModifier = -3
SpecialInternalModifyOtherKeys = -2
SpecialInternalKitty = -1
# VT/DEC standard modes (using canonical mnemonics where available) the
# "official" constants as published should always be used, even if cryptic,
# at least they are sure to match exactly to existing documentation
DECCKM = 1
DECANM = 2
DECCOLM = 3 # https://vt100.net/docs/vt510-rm/DECCOLM.html
DECSCLM = 4 # https://vt100.net/docs/vt510-rm/DECSCLM.html
DECSCNM = 5 # https://vt100.net/docs/vt510-rm/DECSCNM.html
DECOM = 6 # https://vt100.net/docs/vt510-rm/DECOM.html
DECAWM = 7 # https://vt100.net/docs/vt510-rm/DECAWM.html
DECARM = 8 # https://vt100.net/docs/vt510-rm/DECARM.html
DECINLM = 9
DECEDM = 10
DECLTM = 11
DECKANAM = 12
DECSCFDM = 13
DECTEM = 14
DECEKEM = 16
DECPFF = 18
DECPEX = 19
OV1 = 20
BA1 = 21
BA2 = 22
PK1 = 23
AH1 = 24
DECTCEM = 25
DECPSP = 27
DECPSM = 29
SHOW_SCROLLBAR_RXVT = 30
DECRLM = 34
DECHEBM = 35
DECHEM = 36
DECTEK = 38
DECCRNLM = 40
DECUPM = 41
DECNRCM = 42
DECGEPM = 43
DECGPCM = 44
DECGPCS = 45
DECGPBM = 46
DECGRPM = 47
DECTHAIM = 49
DECTHAICM = 50
DECBWRM = 51
DECOPM = 52
DEC131TM = 53
DECBPM = 55
DECNAKB = 57
DECIPEM = 58
DECKKDM = 59
DECHCCM = 60
DECVCCM = 61
DECPCCM = 64
DECBCMM = 65
DECNKM = 66
DECBKM = 67
DECKBUM = 68
DECVSSM = 69
DECFPM = 70
DECXRLM = 73
DECSDM = 80
DECKPM = 81
WY_52_LINE = 83
WYENAT_OFF = 84
REPLACEMENT_CHAR_COLOR = 85
DECTHAISCM = 90
DECNCSM = 95
DECRLCM = 96
DECCRTSM = 97
DECARSM = 98
DECMCM = 99
DECAAM = 100
DECCANSM = 101
DECNULM = 102
DECHDPXM = 103
DECESKM = 104
DECOSCNM = 106
DECNUMLK = 108
DECCAPSLK = 109
DECKLHIM = 110
DECFWM = 111
DECRPL = 112
DECHWUM = 113
DECATCUM = 114
DECATCBM = 115
DECBBSM = 116
DECECM = 117
# Mouse reporting modes and xterm/rxvt extensions
MOUSE_REPORT_CLICK = 1000
MOUSE_HILITE_TRACKING = 1001
MOUSE_REPORT_DRAG = 1002
MOUSE_ALL_MOTION = 1003
FOCUS_IN_OUT_EVENTS = 1004
MOUSE_EXTENDED_UTF8 = 1005
MOUSE_EXTENDED_SGR = 1006
ALT_SCROLL_XTERM = 1007
SCROLL_ON_TTY_OUTPUT_RXVT = 1010
SCROLL_ON_KEYPRESS_RXVT = 1011
FAST_SCROLL = 1014
MOUSE_URXVT = 1015
MOUSE_SGR_PIXELS = 1016
BOLD_ITALIC_HIGH_INTENSITY = 1021
# Keyboard and meta key handling modes
META_SETS_EIGHTH_BIT = 1034
MODIFIERS_ALT_NUMLOCK = 1035
META_SENDS_ESC = 1036
KP_DELETE_SENDS_DEL = 1037
ALT_SENDS_ESC = 1039
# Selection, clipboard, and window manager hint modes
KEEP_SELECTION_NO_HILITE = 1040
USE_CLIPBOARD_SELECTION = 1041
URGENCY_ON_CTRL_G = 1042
RAISE_ON_CTRL_G = 1043
REUSE_CLIPBOARD_DATA = 1044
EXTENDED_REVERSE_WRAPAROUND = 1045
ALT_SCREEN_BUFFER_SWITCH = 1046
# Alternate screen buffer and cursor save/restore combinations
ALT_SCREEN_BUFFER_XTERM = 1047
SAVE_CURSOR_DECSC = 1048
ALT_SCREEN_AND_SAVE_CLEAR = 1049
# Terminal info and function key emulation modes
TERMINFO_FUNC_KEY_MODE = 1050
SUN_FUNC_KEY_MODE = 1051
HP_FUNC_KEY_MODE = 1052
SCO_FUNC_KEY_MODE = 1053
# Legacy keyboard emulation modes
LEGACY_KBD_X11R6 = 1060
VT220_KBD_EMULATION = 1061
SIXEL_PRIVATE_PALETTE = 1070
# VTE BiDi extensions
BIDI_ARROW_KEY_SWAPPING = 1243
# iTerm2 extensions
ITERM2_REPORT_KEY_UP = 1337
# XTerm readline and mouse enhancements
READLINE_MOUSE_BUTTON_1 = 2001
READLINE_MOUSE_BUTTON_2 = 2002
READLINE_MOUSE_BUTTON_3 = 2003
BRACKETED_PASTE = 2004
READLINE_CHARACTER_QUOTING = 2005
READLINE_NEWLINE_PASTING = 2006
# Modern terminal extensions
SYNCHRONIZED_OUTPUT = 2026
GRAPHEME_CLUSTERING = 2027
TEXT_REFLOW = 2028
PASSIVE_MOUSE_TRACKING = 2029
REPORT_GRID_CELL_SELECTION = 2030
COLOR_PALETTE_UPDATES = 2031
IN_BAND_WINDOW_RESIZE = 2048
# VTE bidirectional text extensions
MIRROR_BOX_DRAWING = 2500
BIDI_AUTODETECTION = 2501
# mintty extensions
AMBIGUOUS_WIDTH_REPORTING = 7700
SCROLL_MARKERS = 7711
REWRAP_ON_RESIZE_MINTTY = 7723
APPLICATION_ESCAPE_KEY = 7727
ESC_KEY_SENDS_BACKSLASH = 7728
GRAPHICS_POSITION = 7730
ALT_MODIFIED_MOUSEWHEEL = 7765
SHOW_HIDE_SCROLLBAR = 7766
FONT_CHANGE_REPORTING = 7767
GRAPHICS_POSITION_2 = 7780
SHORTCUT_KEY_MODE = 7783
MOUSEWHEEL_REPORTING = 7786
APPLICATION_MOUSEWHEEL = 7787
BIDI_CURRENT_LINE = 7796
# Terminal-specific extensions
TTCTH = 8200
SIXEL_SCROLLING_LEAVES_CURSOR = 8452
CHARACTER_MAPPING_SERVICE = 8800
AMBIGUOUS_WIDTH_DOUBLE_WIDTH = 8840
WIN32_INPUT_MODE = 9001
KITTY_HANDLE_CTRL_C_Z = 19997
MINTTY_BIDI = 77096
INPUT_METHOD_EDITOR = 737769
# Comprehensive descriptions for each mode -- it would have been nice if all
# 3 mode items (value, key, description) could be defined side-by-side but
# we wish to have a int-derived and like-type and not do any metaclassing,
# or otherwise "unpicklable" or difficult to reason about for compatibility
_LONG_DESCRIPTIONS = {
SpecialInternalLegacyCSIModifier: "Non-DEC Mode used internally by Keystroke",
SpecialInternalModifyOtherKeys: "Non-DEC Mode used internally by Keystroke",
SpecialInternalKitty: "Non_DEC Mode used internally by Keystroke",
# DEC standard modes (1-117). The "classical" names are used instead of
# more friendly mnemonics, that's because most of these are legacy and
# unused and it makes it easier to find out about them.
DECCKM: "Cursor Keys Mode",
DECANM: "ANSI/VT52 Mode",
DECCOLM: "Column Mode",
DECSCLM: "Scrolling Mode",
DECSCNM: "Screen Mode (light or dark screen)",
DECOM: "Origin Mode",
DECAWM: "Auto Wrap Mode",
DECARM: "Auto Repeat Mode",
DECINLM: "Interlace Mode / Mouse X10 tracking",
DECEDM: "Editing Mode / Show toolbar (rxvt)",
DECLTM: "Line Transmit Mode",
DECKANAM: "Katakana Shift Mode / Blinking cursor (xterm)",
DECSCFDM: ("Space Compression/Field Delimiter Mode / "
"Start blinking cursor (xterm)"),
DECTEM: "Transmit Execution Mode / Enable XOR of blinking cursor control (xterm)",
DECEKEM: "Edit Key Execution Mode",
DECPFF: "Print Form Feed",
DECPEX: "Printer Extent",
OV1: "Overstrike",
BA1: "Local BASIC",
BA2: "Host BASIC",
PK1: "Programmable Keypad",
AH1: "Auto Hardcopy",
DECTCEM: "Text Cursor Enable Mode",
DECPSP: "Proportional Spacing",
DECPSM: "Pitch Select Mode",
SHOW_SCROLLBAR_RXVT: "Show scrollbar (rxvt)",
DECRLM: "Cursor Right to Left Mode",
DECHEBM: "Hebrew (Keyboard) Mode / Enable font-shifting functions (rxvt)",
DECHEM: "Hebrew Encoding Mode",
DECTEK: "Tektronix 4010/4014 Mode",
DECCRNLM: "Carriage Return/New Line Mode / Allow 80⇒132 mode (xterm)",
DECUPM: "Unidirectional Print Mode / more(1) fix (xterm)",
DECNRCM: "National Replacement Character Set Mode",
DECGEPM: "Graphics Expanded Print Mode",
DECGPCM: "Graphics Print Color Mode / Turn on margin bell (xterm)",
DECGPCS: "Graphics Print Color Syntax / Reverse-wraparound mode (xterm)",
DECGPBM: "Graphics Print Background Mode / Start logging (xterm)",
DECGRPM: "Graphics Rotated Print Mode / Use Alternate Screen Buffer (xterm)",
DECTHAIM: "Thai Input Mode",
DECTHAICM: "Thai Cursor Mode",
DECBWRM: "Black/White Reversal Mode",
DECOPM: "Origin Placement Mode",
DEC131TM: "VT131 Transmit Mode",
DECBPM: "Bold Page Mode",
DECNAKB: "Greek/N-A Keyboard Mapping Mode",
DECIPEM: "Enter IBM Proprinter Emulation Mode",
DECKKDM: "Kanji/Katakana Display Mode",
DECHCCM: "Horizontal Cursor Coupling",
DECVCCM: "Vertical Cursor Coupling Mode",
DECPCCM: "Page Cursor Coupling Mode",
DECBCMM: "Business Color Matching Mode",
DECNKM: "Numeric Keypad Mode",
DECBKM: "Backarrow Key Mode",
DECKBUM: "Keyboard Usage Mode",
DECVSSM: "Vertical Split Screen Mode / DECLRMM - Left Right Margin Mode",
DECFPM: "Force Plot Mode",
DECXRLM: "Transmission Rate Limiting",
DECSDM: "Sixel Display Mode",
DECKPM: "Key Position Mode",
WY_52_LINE: "52 line mode (WY-370)",
WYENAT_OFF: "Erasable/nonerasable WYENAT Off attribute select (WY-370)",
REPLACEMENT_CHAR_COLOR: "Replacement character color (WY-370)",
DECTHAISCM: "Thai Space Compensating Mode",
DECNCSM: "No Clearing Screen on Column Change Mode",
DECRLCM: "Right to Left Copy Mode",
DECCRTSM: "CRT Save Mode",
DECARSM: "Auto Resize Mode",
DECMCM: "Modem Control Mode",
DECAAM: "Auto Answerback Mode",
DECCANSM: "Conceal Answerback Message Mode",
DECNULM: "Ignore Null Mode",
DECHDPXM: "Half Duplex Mode",
DECESKM: "Secondary Keyboard Language Mode",
DECOSCNM: "Overscan Mode",
DECNUMLK: "NumLock Mode",
DECCAPSLK: "Caps Lock Mode",
DECKLHIM: "Keyboard LEDs Host Indicator Mode",
DECFWM: "Framed Windows Mode",
DECRPL: "Review Previous Lines Mode",
DECHWUM: "Host Wake-Up Mode",
DECATCUM: "Alternate Text Color Underline Mode",
DECATCBM: "Alternate Text Color Blink Mode",
DECBBSM: "Bold and Blink Style Mode",
DECECM: "Erase Color Mode",
# Mouse and xterm extensions (1000+)
MOUSE_REPORT_CLICK: "Send Mouse X & Y on button press",
MOUSE_HILITE_TRACKING: "Use Hilite Mouse Tracking",
MOUSE_REPORT_DRAG: "Use Cell Motion Mouse Tracking",
MOUSE_ALL_MOTION: "Use All Motion Mouse Tracking",
FOCUS_IN_OUT_EVENTS: "Send FocusIn/FocusOut events",
MOUSE_EXTENDED_UTF8: "Enable UTF-8 Mouse Mode",
MOUSE_EXTENDED_SGR: "Enable SGR Mouse Mode",
ALT_SCROLL_XTERM: "Enable Alternate Scroll Mode",
SCROLL_ON_TTY_OUTPUT_RXVT: "Scroll to bottom on tty output",
SCROLL_ON_KEYPRESS_RXVT: "Scroll to bottom on key press",
FAST_SCROLL: "Enable fastScroll resource",
MOUSE_URXVT: "Enable urxvt Mouse Mode",
MOUSE_SGR_PIXELS: "Enable SGR Mouse PixelMode",
BOLD_ITALIC_HIGH_INTENSITY: "Bold/italic implies high intensity",
# Keyboard and meta key handling modes
META_SETS_EIGHTH_BIT: 'Interpret "meta" key',
MODIFIERS_ALT_NUMLOCK: "Enable special modifiers for Alt and NumLock keys",
META_SENDS_ESC: "Send ESC when Meta modifies a key",
KP_DELETE_SENDS_DEL: "Send DEL from the editing-keypad Delete key",
ALT_SENDS_ESC: "Send ESC when Alt modifies a key",
# Selection, clipboard, and window manager hint modes
KEEP_SELECTION_NO_HILITE: "Keep selection even if not highlighted",
USE_CLIPBOARD_SELECTION: "Use the CLIPBOARD selection",
URGENCY_ON_CTRL_G: "Enable Urgency window manager hint when Control-G is received",
RAISE_ON_CTRL_G: "Enable raising of the window when Control-G is received",
REUSE_CLIPBOARD_DATA: "Reuse the most recent data copied to CLIPBOARD",
EXTENDED_REVERSE_WRAPAROUND: "Extended Reverse-wraparound mode (XTREVWRAP2)",
ALT_SCREEN_BUFFER_SWITCH: "Enable switching to/from Alternate Screen Buffer",
# Alternate screen buffer and cursor save/restore combinations
ALT_SCREEN_BUFFER_XTERM: "Use Alternate Screen Buffer",
SAVE_CURSOR_DECSC: "Save cursor as in DECSC",
ALT_SCREEN_AND_SAVE_CLEAR: "Save cursor as in DECSC and use alternate screen buffer",
# Terminal info and function key emulation modes
TERMINFO_FUNC_KEY_MODE: "Set terminfo/termcap function-key mode",
SUN_FUNC_KEY_MODE: "Set Sun function-key mode",
HP_FUNC_KEY_MODE: "Set HP function-key mode",
SCO_FUNC_KEY_MODE: "Set SCO function-key mode",
# Legacy keyboard emulation modes
LEGACY_KBD_X11R6: "Set legacy keyboard emulation, i.e, X11R6",
VT220_KBD_EMULATION: "Set VT220 keyboard emulation",
SIXEL_PRIVATE_PALETTE: "Use private color registers for each graphic",
# VTE BiDi extensions
BIDI_ARROW_KEY_SWAPPING: "Arrow keys swapping (BiDi)",
# iTerm2 extensions
ITERM2_REPORT_KEY_UP: "Report Key Up",
# XTerm readline and mouse enhancements
READLINE_MOUSE_BUTTON_1: "Enable readline mouse button-1",
READLINE_MOUSE_BUTTON_2: "Enable readline mouse button-2",
READLINE_MOUSE_BUTTON_3: "Enable readline mouse button-3",
BRACKETED_PASTE: "Set bracketed paste mode",
READLINE_CHARACTER_QUOTING: "Enable readline character-quoting",
READLINE_NEWLINE_PASTING: "Enable readline newline pasting",
# Modern terminal extensions
SYNCHRONIZED_OUTPUT: "Synchronized Output",
GRAPHEME_CLUSTERING: "Grapheme Clustering",
TEXT_REFLOW: "Text reflow",
PASSIVE_MOUSE_TRACKING: "Passive Mouse Tracking",
REPORT_GRID_CELL_SELECTION: "Report grid cell selection",
COLOR_PALETTE_UPDATES: "Color palette updates",
IN_BAND_WINDOW_RESIZE: "In-Band Window Resize Notifications",
# VTE bidirectional text extensions
MIRROR_BOX_DRAWING: "Mirror box drawing characters",
BIDI_AUTODETECTION: "BiDi autodetection",
# mintty extensions
AMBIGUOUS_WIDTH_REPORTING: "Ambiguous width reporting",
SCROLL_MARKERS: "Scroll markers (prompt start)",
REWRAP_ON_RESIZE_MINTTY: "Rewrap on resize",
APPLICATION_ESCAPE_KEY: "Application escape key mode",
ESC_KEY_SENDS_BACKSLASH: "Send ^\\ instead of the standard ^[ for the ESC key",
GRAPHICS_POSITION: "Graphics position",
ALT_MODIFIED_MOUSEWHEEL: "Alt-modified mousewheel mode",
SHOW_HIDE_SCROLLBAR: "Show/hide scrollbar",
FONT_CHANGE_REPORTING: "Font change reporting",
GRAPHICS_POSITION_2: "Graphics position",
SHORTCUT_KEY_MODE: "Shortcut key mode",
MOUSEWHEEL_REPORTING: "Mousewheel reporting",
APPLICATION_MOUSEWHEEL: "Application mousewheel mode",
BIDI_CURRENT_LINE: "BiDi on current line",
# Terminal-specific extensions
TTCTH: "Terminal-to-Computer Talk-back Handler",
SIXEL_SCROLLING_LEAVES_CURSOR: "Sixel scrolling leaves cursor to right of graphic",
CHARACTER_MAPPING_SERVICE: "enable/disable character mapping service",
AMBIGUOUS_WIDTH_DOUBLE_WIDTH: "Treat ambiguous width characters as double-width",
WIN32_INPUT_MODE: "win32-input-mode",
KITTY_HANDLE_CTRL_C_Z: "Handle Ctrl-C/Ctrl-Z mode",
MINTTY_BIDI: "BiDi",
INPUT_METHOD_EDITOR: "Input Method Editor (IME) mode",
}
# Reverse lookup mapping from numeric value to mnemonic name
_VALUE_TO_NAME = {v: k for k, v in locals().items()
if k.isupper() and isinstance(v, int)
and not k.startswith('_')}
def __init__(self, value: int):
"""
Initialize DEC Private Mode with numeric identifier.
:param value: Numeric DEC private mode identifier
:type value: int
"""
self.value = int(value)
self.name = self._VALUE_TO_NAME.get(value, "UNKNOWN")
def __repr__(self) -> str:
"""
Return representation in format, ``'NAME(value)'``.
:rtype: str
"""
return f"{self.name}({self.value})"
def __int__(self) -> int:
"""
Return the integer value of this mode.
:rtype: int
"""
return self.value
def __index__(self) -> int:
"""
Return integer value for use in contexts requiring an integer index.
:rtype: int
"""
return self.value
def __eq__(self, other: Any) -> bool:
"""
Compare with another :class:`DecPrivateMode` or int.
:param other: Object to compare with
:type other: DecPrivateMode or int
:rtype: bool
"""
if isinstance(other, DecPrivateMode):
return self.value == other.value
if isinstance(other, int):
return self.value == other
return False
def __hash__(self) -> int:
"""
Return hash based on numeric value.
:rtype: int
"""
return hash(self.value)
@property
def long_description(self) -> str:
"""
Full description of this DEC private mode's functionality.
:rtype: str
:returns: Detailed description or "Unknown mode" for unrecognized modes
"""
return self._LONG_DESCRIPTIONS.get(self.value, "Unknown mode")
jquast-blessed-864a8f7/blessed/formatters.py 0000664 0000000 0000000 00000046242 15107137110 0021176 0 ustar 00root root 0000000 0000000 """Sub-module providing sequence-formatting functions."""
# std imports
import platform
from typing import TYPE_CHECKING, Set, Dict, List, Type, Tuple, Union, TypeVar, Callable, Optional
# local
from blessed.colorspace import CGA_COLORS, X11_COLORNAMES_TO_RGB
if TYPE_CHECKING: # pragma: no cover
# local
from blessed.terminal import Terminal
_T = TypeVar("_T")
# isort: off
# curses
if platform.system() == 'Windows':
import jinxed as curses # pylint: disable=import-error
else:
import curses
def _make_colors() -> Set[str]:
"""
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(f'on_{cga_color}')
colors.add(f'bright_{cga_color}')
colors.add(f'on_bright_{cga_color}')
# foreground and background VGA color
for vga_color in X11_COLORNAMES_TO_RGB:
colors.add(vga_color)
colors.add(f'on_{vga_color}')
return colors
#: Valid colors and their background (on), bright, and bright-background
#: derivatives.
COLORS: Set[str] = _make_colors()
#: Attributes that may be compounded with colors, by underscore, such as
#: 'reverse_indigo'.
COMPOUNDABLES: Set[str] = set('bold underline reverse blink italic standout'.split())
class ParameterizingString(str):
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')
'\x1b[91mcolor #9\x1b(B\x1b[m'
"""
def __new__(cls: Type[_T], cap: str, normal: str = '', name: str = '') -> _T:
"""
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 = str.__new__(cls, cap)
new._normal = normal
new._name = name
return new
def __call__(self, *args: object) -> "FormattingString":
"""
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], str):
raise TypeError(
f"Unknown terminal capability, {self._name!r}, or, TypeError "
f"for arguments {args!r}: {err}") from 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 str(err):
raise
return NullCallableString()
class ParameterizingProxyString(str):
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)
''
>>> fmt = '\x1b[{0}G'
>>> fmt_arg = lambda *arg: (arg[0] + 1,)
>>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa')
>>> hpa(9)
'\x1b[10G'
"""
def __new__(cls: Type[_T], fmt_pair: Tuple[str, Callable[..., Tuple[object, ...]]],
normal: str = '', name: str = '') -> _T:
"""
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 = str.__new__(cls, fmt_pair[0])
new._fmt_args = fmt_pair[1]
new._normal = normal
new._name = name
return new
def __call__(self, *args: object) -> "FormattingString":
"""
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(str):
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))
'\x1b[94m'
>>> style('Big Blue')
'\x1b[94mBig Blue\x1b(B\x1b[m'
"""
def __new__(cls: Type[_T], sequence: str, normal: str = '') -> _T:
"""
Class constructor accepting 2 positional arguments.
:arg str sequence: terminal attribute sequence.
:arg str normal: terminating sequence for this attribute (optional).
"""
new = str.__new__(cls, sequence)
new._normal = normal
return new
def __call__(self, *args: str) -> str:
"""
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, str):
raise TypeError(
f"TypeError for FormattingString argument, {ucs_part!r}, at position {idx}: "
f"expected type {str.__name__}, got {type(ucs_part).__name__}"
)
postfix = ''
if self and self._normal:
postfix = self._normal
_refresh = f'{self._normal}{self}'
args_list = [_refresh.join(ucs_part.split(self._normal))
for ucs_part in args]
args = tuple(args_list)
return f'{self}{"".join(args)}{postfix}'
class FormattingOtherString(str):
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))
'\x1b[C'
>>> print(repr(move_right(666)))
'\x1b[666C'
>>> print(repr(move_right()))
'\x1b[C'
"""
def __new__(cls: Type[_T], direct: ParameterizingString, target: ParameterizingString) -> _T:
"""
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 = str.__new__(cls, direct)
new._callable = target
return new
def __getnewargs__(self) -> Tuple[str, ParameterizingString]:
# return arguments used for the __new__ method upon unpickling.
return str.__new__(str, self), self._callable
def __call__(self, *args: object) -> str:
"""Return ``text`` by ``target``."""
return self._callable(*args) if args else self
class NullCallableString(str):
"""
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: Type[_T]) -> _T:
"""Class constructor."""
return str.__new__(cls, '')
def __call__(self, *args: str) -> str:
"""
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 ''.join(args)
def get_proxy_string(term: 'Terminal', attr: str) -> Optional[ParameterizingProxyString]:
"""
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.kind)
_proxy_table: Dict[str, Dict[str, object]] = { # pragma: no cover
'screen': {
# proxy move_x/move_y for 'screen' terminal type, used by tmux(1).
'hpa': ParameterizingProxyString(
('\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr),
'vpa': ParameterizingProxyString(
('\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(
('\x1b[?25l', lambda *arg: ()), term.normal, attr),
'cnorm': ParameterizingProxyString(
('\x1b[?25h', lambda *arg: ()), term.normal, attr),
'hpa': ParameterizingProxyString(
('\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr),
'vpa': ParameterizingProxyString(
('\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: str) -> List[str]:
"""
Split compound formatting 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 formatting string segments
"""
merged_segs: List[str] = []
# 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] += f'_{segment}'
else:
merged_segs.append(segment)
return merged_segs
def resolve_capability(term: 'Terminal', attr: str) -> str:
"""
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 ('') if not found or not supported by the
given :attr:`~.Terminal.kind`.
:rtype: str
"""
if not term.does_styling:
return ''
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 '\xff'.
return '' if val is None else val.decode('latin1')
def resolve_color(term: 'Terminal', color: str) -> Union[NullCallableString, FormattingString]:
"""
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]
fmt_attr = vga_color_cap(getattr(curses, f'COLOR_{base_color.upper()}') + 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;;;m
# - ^[48;2;;;m
assert term.number_of_colors == 1 << 24
return FormattingString(
f'\x1b[{("48" if "on_" in color else "38")};2;{rgb.red};{rgb.green};{rgb.blue}m',
term.normal
)
def resolve_attribute(term: 'Terminal', attr: str) -> Union[ParameterizingString, FormattingString]:
"""
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(''.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)
jquast-blessed-864a8f7/blessed/keyboard.py 0000664 0000000 0000000 00000260007 15107137110 0020605 0 ustar 00root root 0000000 0000000 """Sub-module providing 'keyboard awareness'."""
# pylint: disable=too-many-lines
# std imports
import os
import re
import time
import typing
import platform
import functools
from typing import TYPE_CHECKING, Set, Dict, Match, Tuple, TypeVar, Optional
from collections import OrderedDict, namedtuple
if TYPE_CHECKING: # pragma: no cover
# local
from blessed.terminal import Terminal
# local
from blessed.mouse import (RE_PATTERN_MOUSE_SGR,
RE_PATTERN_MOUSE_LEGACY,
MouseEvent,
MouseSGREvent,
MouseLegacyEvent)
from blessed.dec_modes import DecPrivateMode
_T = TypeVar('_T', bound='Keystroke')
# 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
# DEC event namedtuples
BracketedPasteEvent = namedtuple('BracketedPasteEvent', 'text')
FocusEvent = namedtuple('FocusEvent', 'gained')
SyncEvent = namedtuple('SyncEvent', 'begin')
ResizeEvent = namedtuple('ResizeEvent', 'height_chars width_chars height_pixels width_pixels')
# Keyboard protocol namedtuples
KittyKeyEvent = namedtuple('KittyKeyEvent',
'unicode_key shifted_key base_key modifiers event_type int_codepoints')
ModifyOtherKeysEvent = namedtuple('ModifyOtherKeysEvent', 'key modifiers')
LegacyCSIKeyEvent = namedtuple('LegacyCSIKeyEvent', 'kind key_id modifiers event_type')
# Regex patterns for keyboard protocols
# Kitty keyboard protocol: ESC [ unicode_key [: shifted_key : base_key]
# [; modifiers [: event_type]] [; text_codepoints] u
RE_PATTERN_KITTY_KB_PROTOCOL = re.compile(
r'\x1b\[(?P\d+)'
r'(?::(?P\d*))?'
r'(?::(?P\d*))?'
r'(?:;(?P\d*))?'
r'(?::(?P\d+))?'
r'(?:;(?P[\d:]+))?'
r'u')
# Legacy CSI modifiers: ESC [ 1 ; modifiers [ABCDEFHPQRS]
RE_PATTERN_LEGACY_CSI_MODIFIERS = re.compile(
r'\x1b\[1;(?P\d+)(?::(?P\d+))?(?P[ABCDEFHPQRS])')
RE_PATTERN_LEGACY_CSI_TILDE = re.compile(
r'\x1b\[(?P\d+);(?P\d+)(?::(?P\d+))?~')
RE_PATTERN_LEGACY_SS3_FKEYS = re.compile(r'\x1bO(?P\d)(?P[PQRS])')
# ModifyOtherKeys: ESC [ 27 ; modifiers ; key [~]
RE_PATTERN_MODIFY_OTHER = re.compile(
r'\x1b\[27;(?P\d+);(?P\d+)(?P~?)')
# Bracketed paste (mode 2004): ESC [ 200 ~ text ESC [ 201 ~
RE_PATTERN_BRACKETED_PASTE = re.compile(r'\x1b\[200~(?P.*?)\x1b\[201~', re.DOTALL)
# Focus tracking (mode 1004): ESC [ I or ESC [ O
RE_PATTERN_FOCUS = re.compile(r'\x1b\[(?P[IO])')
# Resize notification (mode 2048): ESC [ 48 ; height ; width ; height_px ; width_px t
RE_PATTERN_RESIZE = re.compile(r'\x1b\[48;(?P\d+);(?P\d+)'
r';(?P\d+);(?P\d+)t')
# DEC event pattern container
DECEventPattern = functools.namedtuple("DEC_EVENT_PATTERN", ["mode", "pattern"])
# DEC event patterns - compiled regexes with metadata of the 'mode' that
# triggered it, this prevents searching for bracketed paste or mouse modes
# unless it is enabled, and, when enabled, to supplant the match with the DEC
# mode that triggered it. For Mouse modes, there is some order of precedence.
DEC_EVENT_PATTERNS = [
DECEventPattern(mode=DecPrivateMode.BRACKETED_PASTE, pattern=RE_PATTERN_BRACKETED_PASTE),
DECEventPattern(mode=DecPrivateMode.MOUSE_SGR_PIXELS, pattern=RE_PATTERN_MOUSE_SGR),
DECEventPattern(mode=DecPrivateMode.MOUSE_EXTENDED_SGR, pattern=RE_PATTERN_MOUSE_SGR),
DECEventPattern(mode=DecPrivateMode.MOUSE_ALL_MOTION, pattern=RE_PATTERN_MOUSE_LEGACY),
DECEventPattern(mode=DecPrivateMode.MOUSE_REPORT_DRAG, pattern=RE_PATTERN_MOUSE_LEGACY),
DECEventPattern(mode=DecPrivateMode.MOUSE_REPORT_CLICK, pattern=RE_PATTERN_MOUSE_LEGACY),
DECEventPattern(mode=DecPrivateMode.FOCUS_IN_OUT_EVENTS, pattern=RE_PATTERN_FOCUS),
DECEventPattern(mode=DecPrivateMode.IN_BAND_WINDOW_RESIZE, pattern=RE_PATTERN_RESIZE),
]
# Control character mappings
# Note: Ctrl+Space (code 0) is handled specially as 'SPACE', not '@' or ' '.
SYMBOLS_MAP_CTRL_CHAR = {'[': 27, '\\': 28, ']': 29, '^': 30, '_': 31, '?': 127}
SYMBOLS_MAP_CTRL_VALUE = {v: k for k, v in SYMBOLS_MAP_CTRL_CHAR.items()}
# Event type tokens for keystroke predicates
_EVENT_TYPE_TOKENS = {'pressed', 'repeated', 'released'}
# PUA keypad key names mapping (for keys without legacy non-PUA versions)
_PUA_KEYPAD_NAMES = {
57399: 'KEY_KP_0', 57400: 'KEY_KP_1', 57401: 'KEY_KP_2', 57402: 'KEY_KP_3',
57403: 'KEY_KP_4', 57404: 'KEY_KP_5', 57405: 'KEY_KP_6', 57406: 'KEY_KP_7',
57407: 'KEY_KP_8', 57408: 'KEY_KP_9', 57409: 'KEY_KP_DECIMAL',
57410: 'KEY_KP_DIVIDE', 57411: 'KEY_KP_MULTIPLY', 57412: 'KEY_KP_SUBTRACT',
57413: 'KEY_KP_ADD', 57415: 'KEY_KP_EQUAL', 57416: 'KEY_KP_SEPARATOR',
}
# Alt-only control character name mappings
ALT_CONTROL_NAMES = {
0x1b: 'KEY_ALT_ESCAPE', # ESC
0x7f: 'KEY_ALT_BACKSPACE', # DEL
0x0d: 'KEY_ALT_ENTER', # CR
0x09: 'KEY_ALT_TAB', # TAB
0x5b: 'CSI' # CSI '['
}
class KittyModifierBits:
"""Standard modifier bit flags (compatible with Kitty keyboard protocol)."""
# pylint: disable=too-few-public-methods
shift = 0b1
alt = 0b10
ctrl = 0b100
super = 0b1000
hyper = 0b10000
meta = 0b100000
caps_lock = 0b1000000
num_lock = 0b10000000
#: Names of bitwise flags attached to this class
names = ('shift', 'alt', 'ctrl', 'super', 'hyper', 'meta',
'caps_lock', 'num_lock')
#: Modifiers only, in the generally preferred order in phrasing
names_modifiers_only = ('ctrl', 'alt', 'shift', 'super', 'hyper', 'meta')
class Keystroke(str):
"""
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``. Note that keystrokes may
also represent mouse input, bracketed paste, or focus in/out events
depending on enabled terminal modes.
The string :attr:`name` of the sequence is used to identify in code logic,
such as ``'KEY_LEFT'`` to represent a common and human-readable form of
the Keystroke this class instance represents.
"""
_name: Optional[str] = None
_code: Optional[int] = None
_mode: Optional[int] = None
_match: typing.Any = None
_modifiers: int = 1
def __new__(cls: typing.Type[_T], ucs: str = '', code: Optional[int] = None,
name: Optional[str] = None, mode: Optional[int] = None,
match: typing.Any = None) -> _T:
# pylint: disable=too-many-positional-arguments
"""Class constructor."""
new = str.__new__(cls, ucs)
new._name = name
new._code = code # curses keycode is exposed for legacy API
new._mode = mode # Internal mode indicator for different protocols
new._match = match # regex match object for protocol-specific data
new._modifiers = cls._infer_modifiers(ucs, mode, match)
return new
@staticmethod
def _infer_modifiers(ucs: str, mode: Optional[int], match: typing.Any) -> int:
"""
Infer modifiers from keystroke data.
Returns modifiers in standard format: 1 + bitwise OR of modifier flags.
"""
# ModifyOtherKeys or Legacy CSI modifiers
if mode is not None and mode < 0 and match is not None:
return match.modifiers
# Legacy sequences starting with ESC (metaSendsEscape)
if len(ucs) == 2 and ucs[0] == '\x1b':
char_code = ord(ucs[1])
# Special C0 controls that should be Alt-only per legacy spec
# These represent common Alt+key combinations that are unambiguous
# (Enter, Escape, DEL, Tab)
if char_code in {0x0d, 0x1b, 0x7f, 0x09}:
return 1 + KittyModifierBits.alt # 1 + alt flag = 3
# Other control characters represent Ctrl+Alt combinations
# (ESC prefix for Alt + control char from Ctrl+letter mapping)
if 0 <= char_code <= 31 or char_code == 127:
# 1 + alt flag + ctrl flag = 7
return 1 + KittyModifierBits.alt + KittyModifierBits.ctrl
# Printable characters - Alt-only unless uppercase letter (which adds Shift)
if 32 <= char_code <= 126:
ch = ucs[1]
shift = KittyModifierBits.shift if ch.isalpha() and ch.isupper() else 0
return 1 + KittyModifierBits.alt + shift
# Legacy Ctrl: single control character
if len(ucs) == 1:
char_code = ord(ucs)
if 0 <= char_code <= 31 or char_code == 127:
return 1 + KittyModifierBits.ctrl # 1 + ctrl flag = 5
# No modifiers detected
return 1
@property
def is_sequence(self) -> bool:
"""Whether the value represents a multibyte sequence (bool)."""
return self._code is not None or self._mode is not None or len(self) > 1
def __repr__(self) -> str:
"""Docstring overwritten."""
return (str.__repr__(self) if self._name is None else
self._name)
__repr__.__doc__ = str.__doc__
def _get_modified_keycode_name(self) -> Optional[str]:
"""
Get name for modern/legacy CSI sequence with modifiers.
Returns name like 'KEY_CTRL_ALT_F1' or 'KEY_SHIFT_UP_RELEASED'. Also handles release/repeat
events for keys without modifiers.
"""
# Check if this is a special keyboard protocol mode
if not (self.uses_keyboard_protocol and self._code is not None):
return None
# turn keycode value into 'base name', eg.
# self._code of 265 -> 'KEY_F1' -> 'F1' base_name
keycodes = get_keyboard_codes()
base_name = keycodes.get(self._code)
# handle PUA keypad keys that aren't in get_keyboard_codes()
if not base_name and 57399 <= self._code <= 57416: # Keypad PUA range
base_name = _PUA_KEYPAD_NAMES.get(self._code)
if not base_name or not base_name.startswith('KEY_'):
return None
# get "base name" name by, 'KEY_F1' -> 'F1'
base_name = base_name[4:]
# Build possible modifier prefix series (excludes num/capslock)
# "Ctrl + Alt + Shift + Super / Meta"
mod_parts = []
for mod_name in KittyModifierBits.names_modifiers_only:
if getattr(self, f'_{mod_name}'): # 'if self._shift'
mod_parts.append(mod_name.upper()) # -> 'SHIFT'
# For press events with no modifiers, check if this is a PUA functional
# key or a control character key (Escape, Tab, Enter, Backspace).
is_control_char_key = self._code in _KITTY_CONTROL_CHAR_TO_KEYCODE.values()
if (not mod_parts
and not (self.released or self.repeated)
and not _is_kitty_functional_key(self._code)
and not is_control_char_key):
return None
# Build base result with modifiers (if any)
base_result = (f"KEY_{'_'.join(mod_parts)}_{base_name}"
if mod_parts
else f"KEY_{base_name}")
# Append event type suffix if not a press event
if self.repeated:
return f"{base_result}_REPEATED"
if self.released:
return f"{base_result}_RELEASED"
return base_result # pressed (no suffix)
def _get_kitty_protocol_name(self) -> Optional[str]:
"""
Get name for Kitty keyboard protocol letter/digit/symbol.
Returns name like 'KEY_CTRL_ALT_A', 'KEY_ALT_SHIFT_5', 'KEY_CTRL_J_RELEASED', etc.
"""
# pylint: disable=too-many-return-statements
if self._mode != DecPrivateMode.SpecialInternalKitty:
return None
# Determine the base key - prefer base_key if available
base_codepoint = (self._match.base_key if self._match.base_key is not None
else self._match.unicode_key)
# Special case: '[' always returns 'CSI' regardless of modifiers
if base_codepoint == 91: # '['
return 'CSI'
# Only proceed if it's an ASCII letter or digit
if not ((65 <= base_codepoint <= 90) or # A-Z
(97 <= base_codepoint <= 122) or # a-z
(48 <= base_codepoint <= 57)): # 0-9
return None
# For letters: convert to uppercase for consistent naming
# For digits: use as-is
char = (chr(base_codepoint).upper()
if (65 <= base_codepoint <= 90 or 97 <= base_codepoint <= 122)
else chr(base_codepoint))
# Build modifier prefix list in order: CTRL, ALT, SHIFT, SUPER, HYPER, META
mod_parts = []
for mod_name in KittyModifierBits.names_modifiers_only:
if getattr(self, f'_{mod_name}'):
mod_parts.append(mod_name.upper())
# Only synthesize name if at least one modifier is present
if not mod_parts:
return None
base_result = f"KEY_{'_'.join(mod_parts)}_{char}"
# Append event type suffix if not a press event
if self.repeated:
return f"{base_result}_REPEATED"
if self.released:
return f"{base_result}_RELEASED"
return base_result # pressed (no suffix)
def _get_control_char_name(self) -> Optional[str]:
"""
Get name for single-character control sequences.
Returns name like 'KEY_CTRL_A' or 'KEY_CTRL_SPACE'.
"""
if len(self) != 1:
return None
char_code = ord(self)
if char_code == 0:
return 'KEY_CTRL_SPACE'
if 1 <= char_code <= 26:
# Ctrl+A through Ctrl+Z
return f'KEY_CTRL_{chr(char_code + ord("A") - 1)}'
if char_code in SYMBOLS_MAP_CTRL_VALUE:
return f'KEY_CTRL_{SYMBOLS_MAP_CTRL_VALUE[char_code]}'
return None
def _get_control_symbol(self, char_code: int) -> str:
"""
Get control symbol for a character code.
Returns symbol like 'A' for Ctrl+A, 'SPACE' for Ctrl+Space, 'BACKSPACE' for Ctrl+H, etc.
"""
if char_code == 0:
return 'SPACE'
# Special case: Ctrl+H (Backspace) sends \x08
if char_code == 8:
return 'BACKSPACE'
if 1 <= char_code <= 26:
# Ctrl+A through Ctrl+Z
return chr(char_code + ord("A") - 1)
# Ctrl+symbol
return SYMBOLS_MAP_CTRL_VALUE[char_code]
def _get_alt_only_control_name(self, char_code: int) -> Optional[str]:
"""
Get name for Alt-only special control characters.
Returns names like 'KEY_ALT_ESCAPE', 'KEY_ALT_BACKSPACE', etc.
"""
return ALT_CONTROL_NAMES.get(char_code)
def _get_meta_escape_name(self) -> Optional[str]:
"""
Get name for metaSendsEscape sequences (ESC + char).
Returns name like 'KEY_ALT_A', 'KEY_ALT_SHIFT_Z', 'KEY_CTRL_ALT_C', or 'KEY_ALT_ESCAPE'.
"""
# pylint: disable=too-many-return-statements
if not self._is_escape_sequence():
return None
char_code = ord(self[1])
# Check for ESC + control char
if 0 <= char_code <= 31 or char_code == 127:
symbol = self._get_control_symbol(char_code)
# Check if this is Alt-only or Ctrl+Alt based on modifiers
if self.modifiers == 3: # Alt-only (1 + 2)
# Special C0 controls that are Alt-only
return self._get_alt_only_control_name(char_code)
if self.modifiers == 7: # Ctrl+Alt (1 + 2 + 4)
return f'KEY_CTRL_ALT_{symbol}'
# return KEY_ALT_ for "metaSendsEscape"
ch = self[1]
if ch.isalpha():
if ch.isupper():
return f'KEY_ALT_SHIFT_{ch}'
return f'KEY_ALT_{ch.upper()}'
if ch == '[':
return 'CSI'
if ch == ' ':
return 'KEY_ALT_SPACE'
return f'KEY_ALT_{ch}'
def _get_mouse_event_name(self) -> Optional[str]:
"""
Get name for mouse events.
Returns name like 'MOUSE_LEFT', 'MOUSE_CTRL_LEFT', 'MOUSE_SCROLL_UP', 'MOUSE_LEFT_RELEASED',
'MOUSE_MOTION', 'MOUSE_RIGHT_MOTION', etc.
"""
# Check if this is a mouse mode
if self._mode not in (DecPrivateMode.MOUSE_EXTENDED_SGR,
DecPrivateMode.MOUSE_SGR_PIXELS,
DecPrivateMode.MOUSE_REPORT_CLICK,
DecPrivateMode.MOUSE_HILITE_TRACKING,
DecPrivateMode.MOUSE_REPORT_DRAG,
DecPrivateMode.MOUSE_ALL_MOTION):
return None
# Get the button name from _mode_values
mouse_event = self._mode_values
if not isinstance(mouse_event, MouseEvent):
return None
# Return MOUSE_ prefix + button name
return f'MOUSE_{mouse_event.button}'
def _get_focus_event_name(self) -> Optional[str]:
"""
Get name for focus events.
Returns 'FOCUS_IN' or 'FOCUS_OUT'.
"""
if self._mode != DecPrivateMode.FOCUS_IN_OUT_EVENTS:
return None
# Check the io group to determine if focus was gained or lost
if self._match is not None and self._match.group('io') == 'I':
return 'FOCUS_IN'
if self._match is not None and self._match.group('io') == 'O':
return 'FOCUS_OUT'
return None
def _get_bracketed_paste_name(self) -> Optional[str]:
"""
Get name for bracketed paste events.
Returns 'BRACKETED_PASTE'.
"""
if self._mode == DecPrivateMode.BRACKETED_PASTE:
return 'BRACKETED_PASTE'
return None
@property
def name(self) -> Optional[str]: # pylint: disable=too-many-return-statements
r"""
Special application key name.
This is the best equality attribute to use for special keys, as raw string value of the 'F1'
key can be received in many different values.
The 'name' property will return a reliable constant, eg. ``'KEY_F1'``.
The name supports "modifiers", such as ``'KEY_CTRL_F1'``,
``'KEY_CTRL_ALT_F1'``, ``'KEY_CTRL_ALT_SHIFT_F1'``
For mouse events, the name includes the ``'MOUSE_'`` prefix followed by the
button/action name, such as ``'MOUSE_LEFT'``, ``'MOUSE_MOTION'``,
``'MOUSE_RIGHT_MOTION'``, ``'MOUSE_LEFT_RELEASED'``.
For other DEC events:
- Focus events: 'FOCUS_IN' or 'FOCUS_OUT'
- Bracketed paste: 'BRACKETED_PASTE'
- Resize events: 'RESIZE_EVENT'
When non-None, all phrases begin with either 'KEY', 'MOUSE', 'FOCUS_IN', 'FOCUS_OUT',
'BRACKETED_PASTE', or 'RESIZE_EVENT', with one exception: 'CSI' is returned for '\\x1b['
to indicate the beginning of a presumed unsupported input sequence. The phrase 'KEY_ALT_['
is never returned and unsupported.
If this value is None, then it can probably be assumed that the value is an unsurprising
textual character without any modifiers, like the letter ``'a'``.
"""
if self._name is not None:
return self._name
# Try each helper method in sequence
# DEC events first
result = self._get_mouse_event_name()
if result is not None:
return result
result = self._get_focus_event_name()
if result is not None:
return result
result = self._get_bracketed_paste_name()
if result is not None:
return result
# Inline resize event check
if self._mode == DecPrivateMode.IN_BAND_WINDOW_RESIZE:
return 'RESIZE_EVENT'
# Keyboard events
result = self._get_modified_keycode_name()
if result is not None:
return result
result = self._get_kitty_protocol_name()
if result is not None:
return result
result = self._get_control_char_name()
if result is not None:
return result
result = self._get_meta_escape_name()
if result is not None:
return result
return self._name
@property
def code(self) -> Optional[int]:
"""Legacy curses-alike keycode value (int)."""
return self._code
@property
def modifiers(self) -> int:
"""
Modifier flags in standard keyboard protocol format.
:rtype: int
:returns: Standard-style modifiers value (1 means no modifiers)
The value is 1 + bitwise OR of modifier flags:
- shift: 0b1 (1)
- alt: 0b10 (2)
- ctrl: 0b100 (4)
- super: 0b1000 (8)
- hyper: 0b10000 (16)
- meta: 0b100000 (32)
- caps_lock: 0b1000000 (64)
- num_lock: 0b10000000 (128)
"""
return self._modifiers
@property
def modifiers_bits(self) -> int:
"""
Raw modifier bit flags without the +1 offset.
:rtype: int
:returns: Raw bitwise OR of modifier flags (0 means no modifiers)
"""
return max(0, self._modifiers - 1)
# Private modifier flag properties (internal use)
@property
def _shift(self) -> bool:
"""Whether the shift modifier is active."""
return bool(self.modifiers_bits & KittyModifierBits.shift)
@property
def _alt(self) -> bool:
"""Whether the alt modifier is active."""
return bool(self.modifiers_bits & KittyModifierBits.alt)
@property
def _ctrl(self) -> bool:
"""Whether the ctrl modifier is active."""
return bool(self.modifiers_bits & KittyModifierBits.ctrl)
@property
def _super(self) -> bool:
"""Whether the super (Windows/Cmd) modifier is active."""
return bool(self.modifiers_bits & KittyModifierBits.super)
@property
def _hyper(self) -> bool:
"""Whether the hyper modifier is active."""
return bool(self.modifiers_bits & KittyModifierBits.hyper)
@property
def _meta(self) -> bool:
"""Whether the meta modifier is active."""
return bool(self.modifiers_bits & KittyModifierBits.meta)
@property
def _caps_lock(self) -> bool:
"""Whether caps lock was known to be active during this sequence."""
return bool(self.modifiers_bits & KittyModifierBits.caps_lock)
@property
def _num_lock(self) -> bool:
"""Whether num lock was known to be active during this sequence."""
return bool(self.modifiers_bits & KittyModifierBits.num_lock)
@property
def uses_keyboard_protocol(self) -> bool:
"""
Whether this keystroke uses a special keyboard protocol mode.
Returns True for Kitty, ModifyOtherKeys, or LegacyCSIModifier protocols, which use negative
mode values (SpecialInternalKitty=-1, SpecialInternalModifyOtherKeys=-2,
SpecialInternalLegacyCSIModifier=-3).
:rtype: bool
:returns: True if using special keyboard protocol mode
"""
return self._mode is not None and self._mode < 0
@property
def pressed(self) -> bool:
"""
Whether this is a key press event.
:rtype: bool
:returns: True if this is a key press event (event_type=1 or not specified), False for
repeat or release events
"""
if self.uses_keyboard_protocol:
# Check if _match has event_type (Kitty, LegacyCSI, ModifyOtherKeys),
# defaulting to 1 (pressed) if not present.
return getattr(self._match, 'event_type', 1) == 1
# Default: always a 'pressed' event
return True
@property
def repeated(self) -> bool:
"""
Whether this is a key repeat event.
:rtype: bool
:returns: True if this is a key repeat event (event_type=2), False otherwise
"""
if self.uses_keyboard_protocol:
return getattr(self._match, 'event_type', 1) == 2
# Default: not a repeat event
return False
@property
def released(self) -> bool:
"""
Whether this is a key release event.
:rtype: bool
:returns: True if this is a key release event (event_type=3), False otherwise
"""
if self.uses_keyboard_protocol:
return getattr(self._match, 'event_type', 1) == 3
# Default: not a release event
return False
def _is_escape_sequence(self, length: int = 2) -> bool:
"""
Check if keystroke is an escape sequence of given length.
:arg int length: Expected length of escape sequence (default 2)
:rtype: bool
:returns: True if keystroke matches ESC + (length-1) chars pattern
"""
return len(self) == length and self[0] == '\x1b'
@staticmethod
def _make_expected_bits(tokens_modifiers: typing.List[str]) -> int:
"""Build expected modifier bits from token list."""
expected_bits = 0
for token in tokens_modifiers:
expected_bits |= getattr(KittyModifierBits, token)
return expected_bits
def _make_effective_bits(self) -> int:
"""Returns modifier bits stripped of caps_lock and num_lock."""
stripped_bits = KittyModifierBits.caps_lock | KittyModifierBits.num_lock
return self.modifiers_bits & ~(stripped_bits)
@staticmethod
def _get_keycode_by_name(key_name: str) -> Optional[int]:
"""Get keycode value for a given key name."""
keycodes = get_keyboard_codes()
expected_key_constant = f'KEY_{key_name.upper()}'
for code, name in keycodes.items():
if name == expected_key_constant:
return code
return None
def _build_appkeys_predicate(self, tokens_modifiers: typing.List[str], key_name: str,
event_type: Optional[str] = None
) -> typing.Callable[[Optional[str], bool], bool]:
"""Build a predicate function for application keys."""
def keycode_predicate(char: Optional[str] = None, ignore_case: bool = True) -> bool:
# pylint: disable=unused-argument
# char and ignore_case parameters are accepted but not used for application keys
# Application keys never match when 'char' is non-None/non-Empty
if char:
return False
# Get expected keycode from key name
expected_code = Keystroke._get_keycode_by_name(key_name)
if expected_code is None or self._code != expected_code:
return False
# Validate modifiers
if self._make_expected_bits(tokens_modifiers) != self._make_effective_bits():
return False
# Check event type if specified
if event_type is not None:
event_type_map = {
'pressed': self.pressed,
'repeated': self.repeated,
'released': self.released
}
return event_type_map.get(event_type, False)
return True
return keycode_predicate
def _build_alphanum_predicate(self, tokens_modifiers: typing.List[str]
) -> typing.Callable[[Optional[str], bool], bool]:
"""Build a predicate function for modifier checking of alphanumeric input."""
def modifier_predicate(char: Optional[str] = None, ignore_case: bool = True) -> bool:
# Build expected modifier bits from tokens,
# Stripped to ignore caps_lock and num_lock
expected_bits = self._make_expected_bits(tokens_modifiers)
effective_bits = self._make_effective_bits()
# When matching with a character and it's alphabetic, be lenient
# about Shift because it is implicit in the case of the letter
if char and len(char) == 1 and char.isalpha():
# Strip shift from both sides for letter matching
effective_bits_no_shift = effective_bits & ~KittyModifierBits.shift
expected_bits_no_shift = expected_bits & ~KittyModifierBits.shift
if effective_bits_no_shift != expected_bits_no_shift:
return False
elif effective_bits != expected_bits:
# Exact matching (no char, or non-alpha char)
return False
# If no character specified, always return False
# Text keys need char argument: is_ctrl('a')
# Application keys need specific predicate: is_ctrl_up()
if char is None:
return False
# Check character match using value property
keystroke_char = self.value
# Compare characters
if ignore_case:
return keystroke_char.lower() == char.lower()
return keystroke_char == char
return modifier_predicate
# pylint: disable=too-complex
def __getattr__(self, attr: str) -> typing.Callable[[Optional[str], bool], bool]:
"""
Dynamic compound modifier and application key predicates via __getattr__.
Recognizes attributes starting with "is_" and parses underscore-separated
tokens to create dynamic predicate functions.
:arg str attr: Attribute name being accessed
:rtype: callable or raises AttributeError
:returns: Callable predicate function with signature
``Callable[[Optional[str], bool], bool]``.
All predicates accept the same parameters:
- ``char`` (Optional[str]): Character to match against keystroke value
- ``ignore_case`` (bool): Whether to ignore case when matching characters
For event predicates, application key predicates, and mouse button predicates,
these parameters are accepted but not used.
Mouse button predicates use the pattern ``is_mouse_