pax_global_header00006660000000000000000000000064151071371100014506gustar00rootroot0000000000000052 comment=864a8f7d6de752d7fede2c030758d245f1bb8e21 jquast-blessed-864a8f7/000077500000000000000000000000001510713711000150055ustar00rootroot00000000000000jquast-blessed-864a8f7/.editorconfig000066400000000000000000000004401510713711000174600ustar00rootroot00000000000000root = 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/000077500000000000000000000000001510713711000163455ustar00rootroot00000000000000jquast-blessed-864a8f7/.github/workflows/000077500000000000000000000000001510713711000204025ustar00rootroot00000000000000jquast-blessed-864a8f7/.github/workflows/tests.yml000066400000000000000000000044001510713711000222650ustar00rootroot00000000000000name: 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/.gitignore000066400000000000000000000004211510713711000167720ustar00rootroot00000000000000.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.yml000066400000000000000000000004221510713711000200710ustar00rootroot00000000000000# 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.txt000066400000000000000000000000341510713711000220530ustar00rootroot00000000000000padd parms iTerm THIRDPARTY jquast-blessed-864a8f7/CONTRIBUTING.rst000066400000000000000000000022521510713711000174470ustar00rootroot00000000000000Contributing ============ 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/LICENSE000066400000000000000000000020731510713711000160140ustar00rootroot00000000000000Copyright (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.in000066400000000000000000000003131510713711000165400ustar00rootroot00000000000000graft 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.rst000077700000000000000000000000001510713711000213042docs/intro.rstustar00rootroot00000000000000jquast-blessed-864a8f7/bin/000077500000000000000000000000001510713711000155555ustar00rootroot00000000000000jquast-blessed-864a8f7/bin/bounce.py000077500000000000000000000014701510713711000174070ustar00rootroot00000000000000#!/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.py000066400000000000000000000026571510713711000167170ustar00rootroot00000000000000"""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.py000077500000000000000000000011211510713711000204700ustar00rootroot00000000000000#!/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.py000066400000000000000000000050251510713711000202710ustar00rootroot00000000000000""" 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.py000066400000000000000000000007451510713711000232570ustar00rootroot00000000000000#!/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.py000066400000000000000000000006751510713711000212600ustar00rootroot00000000000000#!/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.py000066400000000000000000000011421510713711000212740ustar00rootroot00000000000000#!/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.py000066400000000000000000000004101510713711000214150ustar00rootroot00000000000000#!/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.py000066400000000000000000000011451510713711000226510ustar00rootroot00000000000000#!/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.py000077500000000000000000000054211510713711000214200ustar00rootroot00000000000000#!/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.py000077500000000000000000000040171510713711000215470ustar00rootroot00000000000000#!/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.py000077500000000000000000000121621510713711000207060ustar00rootroot00000000000000#!/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.py000077500000000000000000000021111510713711000220730ustar00rootroot00000000000000#!/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.py000077500000000000000000000157421510713711000222750ustar00rootroot00000000000000#!/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.py000077500000000000000000000006521510713711000212650ustar00rootroot00000000000000#!/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.py000077500000000000000000000201451510713711000174220ustar00rootroot00000000000000#!/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.py000066400000000000000000000031231510713711000215240ustar00rootroot00000000000000# 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.py000066400000000000000000000051161510713711000224710ustar00rootroot00000000000000# 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.py000077500000000000000000000006431510713711000217740ustar00rootroot00000000000000#!/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.py000077500000000000000000000015571510713711000223470ustar00rootroot00000000000000#!/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.py000066400000000000000000000012021510713711000225170ustar00rootroot00000000000000#!/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.py000077500000000000000000000007021510713711000226140ustar00rootroot00000000000000#!/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.py000077500000000000000000000002251510713711000213020ustar00rootroot00000000000000#!/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.py000077500000000000000000000003641510713711000224700ustar00rootroot00000000000000#!/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.py000077500000000000000000000376341510713711000201640ustar00rootroot00000000000000#!/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.py000066400000000000000000000007271510713711000206360ustar00rootroot00000000000000#!/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.py000066400000000000000000000006251510713711000202570ustar00rootroot00000000000000#!/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.py000077500000000000000000000016641510713711000213320ustar00rootroot00000000000000#!/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.py000077500000000000000000000025771510713711000204700ustar00rootroot00000000000000#!/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.py000066400000000000000000000007761510713711000206550ustar00rootroot00000000000000#!/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.py000077500000000000000000000011301510713711000205020ustar00rootroot00000000000000#!/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.py000066400000000000000000000006511510713711000206320ustar00rootroot00000000000000#!/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.py000077500000000000000000000023351510713711000201320ustar00rootroot00000000000000#!/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.py000077500000000000000000000075661510713711000174250ustar00rootroot00000000000000#!/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.py000077500000000000000000000026231510713711000206250ustar00rootroot00000000000000#!/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.py000077500000000000000000000061251510713711000174370ustar00rootroot00000000000000#!/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.py000077500000000000000000000015551510713711000205110ustar00rootroot00000000000000#!/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.py000077500000000000000000000004711510713711000172750ustar00rootroot00000000000000#!/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.py000077500000000000000000000014331510713711000174530ustar00rootroot00000000000000#!/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.py000077500000000000000000000202671510713711000173100ustar00rootroot00000000000000#!/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.py000066400000000000000000000071741510713711000211450ustar00rootroot00000000000000# 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/000077500000000000000000000000001510713711000164265ustar00rootroot00000000000000jquast-blessed-864a8f7/blessed/__init__.py000066400000000000000000000005521510713711000205410ustar00rootroot00000000000000""" 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.py000066400000000000000000000147221510713711000215760ustar00rootroot00000000000000"""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.py000066400000000000000000000250511510713711000201210ustar00rootroot00000000000000""" 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.py000066400000000000000000001051341510713711000211360ustar00rootroot00000000000000""" 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.py000066400000000000000000000536511510713711000207340ustar00rootroot00000000000000"""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.py000066400000000000000000000462421510713711000211760ustar00rootroot00000000000000"""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.py000066400000000000000000002600071510713711000206050ustar00rootroot00000000000000"""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_