diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..9109cd45
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,17 @@
+root = true
+
+[*.json]
+charset = utf-8
+tab_width = 4
+indent_size = tab
+indent_space = space
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.py]
+charset = utf-8
+tab_width = 4
+indent_size = tab
+indent_space = space
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
index 94748175..66b09dc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,18 @@
+.coverage
+._coverage.*
+.coverage.*
+.cache
.tox
*.egg-info
*.egg
*.pyc
+results*.xml
build
dist
-docs/_build
\ No newline at end of file
+docs/_build
+htmlcov
+.coveralls.yml
+.DS_Store
+.*.sw?
+.vscode
+.python-version
diff --git a/.landscape.yml b/.landscape.yml
new file mode 100644
index 00000000..578295ea
--- /dev/null
+++ b/.landscape.yml
@@ -0,0 +1,100 @@
+inherits:
+ - strictness_veryhigh
+
+ignore-patterns:
+ - (^|/)\..+
+ - ^docs/
+ - ^build/
+ # ignore these, their quality does not so much matter.
+ - ^blessed/tests/
+ - ^tools/
+
+test-warnings: true
+
+output-format: grouped
+
+dodgy:
+ # Looks at Python code to search for things which look "dodgy"
+ # such as passwords or git conflict artifacts
+ run: true
+
+pyflakes:
+ # preferring 'frosted' instead (a fork of)
+ run: true
+ disable:
+ # Terminal imported but unused (false)
+ - 'E101'
+
+frosted:
+ run: false
+
+mccabe:
+ # complexity checking. We know of only one offense of the
+ # default of 10, which is Terminal.inkey() at exactly 10.
+ # It is not easily avoided, likely removing deprecation
+ # warnings in a future release will help reduce it.
+ run: false
+ options:
+ max-complexity: 11
+
+pep257:
+ # docstring checking
+ run: true
+ disable:
+ # 1 blank line required before class docstring
+ - 'D203'
+ # 1 blank line required after class docstring
+ - 'D204'
+ # Multi-line docstring summary should start at the first line
+ - 'D212'
+ # First line should be in imperative mood
+ - 'D401'
+
+pep8:
+ # style checking
+ run: true
+
+pylint:
+ # static analysis and then some
+ run: true
+ options:
+ # pytest module has dynamically assigned functions,
+ # raising errors such as: E1101: Module 'pytest' has
+ # no 'mark' member
+ ignored-classes: pytest
+ # List of builtins function names that should not be used, separated by a comma
+ # bad-functions=map,filter,input
+ bad-functions: input
+ persistent: no
+ # 'ks' is often-cited variable representing Keystroke instance.
+ # 'fd' is a common shorthand term for file descriptor (as int).
+ good-names: _,ks,fd
+
+
+ disable:
+ - protected-access
+ - too-few-public-methods
+ - star-args
+ - wrong-import-order
+ - wrong-import-position
+ - ungrouped-imports
+ - useless-object-inheritance
+
+pyroma:
+ # checks setup.py
+ run: true
+ disable:
+ # > Setuptools and Distribute support running tests. By specifying a test
+ # > suite, it's easy to find and run tests both for automated tools and humans.
+ #
+ # https://pytest.org/latest/goodpractises.html#integration-with-setuptools-test-commands
+ # > Most often it is better to use tox instead
+ #
+ # we agree.
+ - 'PYR15'
+
+vulture:
+ # this tool does a good job of finding unused code.
+ run: false
+
+# vim: noai:ts=4:sw=4
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..9714cf85
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,48 @@
+[MASTER]
+load-plugins=
+ pylint.extensions.mccabe,
+ pylint.extensions.check_elif,
+ pylint.extensions.docparams,
+ pylint.extensions.overlapping_exceptions,
+ pylint.extensions.redefined_variable_type
+
+persistent = no
+jobs = 0
+unsafe-load-any-extension = yes
+
+[MESSAGES CONTROL]
+disable=
+ I,
+ fixme,
+ c-extension-no-member,
+ ungrouped-imports
+
+[FORMAT]
+max-line-length: 100
+good-names=ks,fd,_
+
+[PARAMETER_DOCUMENTATION]
+default-docstring-type=sphinx
+accept-no-raise-doc=no
+accept-no-param-doc=yes
+accept-no-return-doc=yes
+
+[DESIGN]
+max-args=10
+max-attributes=7
+max-branches=12
+max-complexity=11
+max-locals=15
+max-module-lines=1300
+max-parents=7
+max-public-methods=20
+max-returns=6
+max-statements=50
+
+[SIMILARITIES]
+ignore-imports=yes
+min-similarity-lines=8
+
+[REPORTS]
+reports=no
+msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
diff --git a/.travis.yml b/.travis.yml
index 41bab6be..5f6e9c19 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,17 +1,46 @@
language: python
-
-python:
- - 2.6
- - 2.7
- - 3.2
- - pypy
-
matrix:
+ fast_finish: true
+ include:
+ - python: 2.7
+ env: TOXENV=py27,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci
+ - python: 3.4
+ env: TOXENV=py34,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci
+ - python: 3.5
+ env: TOXENV=py35,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci
+ - python: 3.6
+ env: TOXENV=py36,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci
+ - python: 3.7
+ env: TOXENV=py37,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci
+ - python: 3.8
+ env: TOXENV=py38,codecov COVERAGE_ID=travis-ci
+ - python: 3.9-dev
+ env: TOXENV=py39,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci
+ - python: 3.8
+ env: TOXENV=about,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci
+ - python: 3.8
+ os: windows
+ language: shell
+ before_install:
+ - choco install python --version 3.8.0
+ - python -m pip install --upgrade pip
+ - python -m pip install tox
+ env: PATH=/c/Python38:/c/Python38/Scripts:$PATH TOXENV=py38,codecov COVERAGE_ID=travis-ci TEST_KEYBOARD=no
+
+jobs:
allow_failures:
- - python: 3.2 # Not a new enough 3.2 for blessings
- - python: pypy # PyPy doesn't have _curses.
+ - env: TOXENV=py39,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci
+ - env: TOXENV=about,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci
+install:
+ - pip install tox
script:
- - pip install -q --use-mirrors nose
- - python setup.py install
- - nosetests -w /tmp blessings.tests
+ - tox
+sudo: false
+
+notifications:
+ email:
+ recipients:
+ - contact@jeffquast.com
+ on_success: change
+ on_failure: change
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
new file mode 100644
index 00000000..af76aaf3
--- /dev/null
+++ b/CONTRIBUTING.rst
@@ -0,0 +1,47 @@
+Contributing
+============
+
+We welcome contributions via GitHub pull requests:
+
+- `Fork a Repo `_
+- `Creating a pull request
+ `_
+
+Developing
+----------
+
+Prepare a developer environment. Then, from the blessed code folder::
+
+ pip install --editable .
+
+Any changes made in this project folder are then made available to the python
+interpreter as the 'blessed' package from any working directory.
+
+Running Tests
+~~~~~~~~~~~~~
+
+Install and run tox
+
+::
+
+ pip install --upgrade tox
+ tox
+
+Py.test is used as the test runner, supporting positional arguments, you may
+for example use `looponfailing
+`_
+with python 3.5, stopping at the first failing test case, and looping
+(retrying) after a filesystem save is detected::
+
+ tox -epy35 -- -fx
+
+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.
diff --git a/LICENSE b/LICENSE
index 3d3a44e6..4b071328 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,4 @@
+Copyright (c) 2014 Jeff Quast
Copyright (c) 2011 Erik Rose
Permission is hereby granted, free of charge, to any person obtaining a copy of
diff --git a/MANIFEST.in b/MANIFEST.in
index 3f4fbd70..1ce192c3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,6 @@
-include README.rst
+include docs/*.rst
include LICENSE
+include version.json
+include *.txt
include tox.ini
+recursive-include tests *.py *.ans
diff --git a/README.rst b/README.rst
deleted file mode 100644
index f6fc8789..00000000
--- a/README.rst
+++ /dev/null
@@ -1,520 +0,0 @@
-=========
-Blessings
-=========
-
-Coding with Blessings looks like this... ::
-
- from blessings import Terminal
-
- t = Terminal()
-
- print t.bold('Hi there!')
- print t.bold_red_on_bright_green('It hurts my eyes!')
-
- with t.location(0, t.height - 1):
- print 'This is at the bottom.'
-
-Or, for byte-level control, you can drop down and play with raw terminal
-capabilities::
-
- print '{t.bold}All your {t.red}bold and red base{t.normal}'.format(t=t)
- print t.wingo(2)
-
-The Pitch
-=========
-
-Blessings lifts several of curses_' limiting assumptions, and it makes your
-code pretty, too:
-
-* Use styles, color, and maybe a little positioning without necessarily
- clearing the whole
- screen first.
-* Leave more than one screenful of scrollback in the buffer after your program
- exits, like a well-behaved command-line app should.
-* Get rid of all those noisy, C-like calls to ``tigetstr`` and ``tparm``, so
- your code doesn't get crowded out by terminal bookkeeping.
-* Act intelligently when somebody redirects your output to a file, omitting the
- terminal control codes the user doesn't want to see (optional).
-
-.. _curses: http://docs.python.org/library/curses.html
-
-Before And After
-----------------
-
-Without Blessings, this is how you'd print some underlined text at the bottom
-of the screen::
-
- from curses import tigetstr, setupterm, tparm
- from fcntl import ioctl
- from os import isatty
- import struct
- import sys
- from termios import TIOCGWINSZ
-
- # If we want to tolerate having our output piped to other commands or
- # files without crashing, we need to do all this branching:
- if hasattr(sys.stdout, 'fileno') and isatty(sys.stdout.fileno()):
- setupterm()
- sc = tigetstr('sc')
- cup = tigetstr('cup')
- rc = tigetstr('rc')
- underline = tigetstr('smul')
- normal = tigetstr('sgr0')
- else:
- sc = cup = rc = underline = normal = ''
- print sc # Save cursor position.
- if cup:
- # tigetnum('lines') doesn't always update promptly, hence this:
- height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0]
- print tparm(cup, height - 1, 0) # Move cursor to bottom.
- print 'This is {under}underlined{normal}!'.format(under=underline,
- normal=normal)
- print rc # Restore cursor position.
-
-That was long and full of incomprehensible trash! Let's try it again, this time
-with Blessings::
-
- from blessings import Terminal
-
- term = Terminal()
- with term.location(0, term.height - 1):
- print 'This is', term.underline('pretty!')
-
-Much better.
-
-What It Provides
-================
-
-Blessings provides just one top-level object: ``Terminal``. Instantiating a
-``Terminal`` figures out whether you're on a terminal at all and, if so, does
-any necessary terminal setup. After that, you can proceed to ask it all sorts
-of things about the terminal. Terminal terminal terminal.
-
-Simple Formatting
------------------
-
-Lots of handy formatting codes ("capabilities" in low-level parlance) are
-available as attributes on a ``Terminal``. For example::
-
- from blessings import Terminal
-
- term = Terminal()
- print 'I am ' + term.bold + 'bold' + term.normal + '!'
-
-Though they are strings at heart, you can also use them as callable wrappers so
-you don't have to say ``normal`` afterward::
-
- print 'I am', term.bold('bold') + '!'
-
-Or, if you want fine-grained control while maintaining some semblance of
-brevity, you can combine it with Python's string formatting, which makes
-attributes easy to access::
-
- print 'All your {t.red}base {t.underline}are belong to us{t.normal}'.format(t=term)
-
-Simple capabilities of interest include...
-
-* ``bold``
-* ``reverse``
-* ``underline``
-* ``no_underline`` (which turns off underlining)
-* ``blink``
-* ``normal`` (which turns off everything, even colors)
-
-Here are a few more which are less likely to work on all terminals:
-
-* ``dim``
-* ``italic`` and ``no_italic``
-* ``shadow`` and ``no_shadow``
-* ``standout`` and ``no_standout``
-* ``subscript`` and ``no_subscript``
-* ``superscript`` and ``no_superscript``
-* ``flash`` (which flashes the screen once)
-
-Note that, while the inverse of ``underline`` is ``no_underline``, the only way
-to turn off ``bold`` or ``reverse`` is ``normal``, which also cancels any
-custom colors. This is because there's no portable way to tell the terminal to
-undo certain pieces of formatting, even at the lowest level.
-
-You might also notice that the above aren't the typical incomprehensible
-terminfo capability names; we alias a few of the harder-to-remember ones for
-readability. However, you aren't limited to these: you can reference any
-string-returning capability listed on the `terminfo man page`_ by the name
-under the "Cap-name" column: for example, ``term.rum``.
-
-.. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/
-
-Color
------
-
-16 colors, both foreground and background, are available as easy-to-remember
-attributes::
-
- from blessings import Terminal
-
- term = Terminal()
- print term.red + term.on_green + 'Red on green? Ick!' + term.normal
- print term.bright_red + term.on_bright_blue + 'This is even worse!' + term.normal
-
-You can also call them as wrappers, which sets everything back to normal at the
-end::
-
- print term.red_on_green('Red on green? Ick!')
- print term.yellow('I can barely see it.')
-
-The available colors are...
-
-* ``black``
-* ``red``
-* ``green``
-* ``yellow``
-* ``blue``
-* ``magenta``
-* ``cyan``
-* ``white``
-
-You can set the background color instead of the foreground by prepending
-``on_``, as in ``on_blue``. There is also a ``bright`` version of each color:
-for example, ``on_bright_blue``.
-
-There is also a numerical interface to colors, which takes an integer from
-0-15::
-
- term.color(5) + 'Hello' + term.normal
- term.on_color(3) + 'Hello' + term.normal
-
- term.color(5)('Hello')
- term.on_color(3)('Hello')
-
-If some color is unsupported (for instance, if only the normal colors are
-available, not the bright ones), trying to use it will, on most terminals, have
-no effect: the foreground and background colors will stay as they were. You can
-get fancy and do different things depending on the supported colors by checking
-`number_of_colors`_.
-
-.. _`number_of_colors`: http://packages.python.org/blessings/#blessings.Terminal.number_of_colors
-
-Compound Formatting
--------------------
-
-If you want to do lots of crazy formatting all at once, you can just mash it
-all together::
-
- from blessings import Terminal
-
- term = Terminal()
- print term.bold_underline_green_on_yellow + 'Woo' + term.normal
-
-Or you can use your newly coined attribute as a wrapper, which implicitly sets
-everything back to normal afterward::
-
- print term.bold_underline_green_on_yellow('Woo')
-
-This compound notation comes in handy if you want to allow users to customize
-the formatting of your app: just have them pass in a format specifier like
-"bold_green" on the command line, and do a quick ``getattr(term,
-that_option)('Your text')`` when you do your formatting.
-
-I'd be remiss if I didn't credit couleur_, where I probably got the idea for
-all this mashing.
-
-.. _couleur: http://pypi.python.org/pypi/couleur
-
-Moving The Cursor
------------------
-
-When you want to move the cursor to output text at a specific spot, you have
-a few choices.
-
-Moving Temporarily
-~~~~~~~~~~~~~~~~~~
-
-Most often, you'll need to flit to a certain location, print something, and
-then return: for example, when updating a progress bar at the bottom of the
-screen. ``Terminal`` provides a context manager for doing this concisely::
-
- from blessings import Terminal
-
- term = Terminal()
- with term.location(0, term.height - 1):
- print 'Here is the bottom.'
- print 'This is back where I came from.'
-
-Parameters to ``location()`` are ``x`` and then ``y``, but you can also pass
-just one of them, leaving the other alone. For example... ::
-
- with term.location(y=10):
- print 'We changed just the row.'
-
-If you're doing a series of ``move`` calls (see below) and want to return the
-cursor to its original position afterward, call ``location()`` with no
-arguments, and it will do only the position restoring::
-
- with term.location():
- print term.move(1, 1) + 'Hi'
- print term.move(9, 9) + 'Mom'
-
-Note that, since ``location()`` uses the terminal's built-in
-position-remembering machinery, you can't usefully nest multiple calls. Use
-``location()`` at the outermost spot, and use simpler things like ``move``
-inside.
-
-Moving Permanently
-~~~~~~~~~~~~~~~~~~
-
-If you just want to move and aren't worried about returning, do something like
-this::
-
- from blessings import Terminal
-
- term = Terminal()
- print term.move(10, 1) + 'Hi, mom!'
-
-``move``
- Position the cursor elsewhere. Parameters are y coordinate, then x
- coordinate.
-``move_x``
- Move the cursor to the given column.
-``move_y``
- Move the cursor to the given row.
-
-How does all this work? These are simply more terminal capabilities, wrapped to
-give them nicer names. The added wrinkle--that they take parameters--is also
-given a pleasant treatment: rather than making you dig up ``tparm()`` all the
-time, we simply make these capabilities into callable strings. You'd get the
-raw capability strings if you were to just print them, but they're fully
-parametrized if you pass params to them as if they were functions.
-
-Consequently, you can also reference any other string-returning capability
-listed on the `terminfo man page`_ by its name under the "Cap-name" column.
-
-.. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/
-
-One-Notch Movement
-~~~~~~~~~~~~~~~~~~
-
-Finally, there are some parameterless movement capabilities that move the
-cursor one character in various directions:
-
-* ``move_left``
-* ``move_right``
-* ``move_up``
-* ``move_down``
-
-For example... ::
-
- print term.move_up + 'Howdy!'
-
-Height And Width
-----------------
-
-It's simple to get the height and width of the terminal, in characters::
-
- from blessings import Terminal
-
- term = Terminal()
- height = term.height
- width = term.width
-
-These are newly updated each time you ask for them, so they're safe to use from
-SIGWINCH handlers.
-
-Clearing The Screen
--------------------
-
-Blessings provides syntactic sugar over some screen-clearing capabilities:
-
-``clear``
- Clear the whole screen.
-``clear_eol``
- Clear to the end of the line.
-``clear_bol``
- Clear backward to the beginning of the line.
-``clear_eos``
- Clear to the end of screen.
-
-Full-Screen Mode
-----------------
-
-Perhaps you have seen a full-screen program, such as an editor, restore the
-exact previous state of the terminal upon exiting, including, for example, the
-command-line prompt from which it was launched. Curses pretty much forces you
-into this behavior, but Blessings makes it optional. If you want to do the
-state-restoration thing, use these capabilities:
-
-``enter_fullscreen``
- Switch to the terminal mode where full-screen output is sanctioned. Print
- this before you do any output.
-``exit_fullscreen``
- Switch back to normal mode, restoring the exact state from before
- ``enter_fullscreen`` was used.
-
-Using ``exit_fullscreen`` will wipe away any trace of your program's output, so
-reserve it for when you don't want to leave anything behind in the scrollback.
-
-There's also a context manager you can use as a shortcut::
-
- from blessings import Terminal
-
- term = Terminal()
- with term.fullscreen():
- # Print some stuff.
-
-Besides brevity, another advantage is that it switches back to normal mode even
-if an exception is raised in the ``with`` block.
-
-Pipe Savvy
-----------
-
-If your program isn't attached to a terminal, like if it's being piped to
-another command or redirected to a file, all the capability attributes on
-``Terminal`` will return empty strings. You'll get a nice-looking file without
-any formatting codes gumming up the works.
-
-If you want to override this--like if you anticipate your program being piped
-through ``less -r``, which handles terminal escapes just fine--pass
-``force_styling=True`` to the ``Terminal`` constructor.
-
-In any case, there is a ``does_styling`` attribute on ``Terminal`` that lets
-you see whether your capabilities will return actual, working formatting codes.
-If it's false, you should refrain from drawing progress bars and other frippery
-and just stick to content, since you're apparently headed into a pipe::
-
- from blessings import Terminal
-
- term = Terminal()
- if term.does_styling:
- with term.location(0, term.height - 1):
- print 'Progress: [=======> ]'
- print term.bold('Important stuff')
-
-Shopping List
-=============
-
-There are decades of legacy tied up in terminal interaction, so attention to
-detail and behavior in edge cases make a difference. Here are some ways
-Blessings has your back:
-
-* Uses the terminfo database so it works with any terminal type
-* Provides up-to-the-moment terminal height and width, so you can respond to
- terminal size changes (SIGWINCH signals). (Most other libraries query the
- ``COLUMNS`` and ``LINES`` environment variables or the ``cols`` or ``lines``
- terminal capabilities, which don't update promptly, if at all.)
-* Avoids making a mess if the output gets piped to a non-terminal
-* Works great with standard Python string templating
-* Provides convenient access to all terminal capabilities, not just a sugared
- few
-* Outputs to any file-like object, not just stdout
-* Keeps a minimum of internal state, so you can feel free to mix and match with
- calls to curses or whatever other terminal libraries you like
-
-Blessings does not provide...
-
-* Native color support on the Windows command prompt. However, it should work
- when used in concert with colorama_.
-
-.. _colorama: http://pypi.python.org/pypi/colorama/0.2.4
-
-Bugs
-====
-
-Bugs or suggestions? Visit the `issue tracker`_.
-
-.. _`issue tracker`: https://github.com/erikrose/blessings/issues/
-
-Blessings tests are run automatically by `Travis CI`_.
-
-.. _`Travis CI`: https://travis-ci.org/erikrose/blessings/
-
-.. image:: https://secure.travis-ci.org/erikrose/blessings.png
-
-
-License
-=======
-
-Blessings is under the MIT License. See the LICENSE file.
-
-Version History
-===============
-
-1.6
- * Add ``does_styling`` property. This takes ``force_styling`` into account
- and should replace most uses of ``is_a_tty``.
- * Make ``is_a_tty`` a read-only property, like ``does_styling``. Writing to
- it never would have done anything constructive.
- * Add ``fullscreen()`` and ``hidden_cursor()`` to the auto-generated docs.
- * Fall back to ``LINES`` and ``COLUMNS`` environment vars to find height and
- width. (jquast)
- * Support terminal types, such as kermit and avatar, that use bytes 127-255
- in their escape sequences. (jquast)
-
-1.5.1
- * Clean up fabfile, removing the redundant ``test`` command.
- * Add Travis support.
- * Make ``python setup.py test`` work without spurious errors on 2.6.
- * Work around a tox parsing bug in its config file.
- * Make context managers clean up after themselves even if there's an
- exception. (Vitja Makarov)
- * Parametrizing a capability no longer crashes when there is no tty. (Vitja
- Makarov)
-
-1.5
- * Add syntactic sugar and documentation for ``enter_fullscreen`` and
- ``exit_fullscreen``.
- * Add context managers ``fullscreen()`` and ``hidden_cursor()``.
- * Now you can force a ``Terminal`` never to emit styles by passing
- ``force_styling=None``.
-
-1.4
- * Add syntactic sugar for cursor visibility control and single-space-movement
- capabilities.
- * Endorse the ``location()`` idiom for restoring cursor position after a
- series of manual movements.
- * Fix a bug in which ``location()`` wouldn't do anything when passed zeroes.
- * Allow tests to be run with ``python setup.py test``.
-
-1.3
- * Added ``number_of_colors``, which tells you how many colors the terminal
- supports.
- * Made ``color(n)`` and ``on_color(n)`` callable to wrap a string, like the
- named colors can. Also, make them both fall back to the ``setf`` and
- ``setb`` capabilities (like the named colors do) if the ANSI ``setaf`` and
- ``setab`` aren't available.
- * Allowed ``color`` attr to act as an unparametrized string, not just a
- callable.
- * Made ``height`` and ``width`` examine any passed-in stream before falling
- back to stdout. (This rarely if ever affects actual behavior; it's mostly
- philosophical.)
- * Made caching simpler and slightly more efficient.
- * Got rid of a reference cycle between Terminals and FormattingStrings.
- * Updated docs to reflect that terminal addressing (as in ``location()``) is
- 0-based.
-
-1.2
- * Added support for Python 3! We need 3.2.3 or greater, because the curses
- library couldn't decide whether to accept strs or bytes before that
- (http://bugs.python.org/issue10570).
- * Everything that comes out of the library is now unicode. This lets us
- support Python 3 without making a mess of the code, and Python 2 should
- continue to work unless you were testing types (and badly). Please file a
- bug if this causes trouble for you.
- * Changed to the MIT License for better world domination.
- * Added Sphinx docs.
-
-1.1
- * Added nicely named attributes for colors.
- * Introduced compound formatting.
- * Added wrapper behavior for styling and colors.
- * Let you force capabilities to be non-empty, even if the output stream is
- not a terminal.
- * Added the ``is_a_tty`` attribute for telling whether the output stream is a
- terminal.
- * Sugared the remaining interesting string capabilities.
- * Let ``location()`` operate on just an x *or* y coordinate.
-
-1.0
- * Extracted Blessings from nose-progressive, my `progress-bar-having,
- traceback-shortcutting, rootin', tootin' testrunner`_. It provided the
- tootin' functionality.
-
-.. _`progress-bar-having, traceback-shortcutting, rootin', tootin' testrunner`: http://pypi.python.org/pypi/nose-progressive/
diff --git a/README.rst b/README.rst
new file mode 120000
index 00000000..9a716adc
--- /dev/null
+++ b/README.rst
@@ -0,0 +1 @@
+docs/intro.rst
\ No newline at end of file
diff --git a/bin/colorchart.py b/bin/colorchart.py
new file mode 100644
index 00000000..57d247b6
--- /dev/null
+++ b/bin/colorchart.py
@@ -0,0 +1,92 @@
+# encoding: utf-8
+"""Utility to show X11 colors in 24-bit and down-converted to 256, 16, and 8 color 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 X11 color represented as in 24-bit and as down-converted to 256, 16, and
+ 8 color with the currently configured 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])(u'█')
+
+ 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 = 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())
diff --git a/bin/detect-multibyte.py b/bin/detect-multibyte.py
new file mode 100755
index 00000000..acd79222
--- /dev/null
+++ b/bin/detect-multibyte.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+# coding: utf-8
+"""
+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"
+from __future__ import print_function
+
+# 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(timeout=5.0))
+
+ 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(u'⦰', end='')
+
+ # store second position
+ pos1 = get_pos(term)
+
+ # determine distance
+ horizontal_distance = pos1.column - pos0.column
+ multibyte_capable = bool(horizontal_distance == 1)
+
+ # rubout character(s)
+ print('\b \b' * horizontal_distance, end='')
+
+ # returned to our original starting position,
+ if not multibyte_capable:
+ print('multibyte encoding failed, horizontal distance is {0}, '
+ 'expected 1 for unicode point https://codepoints.net/U+29B0'
+ .format(horizontal_distance), file=sys.stderr)
+ exit(1)
+
+ print('{checkbox} multibyte encoding supported!'
+ .format(checkbox=term.bold_green(u'✓')))
+
+
+if __name__ == '__main__':
+ exit(main())
diff --git a/bin/display-fpathconf.py b/bin/display-fpathconf.py
new file mode 100755
index 00000000..34ca525a
--- /dev/null
+++ b/bin/display-fpathconf.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+"""Displays os.fpathconf values related to terminals."""
+# pylint: disable=invalid-name
+# Invalid module name "display-sighandlers"
+from __future__ import print_function
+
+# std imports
+import os
+import sys
+
+
+def display_fpathconf():
+ """Program entry point."""
+ 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
+ #
+ # irregardless, 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 = r'\x{0:02x}'.format(value)
+ except OSError as err:
+ value = 'OSErrno {0.errno}'.format(err)
+
+ print(fmt.format(name=name, value=value, description=description))
+ print()
+
+
+if __name__ == '__main__':
+ display_fpathconf()
diff --git a/bin/display-maxcanon.py b/bin/display-maxcanon.py
new file mode 100755
index 00000000..09a1a7f5
--- /dev/null
+++ b/bin/display-maxcanon.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+"""
+A tool which uses pexpect to test expected Canonical mode length.
+
+All systems use the value of MAX_CANON which can be found using
+fpathconf(3) value PC_MAX_CANON -- with the exception of Linux
+and FreeBSD.
+
+Linux, though defining a value of 255, actually honors the value
+of 4096 from linux kernel include file tty.h definition
+N_TTY_BUF_SIZE.
+
+Linux also does not honor IMAXBEL. termios(3) states, "Linux does not
+implement this bit, and acts as if it is always set." Although these
+tests ensure it is enabled, this is a non-op for Linux.
+
+FreeBSD supports neither, and instead uses a fraction (1/5) of the tty
+speed which is always 9600. Therefor, the maximum limited input line
+length is 9600 / 5 = 1920.
+
+In other words, the only way to determine the true MAX_CANON in a
+cross-platform manner is through this systems integrated test: the given
+system definitions are misleading on some operating systems.
+"""
+# pylint: disable=invalid-name
+# Invalid module name "display-sighandlers"
+from __future__ import print_function
+
+# std imports
+import os
+import sys
+
+
+def detect_maxcanon():
+ """Program entry point."""
+ import pexpect
+ bashrc = os.path.join(
+ # re-use pexpect/replwrap.py's bashrc file,
+ os.path.dirname(__file__), os.path.pardir, 'pexpect', 'bashrc.sh')
+
+ child = pexpect.spawn('bash', ['--rcfile', bashrc],
+ echo=True, encoding='utf8',
+ timeout=3)
+
+ child.sendline(u'echo -n READY_; echo GO')
+ child.expect_exact(u'READY_GO')
+
+ child.sendline(u'stty icanon imaxbel erase ^H; echo -n retval: $?')
+ child.expect_exact(u'retval: 0')
+
+ child.sendline(u'echo -n GO_; echo AGAIN')
+ child.expect_exact(u'GO_AGAIN')
+ child.sendline(u'cat')
+
+ child.delaybeforesend = 0
+
+ column, blocksize = 0, 64
+ ch_marker = u'_'
+
+ print('auto-detecting MAX_CANON: ', end='')
+ sys.stdout.flush()
+
+ while True:
+ child.send(ch_marker * blocksize)
+ result = child.expect([ch_marker * blocksize, u'\a', pexpect.TIMEOUT])
+ if result == 0:
+ # entire block fit without emitting bel
+ column += blocksize
+ elif result == 1:
+ # an '\a' was emitted, count the number of ch_markers
+ # found since last blocksize, determining our MAX_CANON
+ column += child.before.count(ch_marker)
+ break
+ elif result == 3:
+ print('Undetermined (Timeout) !')
+ print(('child.before: ', child.before))
+ print(column)
+
+
+if __name__ == '__main__':
+ try:
+ detect_maxcanon()
+ except ImportError:
+ # we'd like to use this with CI -- but until we integrate
+ # with tox, we can't determine a period in testing when
+ # the pexpect module has been installed
+ print('warning: pexpect not in module path, MAX_CANON '
+ 'could not be determined by systems test.',
+ file=sys.stderr)
diff --git a/bin/display-sighandlers.py b/bin/display-sighandlers.py
new file mode 100755
index 00000000..aaf09e2f
--- /dev/null
+++ b/bin/display-sighandlers.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+"""Displays all signals, their values, and their handlers to stdout."""
+# pylint: disable=invalid-name
+# Invalid module name "display-sighandlers"
+from __future__ import print_function
+
+# 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()
diff --git a/bin/display-terminalinfo.py b/bin/display-terminalinfo.py
new file mode 100755
index 00000000..1f581c09
--- /dev/null
+++ b/bin/display-terminalinfo.py
@@ -0,0 +1,221 @@
+#!/usr/bin/env python
+"""Display known information about our terminal."""
+# pylint: disable=invalid-name
+# Invalid module name "display-terminalinfo"
+from __future__ import print_function
+
+# 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('{0} {1} {2}'.format('-' * col1_width,
+ '-' * col2_width,
+ '-' * 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 indicies, 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('{0} {1} {2}'.format('-' * col1_width,
+ '-' * col2_width,
+ '-' * 10))
+ for index_name, name in index.items():
+ try:
+ index = getattr(termios, index_name)
+ value = ctlc[index]
+ if value == b'\xff':
+ value = '_POSIX_VDISABLE'
+ else:
+ value = 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('{0} {1}'.format('-' * col1_width, '-' * 27))
+ for name in names:
+ try:
+ value = getter(name)
+ except OSError as err:
+ value = 'OSErrno {err.errno}'.format(err=err)
+ 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('os.isatty({0}) => {1}'.format(fd, os.isatty(fd)))
+ print('locale.getpreferredencoding() => {0}'.format(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('stdin is not a typewriter: {0}'.format(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('os.ttyname({0}) => {1}'.format(fd, os.ttyname(fd)))
+ print('os.ctermid() => {0}'.format(os.ttyname(fd)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/editor.py b/bin/editor.py
new file mode 100755
index 00000000..2963fce5
--- /dev/null
+++ b/bin/editor.py
@@ -0,0 +1,275 @@
+#!/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.
+
+``Ctrl - L``
+ refresh
+
+``Ctrl - C``
+ quit
+
+``Ctrl - S``
+ save
+"""
+from __future__ import division, print_function
+
+# std imports
+import functools
+import collections
+
+# local
+from blessed import Terminal
+
+# python 2/3 compatibility, provide 'echo' function as an
+# alias for "print without newline and flush"
+try:
+ # pylint: disable=invalid-name
+ # Invalid constant name "echo"
+ echo = functools.partial(print, end='', flush=True)
+ echo(u'')
+except TypeError:
+ # TypeError: 'flush' is an invalid keyword argument for this function
+ import sys
+
+ def echo(text):
+ """Display ``text`` and flush output."""
+ sys.stdout.write(u'{}'.format(text))
+ sys.stdout.flush()
+
+
+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(u' '):
+ # 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 = u''
+ 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(u'\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(u'\n')
+ while col > cur_col:
+ cur_col += 1
+ fout.write(u' ')
+ fout.write(char)
+ cur_col += 1
+ fout.write(u'\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 not (row == lastrow and 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():
+ inp = None
+ while True:
+ echo_yx(csr, term.reverse(screen.get((csr.y, csr.x), u' ')))
+ inp = term.inkey()
+
+ if inp == chr(3):
+ # ^c exits
+ break
+
+ elif inp == chr(19):
+ # ^s saves
+ echo_yx(home(bottom(csr)),
+ term.ljust(term.bold_white(u'Filename: ')))
+ echo_yx(right_of(home(bottom(csr)), len(u'Filename: ')), u'')
+ 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)
+
+ else:
+ n_csr = lookup_move(inp.code, csr)
+
+ if n_csr != csr:
+ # erase old cursor,
+ echo_yx(csr, screen.get((csr.y, csr.x), u' '))
+ 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()
diff --git a/bin/keymatrix.py b/bin/keymatrix.py
new file mode 100755
index 00000000..18753f3e
--- /dev/null
+++ b/bin/keymatrix.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+"""
+A simple "game": hit all application keys to win.
+
+Display all known key capabilities that may match the terminal. As each key is pressed on input, it
+is lit up and points are scored.
+"""
+from __future__ import division, print_function
+
+# std imports
+import sys
+import functools
+
+# local
+from blessed import Terminal
+
+# python 2/3 compatibility, provide 'echo' function as an
+# alias for "print without newline and flush"
+try:
+ # pylint: disable=invalid-name
+ # Invalid constant name "echo"
+ echo = functools.partial(print, end='', flush=True)
+ echo(u'')
+except TypeError:
+ # TypeError: 'flush' is an invalid keyword argument for this function
+
+ def echo(text):
+ """Display ``text`` and flush output."""
+ sys.stdout.write(u'{}'.format(text))
+ sys.stdout.flush()
+
+
+def refresh(term, board, level, score, inps):
+ """Refresh the game screen."""
+ echo(term.home + term.clear)
+ level_color = level % 7
+ if level_color == 0:
+ level_color = 4
+ bottom = 0
+ for keycode, attr in board.items():
+ echo(u''.join((
+ term.move_yx(attr['row'], attr['column']),
+ term.color(level_color),
+ (term.reverse if attr['hit'] else term.bold),
+ keycode,
+ term.normal)))
+ bottom = max(bottom, attr['row'])
+ echo(term.move_yx(term.height, 0) + 'level: %s score: %s' % (level, score,))
+ if bottom >= (term.height - 5):
+ sys.stderr.write(
+ ('\n' * (term.height // 2)) +
+ term.center(term.red_underline('cheater!')) + '\n')
+ sys.stderr.write(
+ term.center("(use a larger screen)") +
+ ('\n' * (term.height // 2)))
+ sys.exit(1)
+ echo(term.move_yx(bottom + 1, 0))
+ echo('Press ^C to exit.')
+ for row, inp in enumerate(inps[(term.height - (bottom + 3)) * -1:], 1):
+ echo(term.move_yx(bottom + row + 1, 0))
+ echo('{0!r}, {1}, {2}'.format(
+ inp.__str__() if inp.is_sequence else inp,
+ inp.code,
+ inp.name))
+ echo(term.clear_eol)
+
+
+def build_gameboard(term):
+ """Build the gameboard layout."""
+ column, row = 0, 0
+ board = dict()
+ spacing = 2
+ for keycode in sorted(term._keycodes.values()):
+ if (keycode.startswith('KEY_F')
+ and keycode[-1].isdigit()
+ and int(keycode[len('KEY_F'):]) > 24):
+ continue
+ if column + len(keycode) + (spacing * 2) >= term.width:
+ column = 0
+ row += 1
+ board[keycode] = {'column': column,
+ 'row': row,
+ 'hit': 0,
+ }
+ column += len(keycode) + (spacing * 2)
+ return board
+
+
+def add_score(score, pts, level):
+ """Add points to score, determine and return new score and level."""
+ lvl_multiplier = 10
+ score += pts
+ if (score % (pts * lvl_multiplier)) == 0:
+ level += 1
+ return score, level
+
+
+def main():
+ """Program entry point."""
+ term = Terminal()
+ score = level = hit_highbit = hit_unicode = 0
+ dirty = True
+
+ gameboard = build_gameboard(term)
+ inps = []
+
+ with term.raw(), term.keypad(), term.location():
+ inp = term.inkey(timeout=0)
+ while inp != chr(3):
+ if dirty:
+ refresh(term, gameboard, level, score, inps)
+ dirty = False
+ inp = term.inkey(timeout=5.0)
+ dirty = True
+ if (inp.is_sequence and
+ inp.name in gameboard and
+ gameboard[inp.name]['hit'] == 0):
+ gameboard[inp.name]['hit'] = 1
+ score, level = add_score(score, 100, level)
+ elif inp and not inp.is_sequence and 128 <= ord(inp) <= 255:
+ hit_highbit += 1
+ if hit_highbit < 5:
+ score, level = add_score(score, 100, level)
+ elif inp and not inp.is_sequence and ord(inp) > 256:
+ hit_unicode += 1
+ if hit_unicode < 5:
+ score, level = add_score(score, 100, level)
+ inps.append(inp)
+
+ with term.cbreak():
+ echo(term.move_yx(term.height))
+ echo(
+ u'{term.clear_eol}Your final score was {score} '
+ u'at level {level}{term.clear_eol}\n'
+ u'{term.clear_eol}\n'
+ u'{term.clear_eol}You hit {hit_highbit} '
+ u' 8-bit characters\n{term.clear_eol}\n'
+ u'{term.clear_eol}You hit {hit_unicode} '
+ u' unicode characters.\n{term.clear_eol}\n'
+ u'{term.clear_eol}press any key\n'.format(
+ term=term,
+ score=score, level=level,
+ hit_highbit=hit_highbit,
+ hit_unicode=hit_unicode)
+ )
+ term.inkey()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/on_resize.py b/bin/on_resize.py
new file mode 100755
index 00000000..8749139c
--- /dev/null
+++ b/bin/on_resize.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+"""
+Example application for the 'blessed' Terminal library for python.
+
+Window size changes are caught by the 'on_resize' function using a traditional signal handler.
+Meanwhile, blocking keyboard input is displayed to stdout. If a resize event is discovered, an empty
+string is returned by term.inkey().
+"""
+from __future__ import print_function
+
+# std imports
+import signal
+
+# local
+from blessed import Terminal
+
+
+def main():
+ """Program entry point."""
+ term = Terminal()
+
+ def on_resize(*args):
+ # pylint: disable=unused-argument
+ # Unused argument 'args'
+
+ # Its generally not a good idea to put blocking functions (such as
+ # print) within a signal handler -- if another SIGWINCH is received
+ # while this function blocks, an error will occur.
+
+ # In most programs, you'll want to set some kind of 'dirty' flag,
+ # perhaps by a Semaphore like threading.Event or (thanks to the GIL)
+ # a simple global variable will suffice.
+ print('height={t.height}, width={t.width}\r'.format(t=term))
+
+ signal.signal(signal.SIGWINCH, on_resize)
+
+ # display initial size
+ on_resize(term)
+
+ with term.cbreak():
+ print("press 'X' to stop.")
+ inp = None
+ while inp != 'X':
+ inp = term.inkey()
+ print(repr(inp))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/plasma.py b/bin/plasma.py
new file mode 100755
index 00000000..5adf0b51
--- /dev/null
+++ b/bin/plasma.py
@@ -0,0 +1,129 @@
+#!/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())
+ print(outp, end='')
+ sys.stdout.flush()
+ dirty = False
+ if pause:
+ show_paused(term)
+
+ inp = term.inkey(timeout=0.01 if not pause else None)
+ if inp == '?':
+ assert False, "don't panic"
+ if 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()))
diff --git a/bin/progress_bar.py b/bin/progress_bar.py
new file mode 100755
index 00000000..e8b0a8d8
--- /dev/null
+++ b/bin/progress_bar.py
@@ -0,0 +1,50 @@
+#!/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.
+"""
+from __future__ import print_function
+
+# std imports
+import sys
+
+# local
+from blessed import Terminal
+
+
+def main():
+ """Program entry point."""
+ term = Terminal()
+ assert term.hpa(1) != u'', (
+ '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) + u'[')
+ sys.stderr.write(term.move_x(term.width - 1) + u']' + 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(u'.')
+ else:
+ sys.stderr.write(u'=')
+ col += offset
+ sys.stderr.write(term.move_x(col))
+ sys.stderr.write(u'|\b')
+ sys.stderr.flush()
+ inp = term.inkey(0.04)
+ print()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/resize.py b/bin/resize.py
new file mode 100755
index 00000000..8a3a98a8
--- /dev/null
+++ b/bin/resize.py
@@ -0,0 +1,86 @@
+#!/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.
+"""
+from __future__ import print_function
+
+# 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(timeout=5.0))
+
+ 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("COLUMNS={columns};\nLINES={lines};\nexport COLUMNS LINES;"
+ .format(columns=columns, lines=lines))
+
+
+if __name__ == '__main__':
+ exit(main())
diff --git a/bin/strip.py b/bin/strip.py
new file mode 100755
index 00000000..1229ab73
--- /dev/null
+++ b/bin/strip.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+"""Example scrip 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()
diff --git a/bin/tprint.py b/bin/tprint.py
new file mode 100755
index 00000000..3bffde6d
--- /dev/null
+++ b/bin/tprint.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+"""
+A simple cmd-line tool for displaying FormattingString capabilities.
+
+For example::
+
+ $ python tprint.py bold A rather bold statement.
+"""
+# std
+from __future__ import print_function
+
+# 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()))
diff --git a/bin/worms.py b/bin/worms.py
new file mode 100755
index 00000000..233a29ac
--- /dev/null
+++ b/bin/worms.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python
+"""
+Example application for the 'blessed' Terminal library for python.
+
+It is also an experiment in functional programming.
+"""
+
+from __future__ import division, print_function
+
+# std imports
+from random import randrange
+from functools import partial
+from collections import namedtuple
+
+# local
+from blessed import Terminal
+
+# python 2/3 compatibility, provide 'echo' function as an
+# alias for "print without newline and flush"
+try:
+ # pylint: disable=invalid-name
+ # Invalid constant name "echo"
+ echo = partial(print, end='', flush=True)
+ echo(u'')
+except TypeError:
+ # TypeError: 'flush' is an invalid keyword argument for this function
+ import sys
+
+ def echo(text):
+ """Python 2 version of print(end='', flush=True)."""
+ sys.stdout.write(u'{0}'.format(text))
+ sys.stdout.flush()
+
+# 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."""
+ if hit(head, nibble.location):
+ return speed * modifier
+ return speed
+
+
+def head_glyph(direction):
+ """Return character for worm head depending on horiz/vert orientation."""
+ if direction in (left_of, right_of):
+ return u':'
+ return u'"'
+
+
+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(0, 1 + len('{}'.format(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 (u'q', u'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(u' '))
+
+ # 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(u''.join((
+ term.move_yx(yloc, xloc),
+ (color_worm if (yloc, xloc) == head
+ else color_bg)(u' '),
+ term.normal)))
+ # and draw the new,
+ echo(term.move_yx(*n_nibble.location) + (
+ color_nibble('{}'.format(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(u' '))
+ 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(u''.join((term.move_yx(term.height - 1, 1), term.normal)))
+ echo(u''.join((u'\r\n', u'score: {}'.format(score), u'\r\n')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bin/x11_colorpicker.py b/bin/x11_colorpicker.py
new file mode 100644
index 00000000..ccb24bf5
--- /dev/null
+++ b/bin/x11_colorpicker.py
@@ -0,0 +1,97 @@
+# std imports
+import colorsys
+
+# local
+import blessed
+
+
+def sort_colors():
+ colors = {}
+ for color_name, rgb_color in blessed.colorspace.X11_COLORNAMES_TO_RGB.items():
+ if rgb_color in colors:
+ colors[rgb_color].append(color_name)
+ else:
+ colors[rgb_color] = [color_name]
+
+ return sorted(colors.items(),
+ key=lambda rgb: colorsys.rgb_to_hsv(*rgb[0]),
+ reverse=True)
+
+
+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':
+ pass
+ else:
+ 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()
diff --git a/blessed/__init__.py b/blessed/__init__.py
new file mode 100644
index 00000000..ad307dfa
--- /dev/null
+++ b/blessed/__init__.py
@@ -0,0 +1,21 @@
+"""
+A thin, practical wrapper around terminal capabilities in Python.
+
+http://pypi.python.org/pypi/blessed
+"""
+# std imports
+import platform as _platform
+
+if _platform.system() == 'Windows':
+ from blessed.win_terminal import Terminal
+else:
+ from blessed.terminal import Terminal
+
+if ('3', '0', '0') <= _platform.python_version_tuple() < ('3', '2', '2+'):
+ # Good till 3.2.10
+ # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string.
+ raise ImportError('Blessed needs Python 3.2.3 or greater for Python 3 '
+ 'support due to http://bugs.python.org/issue10570.')
+
+__all__ = ('Terminal',)
+__version__ = '1.17.0'
diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py
new file mode 100644
index 00000000..13ce9a63
--- /dev/null
+++ b/blessed/_capabilities.py
@@ -0,0 +1,173 @@
+"""Terminal capability builder patterns."""
+# std imports
+import re
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ # python 2.6 requires 3rd party library (backport)
+ #
+ # pylint: disable=import-error
+ # Unable to import 'ordereddict'
+ from ordereddict import OrderedDict
+
+__all__ = (
+ 'CAPABILITY_DATABASE',
+ 'CAPABILITIES_RAW_MIXIN',
+ 'CAPABILITIES_ADDITIVES',
+ 'CAPABILITIES_CAUSE_MOVEMENT',
+)
+
+CAPABILITY_DATABASE = OrderedDict((
+ ('bell', ('bel', {})),
+ ('carriage_return', ('cr', {})),
+ ('change_scroll_region', ('csr', {'nparams': 2})),
+ ('clear_all_tabs', ('tbc', {})),
+ ('clear_screen', ('clear', {})),
+ ('clr_bol', ('el1', {})),
+ ('clr_eol', ('el', {})),
+ ('clr_eos', ('clear_eos', {})),
+ ('column_address', ('hpa', {'nparams': 1})),
+ ('cursor_address', ('cup', {'nparams': 2, 'match_grouped': True})),
+ ('cursor_down', ('cud1', {})),
+ ('cursor_home', ('home', {})),
+ ('cursor_invisible', ('civis', {})),
+ ('cursor_left', ('cub1', {})),
+ ('cursor_normal', ('cnorm', {})),
+ ('cursor_report', ('u6', {'nparams': 2, 'match_grouped': True})),
+ ('cursor_right', ('cuf1', {})),
+ ('cursor_up', ('cuu1', {})),
+ ('cursor_visible', ('cvvis', {})),
+ ('delete_character', ('dch1', {})),
+ ('delete_line', ('dl1', {})),
+ ('enter_blink_mode', ('blink', {})),
+ ('enter_bold_mode', ('bold', {})),
+ ('enter_dim_mode', ('dim', {})),
+ ('enter_fullscreen', ('smcup', {})),
+ ('enter_standout_mode', ('standout', {})),
+ ('enter_superscript_mode', ('superscript', {})),
+ ('enter_susimpleript_mode', ('susimpleript', {})),
+ ('enter_underline_mode', ('underline', {})),
+ ('erase_chars', ('ech', {'nparams': 1})),
+ ('exit_alt_charset_mode', ('rmacs', {})),
+ ('exit_am_mode', ('rmam', {})),
+ ('exit_attribute_mode', ('sgr0', {})),
+ ('exit_ca_mode', ('rmcup', {})),
+ ('exit_fullscreen', ('rmcup', {})),
+ ('exit_insert_mode', ('rmir', {})),
+ ('exit_standout_mode', ('rmso', {})),
+ ('exit_underline_mode', ('rmul', {})),
+ ('flash_hook', ('hook', {})),
+ ('flash_screen', ('flash', {})),
+ ('insert_line', ('il1', {})),
+ ('keypad_local', ('rmkx', {})),
+ ('keypad_xmit', ('smkx', {})),
+ ('meta_off', ('rmm', {})),
+ ('meta_on', ('smm', {})),
+ ('orig_pair', ('op', {})),
+ ('parm_down_cursor', ('cud', {'nparams': 1})),
+ ('parm_left_cursor', ('cub', {'nparams': 1, 'match_grouped': True})),
+ ('parm_dch', ('dch', {'nparams': 1})),
+ ('parm_delete_line', ('dl', {'nparams': 1})),
+ ('parm_ich', ('ich', {'nparams': 1})),
+ ('parm_index', ('indn', {'nparams': 1})),
+ ('parm_insert_line', ('il', {'nparams': 1})),
+ ('parm_right_cursor', ('cuf', {'nparams': 1, 'match_grouped': True})),
+ ('parm_rindex', ('rin', {'nparams': 1})),
+ ('parm_up_cursor', ('cuu', {'nparams': 1})),
+ ('print_screen', ('mc0', {})),
+ ('prtr_off', ('mc4', {})),
+ ('prtr_on', ('mc5', {})),
+ ('reset_1string', ('r1', {})),
+ ('reset_2string', ('r2', {})),
+ ('reset_3string', ('r3', {})),
+ ('restore_cursor', ('rc', {})),
+ ('row_address', ('vpa', {'nparams': 1})),
+ ('save_cursor', ('sc', {})),
+ ('scroll_forward', ('ind', {})),
+ ('scroll_reverse', ('rev', {})),
+ ('set0_des_seq', ('s0ds', {})),
+ ('set1_des_seq', ('s1ds', {})),
+ ('set2_des_seq', ('s2ds', {})),
+ ('set3_des_seq', ('s3ds', {})),
+ # this 'color' is deceiving, but often matching, and a better match
+ # than set_a_attributes1 or set_a_foreground.
+ ('color', ('_foreground_color', {'nparams': 1, 'match_any': True,
+ 'numeric': 1})),
+ ('set_a_foreground', ('color', {'nparams': 1, 'match_any': True,
+ 'numeric': 1})),
+ ('set_a_background', ('on_color', {'nparams': 1, 'match_any': True,
+ 'numeric': 1})),
+ ('set_tab', ('hts', {})),
+ ('tab', ('ht', {})),
+ ('italic', ('sitm', {})),
+ ('no_italic', ('sitm', {})),
+))
+
+CAPABILITIES_RAW_MIXIN = {
+ 'bell': re.escape('\a'),
+ 'carriage_return': re.escape('\r'),
+ 'cursor_left': re.escape('\b'),
+ 'cursor_report': re.escape('\x1b') + r'\[(\d+)\;(\d+)R',
+ 'cursor_right': re.escape('\x1b') + r'\[C',
+ 'exit_attribute_mode': re.escape('\x1b') + r'\[m',
+ 'parm_left_cursor': re.escape('\x1b') + r'\[(\d+)D',
+ 'parm_right_cursor': re.escape('\x1b') + r'\[(\d+)C',
+ 'restore_cursor': re.escape(r'\x1b\[u'),
+ 'save_cursor': re.escape(r'\x1b\[s'),
+ 'scroll_forward': re.escape('\n'),
+ 'set0_des_seq': re.escape('\x1b(B'),
+ 'tab': re.escape('\t'),
+ # one could get carried away, such as by adding '\x1b#8' (dec tube
+ # alignment test) by reversing basic vt52, ansi, and xterm sequence
+ # parsers. There is plans to do just that for ANSI.SYS support.
+}
+
+
+CAPABILITIES_ADDITIVES = {
+ 'color256': ('color', re.escape('\x1b') + r'\[38;5;\d+m'),
+ 'shift_in': ('', re.escape('\x0f')),
+ 'shift_out': ('', re.escape('\x0e')),
+ # sgr(...) outputs strangely, use the basic ANSI/EMCA-48 codes here.
+ 'set_a_attributes1': (
+ 'sgr', re.escape('\x1b') + r'\[\d+m'),
+ 'set_a_attributes2': (
+ 'sgr', re.escape('\x1b') + r'\[\d+\;\d+m'),
+ 'set_a_attributes3': (
+ 'sgr', re.escape('\x1b') + r'\[\d+\;\d+\;\d+m'),
+ 'set_a_attributes4': (
+ 'sgr', re.escape('\x1b') + r'\[\d+\;\d+\;\d+\;\d+m'),
+ # this helps where xterm's sgr0 includes set0_des_seq, we'd
+ # rather like to also match this immediate substring.
+ 'sgr0': ('sgr0', re.escape('\x1b') + r'\[m'),
+ 'backspace': ('', re.escape('\b')),
+ 'ascii_tab': ('', re.escape('\t')),
+ 'clr_eol': ('', re.escape('\x1b[K')),
+ 'clr_eol0': ('', re.escape('\x1b[0K')),
+ 'clr_bol': ('', re.escape('\x1b[1K')),
+ 'clr_eosK': ('', re.escape('\x1b[2K')),
+}
+
+CAPABILITIES_CAUSE_MOVEMENT = (
+ 'ascii_tab',
+ 'backspace',
+ 'carriage_return',
+ 'clear_screen',
+ 'column_address',
+ 'cursor_address',
+ 'cursor_down',
+ 'cursor_home',
+ 'cursor_left',
+ 'cursor_right',
+ 'cursor_up',
+ 'enter_fullscreen',
+ 'exit_fullscreen',
+ 'parm_down_cursor',
+ 'parm_left_cursor',
+ 'parm_right_cursor',
+ 'parm_up_cursor',
+ 'restore_cursor',
+ 'row_address',
+ 'scroll_forward',
+ 'tab',
+)
diff --git a/blessed/color.py b/blessed/color.py
new file mode 100644
index 00000000..9a42be4f
--- /dev/null
+++ b/blessed/color.py
@@ -0,0 +1,261 @@
+# encoding: utf-8
+"""
+Sub-module providing color functions.
+
+References,
+- https://en.wikipedia.org/wiki/Color_difference
+- http://www.easyrgb.com/en/math.php
+- Measuring Colour by R.W.G. Hunt and M.R. Pointer
+"""
+
+# std imports
+from math import cos, exp, sin, sqrt, atan2
+
+try:
+ from functools import lru_cache
+except ImportError:
+ from backports.functools_lru_cache import lru_cache
+
+
+def rgb_to_xyz(red, green, blue):
+ """
+ Convert standard RGB color to XYZ color.
+
+ :arg red: RGB value of Red.
+ :arg green: RGB value of Green.
+ :arg blue: RGB value of Blue.
+ :returns: Tuple (X, Y, Z) representing XYZ color
+ :rtype: tuple
+
+ D65/2° standard illuminant
+ """
+ rgb = []
+ for val in red, green, blue:
+ val /= 255
+ if val > 0.04045:
+ val = pow((val + 0.055) / 1.055, 2.4)
+ else:
+ val /= 12.92
+ val *= 100
+ rgb.append(val)
+
+ red, green, blue = rgb # pylint: disable=unbalanced-tuple-unpacking
+ x_val = red * 0.4124 + green * 0.3576 + blue * 0.1805
+ y_val = red * 0.2126 + green * 0.7152 + blue * 0.0722
+ z_val = red * 0.0193 + green * 0.1192 + blue * 0.9505
+
+ return x_val, y_val, z_val
+
+
+def xyz_to_lab(x_val, y_val, z_val):
+ """
+ Convert XYZ color to CIE-Lab color.
+
+ :arg x_val: XYZ value of X.
+ :arg y_val: XYZ value of Y.
+ :arg z_val: XYZ value of Z.
+ :returns: Tuple (L, a, b) representing CIE-Lab color
+ :rtype: tuple
+
+ D65/2° standard illuminant
+ """
+ xyz = []
+ for val, ref in (x_val, 95.047), (y_val, 100.0), (z_val, 108.883):
+ val /= ref
+ if val > 0.008856:
+ val = pow(val, 1 / 3)
+ else:
+ val = 7.787 * val + 16 / 116
+ xyz.append(val)
+
+ x_val, y_val, z_val = xyz # pylint: disable=unbalanced-tuple-unpacking
+ cie_l = 116 * y_val - 16
+ cie_a = 500 * (x_val - y_val)
+ cie_b = 200 * (y_val - z_val)
+
+ return cie_l, cie_a, cie_b
+
+
+@lru_cache(maxsize=256)
+def rgb_to_lab(red, green, blue):
+ """
+ Convert RGB color to CIE-Lab color.
+
+ :arg red: RGB value of Red.
+ :arg green: RGB value of Green.
+ :arg blue: RGB value of Blue.
+ :returns: Tuple (L, a, b) representing CIE-Lab color
+ :rtype: tuple
+
+ D65/2° standard illuminant
+ """
+ return xyz_to_lab(*rgb_to_xyz(red, green, blue))
+
+
+def dist_rgb(rgb1, rgb2):
+ """
+ Determine distance between two rgb colors.
+
+ :arg tuple rgb1: RGB color definition
+ :arg tuple rgb2: RGB color definition
+ :returns: Square of the distance between provided colors
+ :rtype: float
+
+ This works by treating RGB colors as coordinates in three dimensional
+ space and finding the closest point within the configured color range
+ using the formula::
+
+ d^2 = (r2 - r1)^2 + (g2 - g1)^2 + (b2 - b1)^2
+
+ For efficiency, the square of the distance is returned
+ which is sufficient for comparisons
+ """
+ return sum(pow(rgb1[idx] - rgb2[idx], 2) for idx in (0, 1, 2))
+
+
+def dist_rgb_weighted(rgb1, rgb2):
+ """
+ Determine the weighted distance between two rgb colors.
+
+ :arg tuple rgb1: RGB color definition
+ :arg tuple rgb2: RGB color definition
+ :returns: Square of the distance between provided colors
+ :rtype: float
+
+ Similar to a standard distance formula, the values are weighted
+ to approximate human perception of color differences
+
+ For efficiency, the square of the distance is returned
+ which is sufficient for comparisons
+ """
+ red_mean = (rgb1[0] + rgb2[0]) / 2
+
+ return ((2 + red_mean / 256) * pow(rgb1[0] - rgb2[0], 2) +
+ 4 * pow(rgb1[1] - rgb2[1], 2) +
+ (2 + (255 - red_mean) / 256) * pow(rgb1[2] - rgb2[2], 2))
+
+
+def dist_cie76(rgb1, rgb2):
+ """
+ Determine distance between two rgb colors using the CIE94 algorithm.
+
+ :arg tuple rgb1: RGB color definition
+ :arg tuple rgb2: RGB color definition
+ :returns: Square of the distance between provided colors
+ :rtype: float
+
+ For efficiency, the square of the distance is returned
+ which is sufficient for comparisons
+ """
+ l_1, a_1, b_1 = rgb_to_lab(*rgb1)
+ l_2, a_2, b_2 = rgb_to_lab(*rgb2)
+ return pow(l_1 - l_2, 2) + pow(a_1 - a_2, 2) + pow(b_1 - b_2, 2)
+
+
+def dist_cie94(rgb1, rgb2):
+ # pylint: disable=too-many-locals
+ """
+ Determine distance between two rgb colors using the CIE94 algorithm.
+
+ :arg tuple rgb1: RGB color definition
+ :arg tuple rgb2: RGB color definition
+ :returns: Square of the distance between provided colors
+ :rtype: float
+
+ For efficiency, the square of the distance is returned
+ which is sufficient for comparisons
+ """
+ l_1, a_1, b_1 = rgb_to_lab(*rgb1)
+ l_2, a_2, b_2 = rgb_to_lab(*rgb2)
+
+ s_l = k_l = k_c = k_h = 1
+ k_1 = 0.045
+ k_2 = 0.015
+
+ delta_l = l_1 - l_2
+ delta_a = a_1 - a_2
+ delta_b = b_1 - b_2
+ c_1 = sqrt(a_1 ** 2 + b_1 ** 2)
+ c_2 = sqrt(a_2 ** 2 + b_2 ** 2)
+ delta_c = c_1 - c_2
+ delta_h = sqrt(delta_a ** 2 + delta_b ** 2 + delta_c ** 2)
+ s_c = 1 + k_1 * c_1
+ s_h = 1 + k_2 * c_1
+
+ return ((delta_l / (k_l * s_l)) ** 2 +
+ (delta_c / (k_c * s_c)) ** 2 +
+ (delta_h / (k_h * s_h)) ** 2)
+
+
+def dist_cie2000(rgb1, rgb2):
+ # pylint: disable=too-many-locals
+ """
+ Determine distance between two rgb colors using the CIE2000 algorithm.
+
+ :arg tuple rgb1: RGB color definition
+ :arg tuple rgb2: RGB color definition
+ :returns: Square of the distance between provided colors
+ :rtype: float
+
+ For efficiency, the square of the distance is returned
+ which is sufficient for comparisons
+ """
+ s_l = k_l = k_c = k_h = 1
+
+ l_1, a_1, b_1 = rgb_to_lab(*rgb1)
+ l_2, a_2, b_2 = rgb_to_lab(*rgb2)
+
+ delta_l = l_2 - l_1
+ l_mean = (l_1 + l_2) / 2
+
+ c_1 = sqrt(a_1 ** 2 + b_1 ** 2)
+ c_2 = sqrt(a_2 ** 2 + b_2 ** 2)
+ c_mean = (c_1 + c_2) / 2
+ delta_c = c_1 - c_2
+
+ g_x = sqrt(c_mean ** 7 / (c_mean ** 7 + 25 ** 7))
+ h_1 = atan2(b_1, a_1 + (a_1 / 2) * (1 - g_x)) % 360
+ h_2 = atan2(b_2, a_2 + (a_2 / 2) * (1 - g_x)) % 360
+
+ if 0 in (c_1, c_2):
+ delta_h_prime = 0
+ h_mean = h_1 + h_2
+ else:
+ delta_h_prime = h_2 - h_1
+ if abs(delta_h_prime) <= 180:
+ h_mean = (h_1 + h_2) / 2
+ else:
+ if h_2 <= h_1:
+ delta_h_prime += 360
+ else:
+ delta_h_prime -= 360
+ if h_1 + h_2 < 360:
+ h_mean = (h_1 + h_2 + 360) / 2
+ else:
+ h_mean = (h_1 + h_2 - 360) / 2
+
+ delta_h = 2 * sqrt(c_1 * c_2) * sin(delta_h_prime / 2)
+
+ t_x = (1 -
+ 0.17 * cos(h_mean - 30) +
+ 0.24 * cos(2 * h_mean) +
+ 0.32 * cos(3 * h_mean + 6) -
+ 0.20 * cos(4 * h_mean - 63))
+
+ s_l = 1 + (0.015 * (l_mean - 50) ** 2) / sqrt(20 + (l_mean - 50) ** 2)
+ s_c = 1 + 0.045 * c_mean
+ s_h = 1 + 0.015 * c_mean * t_x
+ r_t = -2 * g_x * sin(abs(60 * exp(-1 * abs((delta_h - 275) / 25) ** 2)))
+
+ delta_l = delta_l / (k_l * s_l)
+ delta_c = delta_c / (k_c * s_c)
+ delta_h = delta_h / (k_h * s_h)
+
+ return delta_l ** 2 + delta_c ** 2 + delta_h ** 2 + r_t * delta_c * delta_h
+
+
+COLOR_DISTANCE_ALGORITHMS = {'rgb': dist_rgb,
+ 'rgb-weighted': dist_rgb_weighted,
+ 'cie76': dist_cie76,
+ 'cie94': dist_cie94,
+ 'cie2000': dist_cie2000}
diff --git a/blessed/colorspace.py b/blessed/colorspace.py
new file mode 100644
index 00000000..0b0e972f
--- /dev/null
+++ b/blessed/colorspace.py
@@ -0,0 +1,969 @@
+"""
+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.eu/Softwares/nosh/guide/TerminalCapabilities.html
+"""
+# std imports
+import collections
+
+__all__ = (
+ 'CGA_COLORS',
+ 'RGBColor',
+ 'RGB_256TABLE',
+ 'X11_COLORNAMES_TO_RGB',
+)
+
+CGA_COLORS = set(
+ ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'))
+
+
+class RGBColor(collections.namedtuple("RGBColor", ["red", "green", "blue"])):
+ def __str__(self):
+ return '#{0:02x}{1:02x}{2:02x}'.format(*self)
+
+
+#: X11 Color names to (XTerm-defined) RGB values from xorg-rgb/rgb.txt
+X11_COLORNAMES_TO_RGB = {
+ 'aliceblue': RGBColor(240, 248, 255),
+ 'antiquewhite': RGBColor(250, 235, 215),
+ 'antiquewhite1': RGBColor(255, 239, 219),
+ 'antiquewhite2': RGBColor(238, 223, 204),
+ 'antiquewhite3': RGBColor(205, 192, 176),
+ 'antiquewhite4': RGBColor(139, 131, 120),
+ 'aqua': RGBColor(0, 255, 255),
+ 'aquamarine': RGBColor(127, 255, 212),
+ 'aquamarine1': RGBColor(127, 255, 212),
+ 'aquamarine2': RGBColor(118, 238, 198),
+ 'aquamarine3': RGBColor(102, 205, 170),
+ 'aquamarine4': RGBColor(69, 139, 116),
+ 'azure': RGBColor(240, 255, 255),
+ 'azure1': RGBColor(240, 255, 255),
+ 'azure2': RGBColor(224, 238, 238),
+ 'azure3': RGBColor(193, 205, 205),
+ 'azure4': RGBColor(131, 139, 139),
+ 'beige': RGBColor(245, 245, 220),
+ 'bisque': RGBColor(255, 228, 196),
+ 'bisque1': RGBColor(255, 228, 196),
+ 'bisque2': RGBColor(238, 213, 183),
+ 'bisque3': RGBColor(205, 183, 158),
+ 'bisque4': RGBColor(139, 125, 107),
+ 'black': RGBColor(0, 0, 0),
+ 'blanchedalmond': RGBColor(255, 235, 205),
+ 'blue': RGBColor(0, 0, 255),
+ 'blue1': RGBColor(0, 0, 255),
+ 'blue2': RGBColor(0, 0, 238),
+ 'blue3': RGBColor(0, 0, 205),
+ 'blue4': RGBColor(0, 0, 139),
+ 'blueviolet': RGBColor(138, 43, 226),
+ 'brown': RGBColor(165, 42, 42),
+ 'brown1': RGBColor(255, 64, 64),
+ 'brown2': RGBColor(238, 59, 59),
+ 'brown3': RGBColor(205, 51, 51),
+ 'brown4': RGBColor(139, 35, 35),
+ 'burlywood': RGBColor(222, 184, 135),
+ 'burlywood1': RGBColor(255, 211, 155),
+ 'burlywood2': RGBColor(238, 197, 145),
+ 'burlywood3': RGBColor(205, 170, 125),
+ 'burlywood4': RGBColor(139, 115, 85),
+ 'cadetblue': RGBColor(95, 158, 160),
+ 'cadetblue1': RGBColor(152, 245, 255),
+ 'cadetblue2': RGBColor(142, 229, 238),
+ 'cadetblue3': RGBColor(122, 197, 205),
+ 'cadetblue4': RGBColor(83, 134, 139),
+ 'chartreuse': RGBColor(127, 255, 0),
+ 'chartreuse1': RGBColor(127, 255, 0),
+ 'chartreuse2': RGBColor(118, 238, 0),
+ 'chartreuse3': RGBColor(102, 205, 0),
+ 'chartreuse4': RGBColor(69, 139, 0),
+ 'chocolate': RGBColor(210, 105, 30),
+ 'chocolate1': RGBColor(255, 127, 36),
+ 'chocolate2': RGBColor(238, 118, 33),
+ 'chocolate3': RGBColor(205, 102, 29),
+ 'chocolate4': RGBColor(139, 69, 19),
+ 'coral': RGBColor(255, 127, 80),
+ 'coral1': RGBColor(255, 114, 86),
+ 'coral2': RGBColor(238, 106, 80),
+ 'coral3': RGBColor(205, 91, 69),
+ 'coral4': RGBColor(139, 62, 47),
+ 'cornflowerblue': RGBColor(100, 149, 237),
+ 'cornsilk': RGBColor(255, 248, 220),
+ 'cornsilk1': RGBColor(255, 248, 220),
+ 'cornsilk2': RGBColor(238, 232, 205),
+ 'cornsilk3': RGBColor(205, 200, 177),
+ 'cornsilk4': RGBColor(139, 136, 120),
+ 'crimson': RGBColor(220, 20, 60),
+ 'cyan': RGBColor(0, 255, 255),
+ 'cyan1': RGBColor(0, 255, 255),
+ 'cyan2': RGBColor(0, 238, 238),
+ 'cyan3': RGBColor(0, 205, 205),
+ 'cyan4': RGBColor(0, 139, 139),
+ 'darkblue': RGBColor(0, 0, 139),
+ 'darkcyan': RGBColor(0, 139, 139),
+ 'darkgoldenrod': RGBColor(184, 134, 11),
+ 'darkgoldenrod1': RGBColor(255, 185, 15),
+ 'darkgoldenrod2': RGBColor(238, 173, 14),
+ 'darkgoldenrod3': RGBColor(205, 149, 12),
+ 'darkgoldenrod4': RGBColor(139, 101, 8),
+ 'darkgray': RGBColor(169, 169, 169),
+ 'darkgreen': RGBColor(0, 100, 0),
+ 'darkgrey': RGBColor(169, 169, 169),
+ 'darkkhaki': RGBColor(189, 183, 107),
+ 'darkmagenta': RGBColor(139, 0, 139),
+ 'darkolivegreen': RGBColor(85, 107, 47),
+ 'darkolivegreen1': RGBColor(202, 255, 112),
+ 'darkolivegreen2': RGBColor(188, 238, 104),
+ 'darkolivegreen3': RGBColor(162, 205, 90),
+ 'darkolivegreen4': RGBColor(110, 139, 61),
+ 'darkorange': RGBColor(255, 140, 0),
+ 'darkorange1': RGBColor(255, 127, 0),
+ 'darkorange2': RGBColor(238, 118, 0),
+ 'darkorange3': RGBColor(205, 102, 0),
+ 'darkorange4': RGBColor(139, 69, 0),
+ 'darkorchid': RGBColor(153, 50, 204),
+ 'darkorchid1': RGBColor(191, 62, 255),
+ 'darkorchid2': RGBColor(178, 58, 238),
+ 'darkorchid3': RGBColor(154, 50, 205),
+ 'darkorchid4': RGBColor(104, 34, 139),
+ 'darkred': RGBColor(139, 0, 0),
+ 'darksalmon': RGBColor(233, 150, 122),
+ 'darkseagreen': RGBColor(143, 188, 143),
+ 'darkseagreen1': RGBColor(193, 255, 193),
+ 'darkseagreen2': RGBColor(180, 238, 180),
+ 'darkseagreen3': RGBColor(155, 205, 155),
+ 'darkseagreen4': RGBColor(105, 139, 105),
+ 'darkslateblue': RGBColor(72, 61, 139),
+ 'darkslategray': RGBColor(47, 79, 79),
+ 'darkslategray1': RGBColor(151, 255, 255),
+ 'darkslategray2': RGBColor(141, 238, 238),
+ 'darkslategray3': RGBColor(121, 205, 205),
+ 'darkslategray4': RGBColor(82, 139, 139),
+ 'darkslategrey': RGBColor(47, 79, 79),
+ 'darkturquoise': RGBColor(0, 206, 209),
+ 'darkviolet': RGBColor(148, 0, 211),
+ 'deeppink': RGBColor(255, 20, 147),
+ 'deeppink1': RGBColor(255, 20, 147),
+ 'deeppink2': RGBColor(238, 18, 137),
+ 'deeppink3': RGBColor(205, 16, 118),
+ 'deeppink4': RGBColor(139, 10, 80),
+ 'deepskyblue': RGBColor(0, 191, 255),
+ 'deepskyblue1': RGBColor(0, 191, 255),
+ 'deepskyblue2': RGBColor(0, 178, 238),
+ 'deepskyblue3': RGBColor(0, 154, 205),
+ 'deepskyblue4': RGBColor(0, 104, 139),
+ 'dimgray': RGBColor(105, 105, 105),
+ 'dimgrey': RGBColor(105, 105, 105),
+ 'dodgerblue': RGBColor(30, 144, 255),
+ 'dodgerblue1': RGBColor(30, 144, 255),
+ 'dodgerblue2': RGBColor(28, 134, 238),
+ 'dodgerblue3': RGBColor(24, 116, 205),
+ 'dodgerblue4': RGBColor(16, 78, 139),
+ 'firebrick': RGBColor(178, 34, 34),
+ 'firebrick1': RGBColor(255, 48, 48),
+ 'firebrick2': RGBColor(238, 44, 44),
+ 'firebrick3': RGBColor(205, 38, 38),
+ 'firebrick4': RGBColor(139, 26, 26),
+ 'floralwhite': RGBColor(255, 250, 240),
+ 'forestgreen': RGBColor(34, 139, 34),
+ 'fuchsia': RGBColor(255, 0, 255),
+ 'gainsboro': RGBColor(220, 220, 220),
+ 'ghostwhite': RGBColor(248, 248, 255),
+ 'gold': RGBColor(255, 215, 0),
+ 'gold1': RGBColor(255, 215, 0),
+ 'gold2': RGBColor(238, 201, 0),
+ 'gold3': RGBColor(205, 173, 0),
+ 'gold4': RGBColor(139, 117, 0),
+ 'goldenrod': RGBColor(218, 165, 32),
+ 'goldenrod1': RGBColor(255, 193, 37),
+ 'goldenrod2': RGBColor(238, 180, 34),
+ 'goldenrod3': RGBColor(205, 155, 29),
+ 'goldenrod4': RGBColor(139, 105, 20),
+ 'gray': RGBColor(190, 190, 190),
+ 'gray0': RGBColor(0, 0, 0),
+ 'gray1': RGBColor(3, 3, 3),
+ 'gray10': RGBColor(26, 26, 26),
+ 'gray100': RGBColor(255, 255, 255),
+ 'gray11': RGBColor(28, 28, 28),
+ 'gray12': RGBColor(31, 31, 31),
+ 'gray13': RGBColor(33, 33, 33),
+ 'gray14': RGBColor(36, 36, 36),
+ 'gray15': RGBColor(38, 38, 38),
+ 'gray16': RGBColor(41, 41, 41),
+ 'gray17': RGBColor(43, 43, 43),
+ 'gray18': RGBColor(46, 46, 46),
+ 'gray19': RGBColor(48, 48, 48),
+ 'gray2': RGBColor(5, 5, 5),
+ 'gray20': RGBColor(51, 51, 51),
+ 'gray21': RGBColor(54, 54, 54),
+ 'gray22': RGBColor(56, 56, 56),
+ 'gray23': RGBColor(59, 59, 59),
+ 'gray24': RGBColor(61, 61, 61),
+ 'gray25': RGBColor(64, 64, 64),
+ 'gray26': RGBColor(66, 66, 66),
+ 'gray27': RGBColor(69, 69, 69),
+ 'gray28': RGBColor(71, 71, 71),
+ 'gray29': RGBColor(74, 74, 74),
+ 'gray3': RGBColor(8, 8, 8),
+ 'gray30': RGBColor(77, 77, 77),
+ 'gray31': RGBColor(79, 79, 79),
+ 'gray32': RGBColor(82, 82, 82),
+ 'gray33': RGBColor(84, 84, 84),
+ 'gray34': RGBColor(87, 87, 87),
+ 'gray35': RGBColor(89, 89, 89),
+ 'gray36': RGBColor(92, 92, 92),
+ 'gray37': RGBColor(94, 94, 94),
+ 'gray38': RGBColor(97, 97, 97),
+ 'gray39': RGBColor(99, 99, 99),
+ 'gray4': RGBColor(10, 10, 10),
+ 'gray40': RGBColor(102, 102, 102),
+ 'gray41': RGBColor(105, 105, 105),
+ 'gray42': RGBColor(107, 107, 107),
+ 'gray43': RGBColor(110, 110, 110),
+ 'gray44': RGBColor(112, 112, 112),
+ 'gray45': RGBColor(115, 115, 115),
+ 'gray46': RGBColor(117, 117, 117),
+ 'gray47': RGBColor(120, 120, 120),
+ 'gray48': RGBColor(122, 122, 122),
+ 'gray49': RGBColor(125, 125, 125),
+ 'gray5': RGBColor(13, 13, 13),
+ 'gray50': RGBColor(127, 127, 127),
+ 'gray51': RGBColor(130, 130, 130),
+ 'gray52': RGBColor(133, 133, 133),
+ 'gray53': RGBColor(135, 135, 135),
+ 'gray54': RGBColor(138, 138, 138),
+ 'gray55': RGBColor(140, 140, 140),
+ 'gray56': RGBColor(143, 143, 143),
+ 'gray57': RGBColor(145, 145, 145),
+ 'gray58': RGBColor(148, 148, 148),
+ 'gray59': RGBColor(150, 150, 150),
+ 'gray6': RGBColor(15, 15, 15),
+ 'gray60': RGBColor(153, 153, 153),
+ 'gray61': RGBColor(156, 156, 156),
+ 'gray62': RGBColor(158, 158, 158),
+ 'gray63': RGBColor(161, 161, 161),
+ 'gray64': RGBColor(163, 163, 163),
+ 'gray65': RGBColor(166, 166, 166),
+ 'gray66': RGBColor(168, 168, 168),
+ 'gray67': RGBColor(171, 171, 171),
+ 'gray68': RGBColor(173, 173, 173),
+ 'gray69': RGBColor(176, 176, 176),
+ 'gray7': RGBColor(18, 18, 18),
+ 'gray70': RGBColor(179, 179, 179),
+ 'gray71': RGBColor(181, 181, 181),
+ 'gray72': RGBColor(184, 184, 184),
+ 'gray73': RGBColor(186, 186, 186),
+ 'gray74': RGBColor(189, 189, 189),
+ 'gray75': RGBColor(191, 191, 191),
+ 'gray76': RGBColor(194, 194, 194),
+ 'gray77': RGBColor(196, 196, 196),
+ 'gray78': RGBColor(199, 199, 199),
+ 'gray79': RGBColor(201, 201, 201),
+ 'gray8': RGBColor(20, 20, 20),
+ 'gray80': RGBColor(204, 204, 204),
+ 'gray81': RGBColor(207, 207, 207),
+ 'gray82': RGBColor(209, 209, 209),
+ 'gray83': RGBColor(212, 212, 212),
+ 'gray84': RGBColor(214, 214, 214),
+ 'gray85': RGBColor(217, 217, 217),
+ 'gray86': RGBColor(219, 219, 219),
+ 'gray87': RGBColor(222, 222, 222),
+ 'gray88': RGBColor(224, 224, 224),
+ 'gray89': RGBColor(227, 227, 227),
+ 'gray9': RGBColor(23, 23, 23),
+ 'gray90': RGBColor(229, 229, 229),
+ 'gray91': RGBColor(232, 232, 232),
+ 'gray92': RGBColor(235, 235, 235),
+ 'gray93': RGBColor(237, 237, 237),
+ 'gray94': RGBColor(240, 240, 240),
+ 'gray95': RGBColor(242, 242, 242),
+ 'gray96': RGBColor(245, 245, 245),
+ 'gray97': RGBColor(247, 247, 247),
+ 'gray98': RGBColor(250, 250, 250),
+ 'gray99': RGBColor(252, 252, 252),
+ 'green': RGBColor(0, 255, 0),
+ 'green1': RGBColor(0, 255, 0),
+ 'green2': RGBColor(0, 238, 0),
+ 'green3': RGBColor(0, 205, 0),
+ 'green4': RGBColor(0, 139, 0),
+ 'greenyellow': RGBColor(173, 255, 47),
+ 'grey': RGBColor(190, 190, 190),
+ 'grey0': RGBColor(0, 0, 0),
+ 'grey1': RGBColor(3, 3, 3),
+ 'grey10': RGBColor(26, 26, 26),
+ 'grey100': RGBColor(255, 255, 255),
+ 'grey11': RGBColor(28, 28, 28),
+ 'grey12': RGBColor(31, 31, 31),
+ 'grey13': RGBColor(33, 33, 33),
+ 'grey14': RGBColor(36, 36, 36),
+ 'grey15': RGBColor(38, 38, 38),
+ 'grey16': RGBColor(41, 41, 41),
+ 'grey17': RGBColor(43, 43, 43),
+ 'grey18': RGBColor(46, 46, 46),
+ 'grey19': RGBColor(48, 48, 48),
+ 'grey2': RGBColor(5, 5, 5),
+ 'grey20': RGBColor(51, 51, 51),
+ 'grey21': RGBColor(54, 54, 54),
+ 'grey22': RGBColor(56, 56, 56),
+ 'grey23': RGBColor(59, 59, 59),
+ 'grey24': RGBColor(61, 61, 61),
+ 'grey25': RGBColor(64, 64, 64),
+ 'grey26': RGBColor(66, 66, 66),
+ 'grey27': RGBColor(69, 69, 69),
+ 'grey28': RGBColor(71, 71, 71),
+ 'grey29': RGBColor(74, 74, 74),
+ 'grey3': RGBColor(8, 8, 8),
+ 'grey30': RGBColor(77, 77, 77),
+ 'grey31': RGBColor(79, 79, 79),
+ 'grey32': RGBColor(82, 82, 82),
+ 'grey33': RGBColor(84, 84, 84),
+ 'grey34': RGBColor(87, 87, 87),
+ 'grey35': RGBColor(89, 89, 89),
+ 'grey36': RGBColor(92, 92, 92),
+ 'grey37': RGBColor(94, 94, 94),
+ 'grey38': RGBColor(97, 97, 97),
+ 'grey39': RGBColor(99, 99, 99),
+ 'grey4': RGBColor(10, 10, 10),
+ 'grey40': RGBColor(102, 102, 102),
+ 'grey41': RGBColor(105, 105, 105),
+ 'grey42': RGBColor(107, 107, 107),
+ 'grey43': RGBColor(110, 110, 110),
+ 'grey44': RGBColor(112, 112, 112),
+ 'grey45': RGBColor(115, 115, 115),
+ 'grey46': RGBColor(117, 117, 117),
+ 'grey47': RGBColor(120, 120, 120),
+ 'grey48': RGBColor(122, 122, 122),
+ 'grey49': RGBColor(125, 125, 125),
+ 'grey5': RGBColor(13, 13, 13),
+ 'grey50': RGBColor(127, 127, 127),
+ 'grey51': RGBColor(130, 130, 130),
+ 'grey52': RGBColor(133, 133, 133),
+ 'grey53': RGBColor(135, 135, 135),
+ 'grey54': RGBColor(138, 138, 138),
+ 'grey55': RGBColor(140, 140, 140),
+ 'grey56': RGBColor(143, 143, 143),
+ 'grey57': RGBColor(145, 145, 145),
+ 'grey58': RGBColor(148, 148, 148),
+ 'grey59': RGBColor(150, 150, 150),
+ 'grey6': RGBColor(15, 15, 15),
+ 'grey60': RGBColor(153, 153, 153),
+ 'grey61': RGBColor(156, 156, 156),
+ 'grey62': RGBColor(158, 158, 158),
+ 'grey63': RGBColor(161, 161, 161),
+ 'grey64': RGBColor(163, 163, 163),
+ 'grey65': RGBColor(166, 166, 166),
+ 'grey66': RGBColor(168, 168, 168),
+ 'grey67': RGBColor(171, 171, 171),
+ 'grey68': RGBColor(173, 173, 173),
+ 'grey69': RGBColor(176, 176, 176),
+ 'grey7': RGBColor(18, 18, 18),
+ 'grey70': RGBColor(179, 179, 179),
+ 'grey71': RGBColor(181, 181, 181),
+ 'grey72': RGBColor(184, 184, 184),
+ 'grey73': RGBColor(186, 186, 186),
+ 'grey74': RGBColor(189, 189, 189),
+ 'grey75': RGBColor(191, 191, 191),
+ 'grey76': RGBColor(194, 194, 194),
+ 'grey77': RGBColor(196, 196, 196),
+ 'grey78': RGBColor(199, 199, 199),
+ 'grey79': RGBColor(201, 201, 201),
+ 'grey8': RGBColor(20, 20, 20),
+ 'grey80': RGBColor(204, 204, 204),
+ 'grey81': RGBColor(207, 207, 207),
+ 'grey82': RGBColor(209, 209, 209),
+ 'grey83': RGBColor(212, 212, 212),
+ 'grey84': RGBColor(214, 214, 214),
+ 'grey85': RGBColor(217, 217, 217),
+ 'grey86': RGBColor(219, 219, 219),
+ 'grey87': RGBColor(222, 222, 222),
+ 'grey88': RGBColor(224, 224, 224),
+ 'grey89': RGBColor(227, 227, 227),
+ 'grey9': RGBColor(23, 23, 23),
+ 'grey90': RGBColor(229, 229, 229),
+ 'grey91': RGBColor(232, 232, 232),
+ 'grey92': RGBColor(235, 235, 235),
+ 'grey93': RGBColor(237, 237, 237),
+ 'grey94': RGBColor(240, 240, 240),
+ 'grey95': RGBColor(242, 242, 242),
+ 'grey96': RGBColor(245, 245, 245),
+ 'grey97': RGBColor(247, 247, 247),
+ 'grey98': RGBColor(250, 250, 250),
+ 'grey99': RGBColor(252, 252, 252),
+ 'honeydew': RGBColor(240, 255, 240),
+ 'honeydew1': RGBColor(240, 255, 240),
+ 'honeydew2': RGBColor(224, 238, 224),
+ 'honeydew3': RGBColor(193, 205, 193),
+ 'honeydew4': RGBColor(131, 139, 131),
+ 'hotpink': RGBColor(255, 105, 180),
+ 'hotpink1': RGBColor(255, 110, 180),
+ 'hotpink2': RGBColor(238, 106, 167),
+ 'hotpink3': RGBColor(205, 96, 144),
+ 'hotpink4': RGBColor(139, 58, 98),
+ 'indianred': RGBColor(205, 92, 92),
+ 'indianred1': RGBColor(255, 106, 106),
+ 'indianred2': RGBColor(238, 99, 99),
+ 'indianred3': RGBColor(205, 85, 85),
+ 'indianred4': RGBColor(139, 58, 58),
+ 'indigo': RGBColor(75, 0, 130),
+ 'ivory': RGBColor(255, 255, 240),
+ 'ivory1': RGBColor(255, 255, 240),
+ 'ivory2': RGBColor(238, 238, 224),
+ 'ivory3': RGBColor(205, 205, 193),
+ 'ivory4': RGBColor(139, 139, 131),
+ 'khaki': RGBColor(240, 230, 140),
+ 'khaki1': RGBColor(255, 246, 143),
+ 'khaki2': RGBColor(238, 230, 133),
+ 'khaki3': RGBColor(205, 198, 115),
+ 'khaki4': RGBColor(139, 134, 78),
+ 'lavender': RGBColor(230, 230, 250),
+ 'lavenderblush': RGBColor(255, 240, 245),
+ 'lavenderblush1': RGBColor(255, 240, 245),
+ 'lavenderblush2': RGBColor(238, 224, 229),
+ 'lavenderblush3': RGBColor(205, 193, 197),
+ 'lavenderblush4': RGBColor(139, 131, 134),
+ 'lawngreen': RGBColor(124, 252, 0),
+ 'lemonchiffon': RGBColor(255, 250, 205),
+ 'lemonchiffon1': RGBColor(255, 250, 205),
+ 'lemonchiffon2': RGBColor(238, 233, 191),
+ 'lemonchiffon3': RGBColor(205, 201, 165),
+ 'lemonchiffon4': RGBColor(139, 137, 112),
+ 'lightblue': RGBColor(173, 216, 230),
+ 'lightblue1': RGBColor(191, 239, 255),
+ 'lightblue2': RGBColor(178, 223, 238),
+ 'lightblue3': RGBColor(154, 192, 205),
+ 'lightblue4': RGBColor(104, 131, 139),
+ 'lightcoral': RGBColor(240, 128, 128),
+ 'lightcyan': RGBColor(224, 255, 255),
+ 'lightcyan1': RGBColor(224, 255, 255),
+ 'lightcyan2': RGBColor(209, 238, 238),
+ 'lightcyan3': RGBColor(180, 205, 205),
+ 'lightcyan4': RGBColor(122, 139, 139),
+ 'lightgoldenrod': RGBColor(238, 221, 130),
+ 'lightgoldenrod1': RGBColor(255, 236, 139),
+ 'lightgoldenrod2': RGBColor(238, 220, 130),
+ 'lightgoldenrod3': RGBColor(205, 190, 112),
+ 'lightgoldenrod4': RGBColor(139, 129, 76),
+ 'lightgoldenrodyellow': RGBColor(250, 250, 210),
+ 'lightgray': RGBColor(211, 211, 211),
+ 'lightgreen': RGBColor(144, 238, 144),
+ 'lightgrey': RGBColor(211, 211, 211),
+ 'lightpink': RGBColor(255, 182, 193),
+ 'lightpink1': RGBColor(255, 174, 185),
+ 'lightpink2': RGBColor(238, 162, 173),
+ 'lightpink3': RGBColor(205, 140, 149),
+ 'lightpink4': RGBColor(139, 95, 101),
+ 'lightsalmon': RGBColor(255, 160, 122),
+ 'lightsalmon1': RGBColor(255, 160, 122),
+ 'lightsalmon2': RGBColor(238, 149, 114),
+ 'lightsalmon3': RGBColor(205, 129, 98),
+ 'lightsalmon4': RGBColor(139, 87, 66),
+ 'lightseagreen': RGBColor(32, 178, 170),
+ 'lightskyblue': RGBColor(135, 206, 250),
+ 'lightskyblue1': RGBColor(176, 226, 255),
+ 'lightskyblue2': RGBColor(164, 211, 238),
+ 'lightskyblue3': RGBColor(141, 182, 205),
+ 'lightskyblue4': RGBColor(96, 123, 139),
+ 'lightslateblue': RGBColor(132, 112, 255),
+ 'lightslategray': RGBColor(119, 136, 153),
+ 'lightslategrey': RGBColor(119, 136, 153),
+ 'lightsteelblue': RGBColor(176, 196, 222),
+ 'lightsteelblue1': RGBColor(202, 225, 255),
+ 'lightsteelblue2': RGBColor(188, 210, 238),
+ 'lightsteelblue3': RGBColor(162, 181, 205),
+ 'lightsteelblue4': RGBColor(110, 123, 139),
+ 'lightyellow': RGBColor(255, 255, 224),
+ 'lightyellow1': RGBColor(255, 255, 224),
+ 'lightyellow2': RGBColor(238, 238, 209),
+ 'lightyellow3': RGBColor(205, 205, 180),
+ 'lightyellow4': RGBColor(139, 139, 122),
+ 'lime': RGBColor(0, 255, 0),
+ 'limegreen': RGBColor(50, 205, 50),
+ 'linen': RGBColor(250, 240, 230),
+ 'magenta': RGBColor(255, 0, 255),
+ 'magenta1': RGBColor(255, 0, 255),
+ 'magenta2': RGBColor(238, 0, 238),
+ 'magenta3': RGBColor(205, 0, 205),
+ 'magenta4': RGBColor(139, 0, 139),
+ 'maroon': RGBColor(176, 48, 96),
+ 'maroon1': RGBColor(255, 52, 179),
+ 'maroon2': RGBColor(238, 48, 167),
+ 'maroon3': RGBColor(205, 41, 144),
+ 'maroon4': RGBColor(139, 28, 98),
+ 'mediumaquamarine': RGBColor(102, 205, 170),
+ 'mediumblue': RGBColor(0, 0, 205),
+ 'mediumorchid': RGBColor(186, 85, 211),
+ 'mediumorchid1': RGBColor(224, 102, 255),
+ 'mediumorchid2': RGBColor(209, 95, 238),
+ 'mediumorchid3': RGBColor(180, 82, 205),
+ 'mediumorchid4': RGBColor(122, 55, 139),
+ 'mediumpurple': RGBColor(147, 112, 219),
+ 'mediumpurple1': RGBColor(171, 130, 255),
+ 'mediumpurple2': RGBColor(159, 121, 238),
+ 'mediumpurple3': RGBColor(137, 104, 205),
+ 'mediumpurple4': RGBColor(93, 71, 139),
+ 'mediumseagreen': RGBColor(60, 179, 113),
+ 'mediumslateblue': RGBColor(123, 104, 238),
+ 'mediumspringgreen': RGBColor(0, 250, 154),
+ 'mediumturquoise': RGBColor(72, 209, 204),
+ 'mediumvioletred': RGBColor(199, 21, 133),
+ 'midnightblue': RGBColor(25, 25, 112),
+ 'mintcream': RGBColor(245, 255, 250),
+ 'mistyrose': RGBColor(255, 228, 225),
+ 'mistyrose1': RGBColor(255, 228, 225),
+ 'mistyrose2': RGBColor(238, 213, 210),
+ 'mistyrose3': RGBColor(205, 183, 181),
+ 'mistyrose4': RGBColor(139, 125, 123),
+ 'moccasin': RGBColor(255, 228, 181),
+ 'navajowhite': RGBColor(255, 222, 173),
+ 'navajowhite1': RGBColor(255, 222, 173),
+ 'navajowhite2': RGBColor(238, 207, 161),
+ 'navajowhite3': RGBColor(205, 179, 139),
+ 'navajowhite4': RGBColor(139, 121, 94),
+ 'navy': RGBColor(0, 0, 128),
+ 'navyblue': RGBColor(0, 0, 128),
+ 'oldlace': RGBColor(253, 245, 230),
+ 'olive': RGBColor(128, 128, 0),
+ 'olivedrab': RGBColor(107, 142, 35),
+ 'olivedrab1': RGBColor(192, 255, 62),
+ 'olivedrab2': RGBColor(179, 238, 58),
+ 'olivedrab3': RGBColor(154, 205, 50),
+ 'olivedrab4': RGBColor(105, 139, 34),
+ 'orange': RGBColor(255, 165, 0),
+ 'orange1': RGBColor(255, 165, 0),
+ 'orange2': RGBColor(238, 154, 0),
+ 'orange3': RGBColor(205, 133, 0),
+ 'orange4': RGBColor(139, 90, 0),
+ 'orangered': RGBColor(255, 69, 0),
+ 'orangered1': RGBColor(255, 69, 0),
+ 'orangered2': RGBColor(238, 64, 0),
+ 'orangered3': RGBColor(205, 55, 0),
+ 'orangered4': RGBColor(139, 37, 0),
+ 'orchid': RGBColor(218, 112, 214),
+ 'orchid1': RGBColor(255, 131, 250),
+ 'orchid2': RGBColor(238, 122, 233),
+ 'orchid3': RGBColor(205, 105, 201),
+ 'orchid4': RGBColor(139, 71, 137),
+ 'palegoldenrod': RGBColor(238, 232, 170),
+ 'palegreen': RGBColor(152, 251, 152),
+ 'palegreen1': RGBColor(154, 255, 154),
+ 'palegreen2': RGBColor(144, 238, 144),
+ 'palegreen3': RGBColor(124, 205, 124),
+ 'palegreen4': RGBColor(84, 139, 84),
+ 'paleturquoise': RGBColor(175, 238, 238),
+ 'paleturquoise1': RGBColor(187, 255, 255),
+ 'paleturquoise2': RGBColor(174, 238, 238),
+ 'paleturquoise3': RGBColor(150, 205, 205),
+ 'paleturquoise4': RGBColor(102, 139, 139),
+ 'palevioletred': RGBColor(219, 112, 147),
+ 'palevioletred1': RGBColor(255, 130, 171),
+ 'palevioletred2': RGBColor(238, 121, 159),
+ 'palevioletred3': RGBColor(205, 104, 137),
+ 'palevioletred4': RGBColor(139, 71, 93),
+ 'papayawhip': RGBColor(255, 239, 213),
+ 'peachpuff': RGBColor(255, 218, 185),
+ 'peachpuff1': RGBColor(255, 218, 185),
+ 'peachpuff2': RGBColor(238, 203, 173),
+ 'peachpuff3': RGBColor(205, 175, 149),
+ 'peachpuff4': RGBColor(139, 119, 101),
+ 'peru': RGBColor(205, 133, 63),
+ 'pink': RGBColor(255, 192, 203),
+ 'pink1': RGBColor(255, 181, 197),
+ 'pink2': RGBColor(238, 169, 184),
+ 'pink3': RGBColor(205, 145, 158),
+ 'pink4': RGBColor(139, 99, 108),
+ 'plum': RGBColor(221, 160, 221),
+ 'plum1': RGBColor(255, 187, 255),
+ 'plum2': RGBColor(238, 174, 238),
+ 'plum3': RGBColor(205, 150, 205),
+ 'plum4': RGBColor(139, 102, 139),
+ 'powderblue': RGBColor(176, 224, 230),
+ 'purple': RGBColor(160, 32, 240),
+ 'purple1': RGBColor(155, 48, 255),
+ 'purple2': RGBColor(145, 44, 238),
+ 'purple3': RGBColor(125, 38, 205),
+ 'purple4': RGBColor(85, 26, 139),
+ 'rebeccapurple': RGBColor(102, 51, 153),
+ 'red': RGBColor(255, 0, 0),
+ 'red1': RGBColor(255, 0, 0),
+ 'red2': RGBColor(238, 0, 0),
+ 'red3': RGBColor(205, 0, 0),
+ 'red4': RGBColor(139, 0, 0),
+ 'rosybrown': RGBColor(188, 143, 143),
+ 'rosybrown1': RGBColor(255, 193, 193),
+ 'rosybrown2': RGBColor(238, 180, 180),
+ 'rosybrown3': RGBColor(205, 155, 155),
+ 'rosybrown4': RGBColor(139, 105, 105),
+ 'royalblue': RGBColor(65, 105, 225),
+ 'royalblue1': RGBColor(72, 118, 255),
+ 'royalblue2': RGBColor(67, 110, 238),
+ 'royalblue3': RGBColor(58, 95, 205),
+ 'royalblue4': RGBColor(39, 64, 139),
+ 'saddlebrown': RGBColor(139, 69, 19),
+ 'salmon': RGBColor(250, 128, 114),
+ 'salmon1': RGBColor(255, 140, 105),
+ 'salmon2': RGBColor(238, 130, 98),
+ 'salmon3': RGBColor(205, 112, 84),
+ 'salmon4': RGBColor(139, 76, 57),
+ 'sandybrown': RGBColor(244, 164, 96),
+ 'seagreen': RGBColor(46, 139, 87),
+ 'seagreen1': RGBColor(84, 255, 159),
+ 'seagreen2': RGBColor(78, 238, 148),
+ 'seagreen3': RGBColor(67, 205, 128),
+ 'seagreen4': RGBColor(46, 139, 87),
+ 'seashell': RGBColor(255, 245, 238),
+ 'seashell1': RGBColor(255, 245, 238),
+ 'seashell2': RGBColor(238, 229, 222),
+ 'seashell3': RGBColor(205, 197, 191),
+ 'seashell4': RGBColor(139, 134, 130),
+ 'sienna': RGBColor(160, 82, 45),
+ 'sienna1': RGBColor(255, 130, 71),
+ 'sienna2': RGBColor(238, 121, 66),
+ 'sienna3': RGBColor(205, 104, 57),
+ 'sienna4': RGBColor(139, 71, 38),
+ 'silver': RGBColor(192, 192, 192),
+ 'skyblue': RGBColor(135, 206, 235),
+ 'skyblue1': RGBColor(135, 206, 255),
+ 'skyblue2': RGBColor(126, 192, 238),
+ 'skyblue3': RGBColor(108, 166, 205),
+ 'skyblue4': RGBColor(74, 112, 139),
+ 'slateblue': RGBColor(106, 90, 205),
+ 'slateblue1': RGBColor(131, 111, 255),
+ 'slateblue2': RGBColor(122, 103, 238),
+ 'slateblue3': RGBColor(105, 89, 205),
+ 'slateblue4': RGBColor(71, 60, 139),
+ 'slategray': RGBColor(112, 128, 144),
+ 'slategray1': RGBColor(198, 226, 255),
+ 'slategray2': RGBColor(185, 211, 238),
+ 'slategray3': RGBColor(159, 182, 205),
+ 'slategray4': RGBColor(108, 123, 139),
+ 'slategrey': RGBColor(112, 128, 144),
+ 'snow': RGBColor(255, 250, 250),
+ 'snow1': RGBColor(255, 250, 250),
+ 'snow2': RGBColor(238, 233, 233),
+ 'snow3': RGBColor(205, 201, 201),
+ 'snow4': RGBColor(139, 137, 137),
+ 'springgreen': RGBColor(0, 255, 127),
+ 'springgreen1': RGBColor(0, 255, 127),
+ 'springgreen2': RGBColor(0, 238, 118),
+ 'springgreen3': RGBColor(0, 205, 102),
+ 'springgreen4': RGBColor(0, 139, 69),
+ 'steelblue': RGBColor(70, 130, 180),
+ 'steelblue1': RGBColor(99, 184, 255),
+ 'steelblue2': RGBColor(92, 172, 238),
+ 'steelblue3': RGBColor(79, 148, 205),
+ 'steelblue4': RGBColor(54, 100, 139),
+ 'tan': RGBColor(210, 180, 140),
+ 'tan1': RGBColor(255, 165, 79),
+ 'tan2': RGBColor(238, 154, 73),
+ 'tan3': RGBColor(205, 133, 63),
+ 'tan4': RGBColor(139, 90, 43),
+ 'teal': RGBColor(0, 128, 128),
+ 'thistle': RGBColor(216, 191, 216),
+ 'thistle1': RGBColor(255, 225, 255),
+ 'thistle2': RGBColor(238, 210, 238),
+ 'thistle3': RGBColor(205, 181, 205),
+ 'thistle4': RGBColor(139, 123, 139),
+ 'tomato': RGBColor(255, 99, 71),
+ 'tomato1': RGBColor(255, 99, 71),
+ 'tomato2': RGBColor(238, 92, 66),
+ 'tomato3': RGBColor(205, 79, 57),
+ 'tomato4': RGBColor(139, 54, 38),
+ 'turquoise': RGBColor(64, 224, 208),
+ 'turquoise1': RGBColor(0, 245, 255),
+ 'turquoise2': RGBColor(0, 229, 238),
+ 'turquoise3': RGBColor(0, 197, 205),
+ 'turquoise4': RGBColor(0, 134, 139),
+ 'violet': RGBColor(238, 130, 238),
+ 'violetred': RGBColor(208, 32, 144),
+ 'violetred1': RGBColor(255, 62, 150),
+ 'violetred2': RGBColor(238, 58, 140),
+ 'violetred3': RGBColor(205, 50, 120),
+ 'violetred4': RGBColor(139, 34, 82),
+ 'webgray': RGBColor(128, 128, 128),
+ 'webgreen': RGBColor(0, 128, 0),
+ 'webgrey': RGBColor(128, 128, 128),
+ 'webmaroon': RGBColor(128, 0, 0),
+ 'webpurple': RGBColor(128, 0, 128),
+ 'wheat': RGBColor(245, 222, 179),
+ 'wheat1': RGBColor(255, 231, 186),
+ 'wheat2': RGBColor(238, 216, 174),
+ 'wheat3': RGBColor(205, 186, 150),
+ 'wheat4': RGBColor(139, 126, 102),
+ 'white': RGBColor(255, 255, 255),
+ 'whitesmoke': RGBColor(245, 245, 245),
+ 'x11gray': RGBColor(190, 190, 190),
+ 'x11green': RGBColor(0, 255, 0),
+ 'x11grey': RGBColor(190, 190, 190),
+ 'x11maroon': RGBColor(176, 48, 96),
+ 'x11purple': RGBColor(160, 32, 240),
+ 'yellow': RGBColor(255, 255, 0),
+ 'yellow1': RGBColor(255, 255, 0),
+ 'yellow2': RGBColor(238, 238, 0),
+ 'yellow3': RGBColor(205, 205, 0),
+ 'yellow4': RGBColor(139, 139, 0),
+ 'yellowgreen': RGBColor(154, 205, 50)
+}
+
+#: Curses color indices of 8, 16, and 256-color terminals
+RGB_256TABLE = (
+ RGBColor(0, 0, 0),
+ RGBColor(205, 0, 0),
+ RGBColor(0, 205, 0),
+ RGBColor(205, 205, 0),
+ RGBColor(0, 0, 238),
+ RGBColor(205, 0, 205),
+ RGBColor(0, 205, 205),
+ RGBColor(229, 229, 229),
+ RGBColor(127, 127, 127),
+ RGBColor(255, 0, 0),
+ RGBColor(0, 255, 0),
+ RGBColor(255, 255, 0),
+ RGBColor(92, 92, 255),
+ RGBColor(255, 0, 255),
+ RGBColor(0, 255, 255),
+ RGBColor(255, 255, 255),
+ RGBColor(0, 0, 0),
+ RGBColor(0, 0, 95),
+ RGBColor(0, 0, 135),
+ RGBColor(0, 0, 175),
+ RGBColor(0, 0, 215),
+ RGBColor(0, 0, 255),
+ RGBColor(0, 95, 0),
+ RGBColor(0, 95, 95),
+ RGBColor(0, 95, 135),
+ RGBColor(0, 95, 175),
+ RGBColor(0, 95, 215),
+ RGBColor(0, 95, 255),
+ RGBColor(0, 135, 0),
+ RGBColor(0, 135, 95),
+ RGBColor(0, 135, 135),
+ RGBColor(0, 135, 175),
+ RGBColor(0, 135, 215),
+ RGBColor(0, 135, 255),
+ RGBColor(0, 175, 0),
+ RGBColor(0, 175, 95),
+ RGBColor(0, 175, 135),
+ RGBColor(0, 175, 175),
+ RGBColor(0, 175, 215),
+ RGBColor(0, 175, 255),
+ RGBColor(0, 215, 0),
+ RGBColor(0, 215, 95),
+ RGBColor(0, 215, 135),
+ RGBColor(0, 215, 175),
+ RGBColor(0, 215, 215),
+ RGBColor(0, 215, 255),
+ RGBColor(0, 255, 0),
+ RGBColor(0, 255, 95),
+ RGBColor(0, 255, 135),
+ RGBColor(0, 255, 175),
+ RGBColor(0, 255, 215),
+ RGBColor(0, 255, 255),
+ RGBColor(95, 0, 0),
+ RGBColor(95, 0, 95),
+ RGBColor(95, 0, 135),
+ RGBColor(95, 0, 175),
+ RGBColor(95, 0, 215),
+ RGBColor(95, 0, 255),
+ RGBColor(95, 95, 0),
+ RGBColor(95, 95, 95),
+ RGBColor(95, 95, 135),
+ RGBColor(95, 95, 175),
+ RGBColor(95, 95, 215),
+ RGBColor(95, 95, 255),
+ RGBColor(95, 135, 0),
+ RGBColor(95, 135, 95),
+ RGBColor(95, 135, 135),
+ RGBColor(95, 135, 175),
+ RGBColor(95, 135, 215),
+ RGBColor(95, 135, 255),
+ RGBColor(95, 175, 0),
+ RGBColor(95, 175, 95),
+ RGBColor(95, 175, 135),
+ RGBColor(95, 175, 175),
+ RGBColor(95, 175, 215),
+ RGBColor(95, 175, 255),
+ RGBColor(95, 215, 0),
+ RGBColor(95, 215, 95),
+ RGBColor(95, 215, 135),
+ RGBColor(95, 215, 175),
+ RGBColor(95, 215, 215),
+ RGBColor(95, 215, 255),
+ RGBColor(95, 255, 0),
+ RGBColor(95, 255, 95),
+ RGBColor(95, 255, 135),
+ RGBColor(95, 255, 175),
+ RGBColor(95, 255, 215),
+ RGBColor(95, 255, 255),
+ RGBColor(135, 0, 0),
+ RGBColor(135, 0, 95),
+ RGBColor(135, 0, 135),
+ RGBColor(135, 0, 175),
+ RGBColor(135, 0, 215),
+ RGBColor(135, 0, 255),
+ RGBColor(135, 95, 0),
+ RGBColor(135, 95, 95),
+ RGBColor(135, 95, 135),
+ RGBColor(135, 95, 175),
+ RGBColor(135, 95, 215),
+ RGBColor(135, 95, 255),
+ RGBColor(135, 135, 0),
+ RGBColor(135, 135, 95),
+ RGBColor(135, 135, 135),
+ RGBColor(135, 135, 175),
+ RGBColor(135, 135, 215),
+ RGBColor(135, 135, 255),
+ RGBColor(135, 175, 0),
+ RGBColor(135, 175, 95),
+ RGBColor(135, 175, 135),
+ RGBColor(135, 175, 175),
+ RGBColor(135, 175, 215),
+ RGBColor(135, 175, 255),
+ RGBColor(135, 215, 0),
+ RGBColor(135, 215, 95),
+ RGBColor(135, 215, 135),
+ RGBColor(135, 215, 175),
+ RGBColor(135, 215, 215),
+ RGBColor(135, 215, 255),
+ RGBColor(135, 255, 0),
+ RGBColor(135, 255, 95),
+ RGBColor(135, 255, 135),
+ RGBColor(135, 255, 175),
+ RGBColor(135, 255, 215),
+ RGBColor(135, 255, 255),
+ RGBColor(175, 0, 0),
+ RGBColor(175, 0, 95),
+ RGBColor(175, 0, 135),
+ RGBColor(175, 0, 175),
+ RGBColor(175, 0, 215),
+ RGBColor(175, 0, 255),
+ RGBColor(175, 95, 0),
+ RGBColor(175, 95, 95),
+ RGBColor(175, 95, 135),
+ RGBColor(175, 95, 175),
+ RGBColor(175, 95, 215),
+ RGBColor(175, 95, 255),
+ RGBColor(175, 135, 0),
+ RGBColor(175, 135, 95),
+ RGBColor(175, 135, 135),
+ RGBColor(175, 135, 175),
+ RGBColor(175, 135, 215),
+ RGBColor(175, 135, 255),
+ RGBColor(175, 175, 0),
+ RGBColor(175, 175, 95),
+ RGBColor(175, 175, 135),
+ RGBColor(175, 175, 175),
+ RGBColor(175, 175, 215),
+ RGBColor(175, 175, 255),
+ RGBColor(175, 215, 0),
+ RGBColor(175, 215, 95),
+ RGBColor(175, 215, 135),
+ RGBColor(175, 215, 175),
+ RGBColor(175, 215, 215),
+ RGBColor(175, 215, 255),
+ RGBColor(175, 255, 0),
+ RGBColor(175, 255, 95),
+ RGBColor(175, 255, 135),
+ RGBColor(175, 255, 175),
+ RGBColor(175, 255, 215),
+ RGBColor(175, 255, 255),
+ RGBColor(215, 0, 0),
+ RGBColor(215, 0, 95),
+ RGBColor(215, 0, 135),
+ RGBColor(215, 0, 175),
+ RGBColor(215, 0, 215),
+ RGBColor(215, 0, 255),
+ RGBColor(215, 95, 0),
+ RGBColor(215, 95, 95),
+ RGBColor(215, 95, 135),
+ RGBColor(215, 95, 175),
+ RGBColor(215, 95, 215),
+ RGBColor(215, 95, 255),
+ RGBColor(215, 135, 0),
+ RGBColor(215, 135, 95),
+ RGBColor(215, 135, 135),
+ RGBColor(215, 135, 175),
+ RGBColor(215, 135, 215),
+ RGBColor(215, 135, 255),
+ RGBColor(215, 175, 0),
+ RGBColor(215, 175, 95),
+ RGBColor(215, 175, 135),
+ RGBColor(215, 175, 175),
+ RGBColor(215, 175, 215),
+ RGBColor(215, 175, 255),
+ RGBColor(215, 215, 0),
+ RGBColor(215, 215, 95),
+ RGBColor(215, 215, 135),
+ RGBColor(215, 215, 175),
+ RGBColor(215, 215, 215),
+ RGBColor(215, 215, 255),
+ RGBColor(215, 255, 0),
+ RGBColor(215, 255, 95),
+ RGBColor(215, 255, 135),
+ RGBColor(215, 255, 175),
+ RGBColor(215, 255, 215),
+ RGBColor(215, 255, 255),
+ RGBColor(255, 0, 0),
+ RGBColor(255, 0, 135),
+ RGBColor(255, 0, 95),
+ RGBColor(255, 0, 175),
+ RGBColor(255, 0, 215),
+ RGBColor(255, 0, 255),
+ RGBColor(255, 95, 0),
+ RGBColor(255, 95, 95),
+ RGBColor(255, 95, 135),
+ RGBColor(255, 95, 175),
+ RGBColor(255, 95, 215),
+ RGBColor(255, 95, 255),
+ RGBColor(255, 135, 0),
+ RGBColor(255, 135, 95),
+ RGBColor(255, 135, 135),
+ RGBColor(255, 135, 175),
+ RGBColor(255, 135, 215),
+ RGBColor(255, 135, 255),
+ RGBColor(255, 175, 0),
+ RGBColor(255, 175, 95),
+ RGBColor(255, 175, 135),
+ RGBColor(255, 175, 175),
+ RGBColor(255, 175, 215),
+ RGBColor(255, 175, 255),
+ RGBColor(255, 215, 0),
+ RGBColor(255, 215, 95),
+ RGBColor(255, 215, 135),
+ RGBColor(255, 215, 175),
+ RGBColor(255, 215, 215),
+ RGBColor(255, 215, 255),
+ RGBColor(255, 255, 0),
+ RGBColor(255, 255, 95),
+ RGBColor(255, 255, 135),
+ RGBColor(255, 255, 175),
+ RGBColor(255, 255, 215),
+ RGBColor(255, 255, 255),
+ RGBColor(8, 8, 8),
+ RGBColor(18, 18, 18),
+ RGBColor(28, 28, 28),
+ RGBColor(38, 38, 38),
+ RGBColor(48, 48, 48),
+ RGBColor(58, 58, 58),
+ RGBColor(68, 68, 68),
+ RGBColor(78, 78, 78),
+ RGBColor(88, 88, 88),
+ RGBColor(98, 98, 98),
+ RGBColor(108, 108, 108),
+ RGBColor(118, 118, 118),
+ RGBColor(128, 128, 128),
+ RGBColor(138, 138, 138),
+ RGBColor(148, 148, 148),
+ RGBColor(158, 158, 158),
+ RGBColor(168, 168, 168),
+ RGBColor(178, 178, 178),
+ RGBColor(188, 188, 188),
+ RGBColor(198, 198, 198),
+ RGBColor(208, 208, 208),
+ RGBColor(218, 218, 218),
+ RGBColor(228, 228, 228),
+ RGBColor(238, 238, 238),
+)
diff --git a/blessed/formatters.py b/blessed/formatters.py
new file mode 100644
index 00000000..83bee881
--- /dev/null
+++ b/blessed/formatters.py
@@ -0,0 +1,442 @@
+"""Sub-module providing sequence-formatting functions."""
+# std imports
+import platform
+
+# 3rd party
+import six
+
+# local
+from blessed.colorspace import CGA_COLORS, X11_COLORNAMES_TO_RGB
+
+# curses
+if platform.system() == 'Windows':
+ import jinxed as curses # pylint: disable=import-error
+else:
+ import curses
+
+
+def _make_colors():
+ """
+ Return set of valid colors and their derivatives.
+
+ :rtype: set
+ """
+ colors = set()
+ # basic CGA foreground color, background, high intensity, and bold
+ # background ('iCE colors' in my day).
+ for cga_color in CGA_COLORS:
+ colors.add(cga_color)
+ colors.add('on_' + cga_color)
+ colors.add('bright_' + cga_color)
+ colors.add('on_bright_' + cga_color)
+
+ # foreground and background VGA color
+ for vga_color in X11_COLORNAMES_TO_RGB:
+ colors.add(vga_color)
+ colors.add('on_' + vga_color)
+ return colors
+
+
+#: Valid colors and their background (on), bright, and bright-background
+#: derivatives.
+COLORS = _make_colors()
+
+#: Attributes that may be compounded with colors, by underscore, such as
+#: 'reverse_indigo'.
+COMPOUNDABLES = set('bold underline reverse blink italic standout'.split())
+
+
+class ParameterizingString(six.text_type):
+ r"""
+ A Unicode string which can be called as a parameterizing termcap.
+
+ For example::
+
+ >>> term = Terminal()
+ >>> color = ParameterizingString(term.color, term.normal, 'color')
+ >>> color(9)('color #9')
+ u'\x1b[91mcolor #9\x1b(B\x1b[m'
+ """
+
+ def __new__(cls, *args):
+ """
+ Class constructor accepting 3 positional arguments.
+
+ :arg cap: parameterized string suitable for curses.tparm()
+ :arg normal: terminating sequence for this capability (optional).
+ :arg name: name of this terminal capability (optional).
+ """
+ assert args and len(args) < 4, args
+ new = six.text_type.__new__(cls, args[0])
+ new._normal = args[1] if len(args) > 1 else u''
+ new._name = args[2] if len(args) > 2 else u''
+ return new
+
+ def __call__(self, *args):
+ """
+ Returning :class:`FormattingString` instance for given parameters.
+
+ Return evaluated terminal capability (self), receiving arguments
+ ``*args``, followed by the terminating sequence (self.normal) into
+ a :class:`FormattingString` capable of being called.
+
+ :rtype: :class:`FormattingString` or :class:`NullCallableString`
+ """
+ try:
+ # Re-encode the cap, because tparm() takes a bytestring in Python
+ # 3. However, appear to be a plain Unicode string otherwise so
+ # concats work.
+ attr = curses.tparm(self.encode('latin1'), *args).decode('latin1')
+ return FormattingString(attr, self._normal)
+ except TypeError as err:
+ # If the first non-int (i.e. incorrect) arg was a string, suggest
+ # something intelligent:
+ if args and isinstance(args[0], six.string_types):
+ raise TypeError(
+ "A native or nonexistent capability template, %r received"
+ " invalid argument %r: %s. You probably misspelled a"
+ " formatting call like `bright_red'" % (
+ self._name, args, err))
+ # Somebody passed a non-string; I don't feel confident
+ # guessing what they were trying to do.
+ raise
+ except curses.error as err:
+ # ignore 'tparm() returned NULL', you won't get any styling,
+ # even if does_styling is True. This happens on win32 platforms
+ # with http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed
+ if "tparm() returned NULL" not in six.text_type(err):
+ raise
+ return NullCallableString()
+
+
+class ParameterizingProxyString(six.text_type):
+ r"""
+ A Unicode string which can be called to proxy missing termcap entries.
+
+ This class supports the function :func:`get_proxy_string`, and mirrors
+ the behavior of :class:`ParameterizingString`, except that instead of
+ a capability name, receives a format string, and callable to filter the
+ given positional ``*args`` of :meth:`ParameterizingProxyString.__call__`
+ into a terminal sequence.
+
+ For example::
+
+ >>> from blessed import Terminal
+ >>> term = Terminal('screen')
+ >>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa')
+ >>> hpa(9)
+ u''
+ >>> fmt = u'\x1b[{0}G'
+ >>> fmt_arg = lambda *arg: (arg[0] + 1,)
+ >>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa')
+ >>> hpa(9)
+ u'\x1b[10G'
+ """
+
+ def __new__(cls, *args):
+ """
+ Class constructor accepting 4 positional arguments.
+
+ :arg fmt: format string suitable for displaying terminal sequences.
+ :arg callable: receives __call__ arguments for formatting fmt.
+ :arg normal: terminating sequence for this capability (optional).
+ :arg name: name of this terminal capability (optional).
+ """
+ assert args and len(args) < 4, args
+ assert isinstance(args[0], tuple), args[0]
+ assert callable(args[0][1]), args[0][1]
+ new = six.text_type.__new__(cls, args[0][0])
+ new._fmt_args = args[0][1]
+ new._normal = args[1] if len(args) > 1 else u''
+ new._name = args[2] if len(args) > 2 else u''
+ return new
+
+ def __call__(self, *args):
+ """
+ Returning :class:`FormattingString` instance for given parameters.
+
+ Arguments are determined by the capability. For example, ``hpa``
+ (move_x) receives only a single integer, whereas ``cup`` (move)
+ receives two integers. See documentation in terminfo(5) for the
+ given capability.
+
+ :rtype: FormattingString
+ """
+ return FormattingString(self.format(*self._fmt_args(*args)),
+ self._normal)
+
+
+def get_proxy_string(term, attr):
+ """
+ Proxy and return callable string for proxied attributes.
+
+ :arg Terminal term: :class:`~.Terminal` instance.
+ :arg str attr: terminal capability name that may be proxied.
+ :rtype: None or :class:`ParameterizingProxyString`.
+ :returns: :class:`ParameterizingProxyString` for some attributes
+ of some terminal types that support it, where the terminfo(5)
+ database would otherwise come up empty, such as ``move_x``
+ attribute for ``term.kind`` of ``screen``. Otherwise, None.
+ """
+ # normalize 'screen-256color', or 'ansi.sys' to its basic names
+ term_kind = next(iter(_kind for _kind in ('screen', 'ansi',)
+ if term.kind.startswith(_kind)), term)
+ _proxy_table = { # pragma: no cover
+ 'screen': {
+ # proxy move_x/move_y for 'screen' terminal type, used by tmux(1).
+ 'hpa': ParameterizingProxyString(
+ (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ 'vpa': ParameterizingProxyString(
+ (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ },
+ 'ansi': {
+ # proxy show/hide cursor for 'ansi' terminal type. There is some
+ # demand for a richly working ANSI terminal type for some reason.
+ 'civis': ParameterizingProxyString(
+ (u'\x1b[?25l', lambda *arg: ()), term.normal, attr),
+ 'cnorm': ParameterizingProxyString(
+ (u'\x1b[?25h', lambda *arg: ()), term.normal, attr),
+ 'hpa': ParameterizingProxyString(
+ (u'\x1b[{0}G', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ 'vpa': ParameterizingProxyString(
+ (u'\x1b[{0}d', lambda *arg: (arg[0] + 1,)), term.normal, attr),
+ 'sc': '\x1b[s',
+ 'rc': '\x1b[u',
+ }
+ }
+ return _proxy_table.get(term_kind, {}).get(attr, None)
+
+
+class FormattingString(six.text_type):
+ r"""
+ A Unicode string which doubles as a callable.
+
+ This is used for terminal attributes, so that it may be used both
+ directly, or as a callable. When used directly, it simply emits
+ the given terminal sequence. When used as a callable, it wraps the
+ given (string) argument with the 2nd argument used by the class
+ constructor::
+
+ >>> style = FormattingString(term.bright_blue, term.normal)
+ >>> print(repr(style))
+ u'\x1b[94m'
+ >>> style('Big Blue')
+ u'\x1b[94mBig Blue\x1b(B\x1b[m'
+ """
+
+ def __new__(cls, *args):
+ """
+ Class constructor accepting 2 positional arguments.
+
+ :arg sequence: terminal attribute sequence.
+ :arg normal: terminating sequence for this attribute (optional).
+ """
+ assert 1 <= len(args) <= 2, args
+ new = six.text_type.__new__(cls, args[0])
+ new._normal = args[1] if len(args) > 1 else u''
+ return new
+
+ def __call__(self, *args):
+ """Return ``text`` joined by ``sequence`` and ``normal``."""
+ # Jim Allman brings us this convenience of allowing existing
+ # unicode strings to be joined as a call parameter to a formatting
+ # string result, allowing nestation:
+ #
+ # >>> t.red('This is ', t.bold('extremely'), ' dangerous!')
+ for idx, ucs_part in enumerate(args):
+ if not isinstance(ucs_part, six.string_types):
+ raise TypeError("Positional argument #{idx} is {is_type} "
+ "expected any of {expected_types}: "
+ "{ucs_part!r}".format(
+ idx=idx, ucs_part=ucs_part,
+ is_type=type(ucs_part),
+ expected_types=six.string_types,
+ ))
+ postfix = u''
+ if self and self._normal:
+ postfix = self._normal
+ _refresh = self._normal + self
+ args = [_refresh.join(ucs_part.split(self._normal))
+ for ucs_part in args]
+
+ return self + u''.join(args) + postfix
+
+
+class NullCallableString(six.text_type):
+ """
+ A dummy callable Unicode alternative to :class:`FormattingString`.
+
+ This is used for colors on terminals that do not support colors, it is just a basic form of
+ unicode that may also act as a callable.
+ """
+
+ def __new__(cls):
+ """Class constructor."""
+ new = six.text_type.__new__(cls, u'')
+ return new
+
+ def __call__(self, *args):
+ """
+ Allow empty string to be callable, returning given string, if any.
+
+ When called with an int as the first arg, return an empty Unicode. An
+ int is a good hint that I am a :class:`ParameterizingString`, as there
+ are only about half a dozen string-returning capabilities listed in
+ terminfo(5) which accept non-int arguments, they are seldom used.
+
+ When called with a non-int as the first arg (no no args at all), return
+ the first arg, acting in place of :class:`FormattingString` without
+ any attributes.
+ """
+ if not args or isinstance(args[0], int):
+ # As a NullCallableString, even when provided with a parameter,
+ # such as t.color(5), we must also still be callable, fe:
+ #
+ # >>> t.color(5)('shmoo')
+ #
+ # is actually simplified result of NullCallable()() on terminals
+ # without color support, so turtles all the way down: we return
+ # another instance.
+ return NullCallableString()
+ return u''.join(args)
+
+
+def split_compound(compound):
+ """
+ Split compound formating string into segments.
+
+ >>> split_compound('bold_underline_bright_blue_on_red')
+ ['bold', 'underline', 'bright_blue', 'on_red']
+
+ :arg str compound: a string that may contain compounds, separated by
+ underline (``_``).
+ :rtype: list
+ """
+ merged_segs = []
+ # These occur only as prefixes, so they can always be merged:
+ mergeable_prefixes = ['on', 'bright', 'on_bright']
+ for segment in compound.split('_'):
+ if merged_segs and merged_segs[-1] in mergeable_prefixes:
+ merged_segs[-1] += '_' + segment
+ else:
+ merged_segs.append(segment)
+ return merged_segs
+
+
+def resolve_capability(term, attr):
+ """
+ Resolve a raw terminal capability using :func:`tigetstr`.
+
+ :arg Terminal term: :class:`~.Terminal` instance.
+ :arg str attr: terminal capability name.
+ :returns: string of the given terminal capability named by ``attr``,
+ which may be empty (u'') if not found or not supported by the
+ given :attr:`~.Terminal.kind`.
+ :rtype: str
+ """
+ # Decode sequences as latin1, as they are always 8-bit bytes, so when
+ # b'\xff' is returned, this must be decoded to u'\xff'.
+ if not term.does_styling:
+ return u''
+ val = curses.tigetstr(term._sugar.get(attr, attr))
+ return u'' if val is None else val.decode('latin1')
+
+
+def resolve_color(term, color):
+ """
+ Resolve a simple color name to a callable capability.
+
+ This function supports :func:`resolve_attribute`.
+
+ :arg Terminal term: :class:`~.Terminal` instance.
+ :arg str color: any string found in set :const:`COLORS`.
+ :returns: a string class instance which emits the terminal sequence
+ for the given color, and may be used as a callable to wrap the
+ given string with such sequence.
+ :returns: :class:`NullCallableString` when
+ :attr:`~.Terminal.number_of_colors` is 0,
+ otherwise :class:`FormattingString`.
+ :rtype: :class:`NullCallableString` or :class:`FormattingString`
+ """
+ if term.number_of_colors == 0:
+ return NullCallableString()
+
+ # fg/bg capabilities terminals that support 0-256+ colors.
+ vga_color_cap = (term._background_color if 'on_' in color else
+ term._foreground_color)
+
+ base_color = color.rsplit('_', 1)[-1]
+ if base_color in CGA_COLORS:
+ # curses constants go up to only 7, so add an offset to get at the
+ # bright colors at 8-15:
+ offset = 8 if 'bright_' in color else 0
+ base_color = color.rsplit('_', 1)[-1]
+ attr = 'COLOR_%s' % (base_color.upper(),)
+ fmt_attr = vga_color_cap(getattr(curses, attr) + offset)
+ return FormattingString(fmt_attr, term.normal)
+
+ assert base_color in X11_COLORNAMES_TO_RGB, (
+ 'color not known', base_color)
+ rgb = X11_COLORNAMES_TO_RGB[base_color]
+
+ # downconvert X11 colors to CGA, EGA, or VGA color spaces
+ if term.number_of_colors <= 256:
+ fmt_attr = vga_color_cap(term.rgb_downconvert(*rgb))
+ return FormattingString(fmt_attr, term.normal)
+
+ # Modern 24-bit color terminals are written pretty basically. The
+ # foreground and background sequences are:
+ # - ^[38;2;;;m
+ # - ^[48;2;;;m
+ fgbg_seq = ('48' if 'on_' in color else '38')
+ assert term.number_of_colors == 1 << 24
+ fmt_attr = u'\x1b[' + fgbg_seq + ';2;{0};{1};{2}m'
+ return FormattingString(fmt_attr.format(*rgb), term.normal)
+
+
+def resolve_attribute(term, attr):
+ """
+ Resolve a terminal attribute name into a capability class.
+
+ :arg Terminal term: :class:`~.Terminal` instance.
+ :arg str attr: Sugary, ordinary, or compound formatted terminal
+ capability, such as "red_on_white", "normal", "red", or
+ "bold_on_black", respectively.
+ :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 | COMPOUNDABLES) for fmt in formatters):
+ resolution = (resolve_attribute(term, fmt) for fmt in formatters)
+ return FormattingString(u''.join(resolution), term.normal)
+
+ # otherwise, this is our end-game: given a sequence such as 'csr'
+ # (change scrolling region), return a ParameterizingString instance,
+ # that when called, performs and returns the final string after curses
+ # capability lookup is performed.
+ tparm_capseq = resolve_capability(term, attr)
+ if not tparm_capseq:
+ # and, for special terminals, such as 'screen', provide a Proxy
+ # ParameterizingString for attributes they do not claim to support,
+ # but actually do! (such as 'hpa' and 'vpa').
+ proxy = get_proxy_string(term, term._sugar.get(attr, attr))
+ if proxy is not None:
+ return proxy
+
+ return ParameterizingString(tparm_capseq, term.normal, attr)
diff --git a/blessed/keyboard.py b/blessed/keyboard.py
new file mode 100644
index 00000000..d1ff45a4
--- /dev/null
+++ b/blessed/keyboard.py
@@ -0,0 +1,459 @@
+"""Sub-module providing 'keyboard awareness'."""
+
+# std imports
+import re
+import time
+import platform
+
+# 3rd party
+import six
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ # python 2.6 requires 3rd party library (backport)
+ #
+ # pylint: disable=import-error
+ # Unable to import 'ordereddict'
+ from ordereddict import OrderedDict
+
+# curses
+if platform.system() == 'Windows':
+ # pylint: disable=import-error
+ import jinxed as curses
+ from jinxed.has_key import _capability_names as capability_names
+else:
+ import curses
+ from curses.has_key import _capability_names as capability_names
+
+
+class Keystroke(six.text_type):
+ """
+ A unicode-derived class for describing a single keystroke.
+
+ A class instance describes a single keystroke received on input,
+ which may contain multiple characters as a multibyte sequence,
+ which is indicated by properties :attr:`is_sequence` returning
+ ``True``.
+
+ When the string is a known sequence, :attr:`code` matches terminal
+ class attributes for comparison, such as ``term.KEY_LEFT``.
+
+ The string-name of the sequence, such as ``u'KEY_LEFT'`` is accessed
+ by property :attr:`name`, and is used by the :meth:`__repr__` method
+ to display a human-readable form of the Keystroke this class
+ instance represents. It may otherwise by joined, split, or evaluated
+ just as as any other unicode string.
+ """
+
+ def __new__(cls, ucs='', code=None, name=None):
+ """Class constructor."""
+ new = six.text_type.__new__(cls, ucs)
+ new._name = name
+ new._code = code
+ return new
+
+ @property
+ def is_sequence(self):
+ """Whether the value represents a multibyte sequence (bool)."""
+ return self._code is not None
+
+ def __repr__(self):
+ """Docstring overwritten."""
+ return (six.text_type.__repr__(self) if self._name is None else
+ self._name)
+ __repr__.__doc__ = six.text_type.__doc__
+
+ @property
+ def name(self):
+ """String-name of key sequence, such as ``u'KEY_LEFT'`` (str)."""
+ return self._name
+
+ @property
+ def code(self):
+ """Integer keycode value of multibyte sequence (int)."""
+ return self._code
+
+
+def get_curses_keycodes():
+ """
+ Return mapping of curses key-names paired by their keycode integer value.
+
+ :rtype: dict
+
+ Returns dictionary of (name, code) pairs for curses keyboard constant
+ values and their mnemonic name. Such as code ``260``, with the value of
+ its key-name identity, ``u'KEY_LEFT'``.
+ """
+ _keynames = [attr for attr in dir(curses)
+ if attr.startswith('KEY_')]
+ return dict((keyname, getattr(curses, keyname))
+ for keyname in _keynames)
+
+
+def get_keyboard_codes():
+ """
+ Return mapping of keycode integer values paired by their curses key-name.
+
+ :rtype: dict
+
+ Returns dictionary of (code, name) pairs for curses keyboard constant
+ values and their mnemonic name. Such as key ``260``, with the value of
+ its identity, ``u'KEY_LEFT'``. These are derived from the attributes by
+ the same of the curses module, with the following exceptions:
+
+ * ``KEY_DELETE`` in place of ``KEY_DC``
+ * ``KEY_INSERT`` in place of ``KEY_IC``
+ * ``KEY_PGUP`` in place of ``KEY_PPAGE``
+ * ``KEY_PGDOWN`` in place of ``KEY_NPAGE``
+ * ``KEY_ESCAPE`` in place of ``KEY_EXIT``
+ * ``KEY_SUP`` in place of ``KEY_SR``
+ * ``KEY_SDOWN`` in place of ``KEY_SF``
+
+ This function is the inverse of :func:`get_curses_keycodes`. With the
+ given override "mixins" listed above, the keycode for the delete key will
+ map to our imaginary ``KEY_DELETE`` mnemonic, effectively erasing the
+ phrase ``KEY_DC`` from our code vocabulary for anyone that wishes to use
+ the return value to determine the key-name by keycode.
+ """
+ keycodes = OrderedDict(get_curses_keycodes())
+ keycodes.update(CURSES_KEYCODE_OVERRIDE_MIXIN)
+ # merge _CURSES_KEYCODE_ADDINS added to our module space
+ keycodes.update({
+ name: value
+ for name, value in globals().items()
+ if name.startswith('KEY_')
+ })
+
+ # invert dictionary (key, values) => (values, key), preferring the
+ # last-most inserted value ('KEY_DELETE' over 'KEY_DC').
+ return dict(zip(keycodes.values(), keycodes.keys()))
+
+
+def _alternative_left_right(term):
+ r"""
+ Determine and return mapping of left and right arrow keys sequences.
+
+ :arg blessed.Terminal term: :class:`~.Terminal` instance.
+ :rtype: dict
+
+ This function supports :func:`get_terminal_sequences` to discover
+ the preferred input sequence for the left and right application keys.
+
+ Return dict of sequences ``term._cuf1``, and ``term._cub1``,
+ valued as ``KEY_RIGHT``, ``KEY_LEFT`` (when appropriate). It is
+ necessary to check the value of these sequences to ensure we do not
+ use ``u' '`` and ``u'\b'`` for ``KEY_RIGHT`` and ``KEY_LEFT``,
+ preferring their true application key sequence, instead.
+ """
+ keymap = dict()
+ if term._cuf1 and term._cuf1 != u' ':
+ keymap[term._cuf1] = curses.KEY_RIGHT
+ if term._cub1 and term._cub1 != u'\b':
+ keymap[term._cub1] = curses.KEY_LEFT
+ return keymap
+
+
+def get_keyboard_sequences(term):
+ r"""
+ Return mapping of keyboard sequences paired by keycodes.
+
+ :arg blessed.Terminal term: :class:`~.Terminal` instance.
+ :returns: mapping of keyboard unicode sequences paired by keycodes
+ as integer. This is used as the argument ``mapper`` to
+ the supporting function :func:`resolve_sequence`.
+ :rtype: OrderedDict
+
+ Initialize and return a keyboard map and sequence lookup table,
+ (sequence, keycode) from :class:`~.Terminal` instance ``term``,
+ where ``sequence`` is a multibyte input sequence of unicode
+ characters, such as ``u'\x1b[D'``, and ``keycode`` is an integer
+ value, matching curses constant such as term.KEY_LEFT.
+
+ The return value is an OrderedDict instance, with their keys
+ sorted longest-first.
+ """
+ # A small gem from curses.has_key that makes this all possible,
+ # _capability_names: a lookup table of terminal capability names for
+ # keyboard sequences (fe. kcub1, key_left), keyed by the values of
+ # constants found beginning with KEY_ in the main curses module
+ # (such as KEY_LEFT).
+ #
+ # latin1 encoding is used so that bytes in 8-bit range of 127-255
+ # have equivalent chr() and unichr() values, so that the sequence
+ # of a kermit or avatar terminal, for example, remains unchanged
+ # in its byte sequence values even when represented by unicode.
+ #
+ sequence_map = dict((
+ (seq.decode('latin1'), val)
+ for (seq, val) in (
+ (curses.tigetstr(cap), val)
+ for (val, cap) in capability_names.items()
+ ) if seq
+ ) if term.does_styling else ())
+
+ sequence_map.update(_alternative_left_right(term))
+ sequence_map.update(DEFAULT_SEQUENCE_MIXIN)
+
+ # This is for fast lookup matching of sequences, preferring
+ # full-length sequence such as ('\x1b[D', KEY_LEFT)
+ # over simple sequences such as ('\x1b', KEY_EXIT).
+ return OrderedDict((
+ (seq, sequence_map[seq]) for seq in sorted(
+ sequence_map.keys(), key=len, reverse=True)))
+
+
+def get_leading_prefixes(sequences):
+ """
+ Return a set of proper prefixes for given sequence of strings.
+
+ :arg iterable sequences
+ :rtype: set
+
+ Given an iterable of strings, all textparts leading up to the final
+ string is returned as a unique set. This function supports the
+ :meth:`~.Terminal.inkey` method by determining whether the given
+ input is a sequence that **may** lead to a final matching pattern.
+
+ >>> prefixes(['abc', 'abdf', 'e', 'jkl'])
+ set([u'a', u'ab', u'abd', u'j', u'jk'])
+ """
+ return set(seq[:i] for seq in sequences for i in range(1, len(seq)))
+
+
+def resolve_sequence(text, mapper, codes):
+ r"""
+ Return :class:`Keystroke` instance for given sequence ``text``.
+
+ The given ``text`` may extend beyond a matching sequence, such as
+ ``u\x1b[Dxxx`` returns a :class:`Keystroke` instance of attribute
+ :attr:`Keystroke.sequence` valued only ``u\x1b[D``. It is up to
+ determine that ``xxx`` remains unresolved.
+
+ :arg text: string of characters received from terminal input stream.
+ :arg OrderedDict mapper: unicode multibyte sequences, such as ``u'\x1b[D'``
+ paired by their integer value (260)
+ :arg dict codes: a :type:`dict` of integer values (such as 260) paired
+ by their mnemonic name, such as ``'KEY_LEFT'``.
+ :rtype: Keystroke
+ """
+ for sequence, code in mapper.items():
+ if text.startswith(sequence):
+ return Keystroke(ucs=sequence, code=code, name=codes[code])
+ return Keystroke(ucs=text and text[0] or u'')
+
+
+def _time_left(stime, timeout):
+ """
+ Return time remaining since ``stime`` before given ``timeout``.
+
+ This function assists determining the value of ``timeout`` for
+ class method :meth:`~.Terminal.kbhit` and similar functions.
+
+ :arg float stime: starting time for measurement
+ :arg float timeout: timeout period, may be set to None to
+ indicate no timeout (where None is always returned).
+ :rtype: float or int
+ :returns: time remaining as float. If no time is remaining,
+ then the integer ``0`` is returned.
+ """
+ return max(0, timeout - (time.time() - stime)) if timeout else timeout
+
+
+def _read_until(term, pattern, timeout):
+ """
+ Convenience read-until-pattern function, supporting :meth:`~.get_location`.
+
+ :arg blessed.Terminal term: :class:`~.Terminal` instance.
+ :arg float timeout: timeout period, may be set to None to indicate no
+ timeout (where 0 is always returned).
+ :arg str pattern: target regular expression pattern to seek.
+ :rtype: tuple
+ :returns: tuple in form of ``(match, str)``, *match*
+ may be :class:`re.MatchObject` if pattern is discovered
+ in input stream before timeout has elapsed, otherwise
+ None. ``str`` is any remaining text received exclusive
+ of the matching pattern).
+
+ The reason a tuple containing non-matching data is returned, is that the
+ consumer should push such data back into the input buffer by
+ :meth:`~.Terminal.ungetch` if any was received.
+
+ For example, when a user is performing rapid input keystrokes while its
+ terminal emulator surreptitiously responds to this in-band sequence, we
+ must ensure any such keyboard data is well-received by the next call to
+ term.inkey() without delay.
+ """
+ stime = time.time()
+ match, buf = None, u''
+
+ # first, buffer all pending data. pexpect library provides a
+ # 'searchwindowsize' attribute that limits this memory region. We're not
+ # concerned about OOM conditions: only (human) keyboard input and terminal
+ # response sequences are expected.
+
+ while True:
+ # block as long as necessary to ensure at least one character is
+ # received on input or remaining timeout has elapsed.
+ ucs = term.inkey(timeout=_time_left(stime, timeout))
+ if ucs:
+ buf += ucs
+ # while the keyboard buffer is "hot" (has input), we continue to
+ # aggregate all awaiting data. We do this to ensure slow I/O
+ # calls do not unnecessarily give up within the first 'while' loop
+ # for short timeout periods.
+ while True:
+ ucs = term.inkey(timeout=0)
+ if not ucs:
+ break
+ buf += ucs
+
+ match = re.search(pattern=pattern, string=buf)
+ if match is not None:
+ # match
+ break
+
+ if timeout is not None:
+ if not _time_left(stime, timeout):
+ # timeout
+ break
+
+ return match, buf
+
+
+#: Though we may determine *keynames* and codes for keyboard input that
+#: generate multibyte sequences, it is also especially useful to aliases
+#: a few basic ASCII characters such as ``KEY_TAB`` instead of ``u'\t'`` for
+#: uniformity.
+#:
+#: Furthermore, many key-names for application keys enabled only by context
+#: manager :meth:`~.Terminal.keypad` are surprisingly absent. We inject them
+#: here directly into the curses module.
+_CURSES_KEYCODE_ADDINS = (
+ 'TAB',
+ 'KP_MULTIPLY',
+ 'KP_ADD',
+ 'KP_SEPARATOR',
+ 'KP_SUBTRACT',
+ 'KP_DECIMAL',
+ 'KP_DIVIDE',
+ 'KP_EQUAL',
+ 'KP_0',
+ 'KP_1',
+ 'KP_2',
+ 'KP_3',
+ 'KP_4',
+ 'KP_5',
+ 'KP_6',
+ 'KP_7',
+ 'KP_8',
+ 'KP_9')
+
+_lastval = max(get_curses_keycodes().values())
+for keycode_name in _CURSES_KEYCODE_ADDINS:
+ _lastval += 1
+ globals()['KEY_' + keycode_name] = _lastval
+
+#: In a perfect world, terminal emulators would always send exactly what
+#: the terminfo(5) capability database plans for them, accordingly by the
+#: value of the ``TERM`` name they declare.
+#:
+#: But this isn't a perfect world. Many vt220-derived terminals, such as
+#: those declaring 'xterm', will continue to send vt220 codes instead of
+#: their native-declared codes, for backwards-compatibility.
+#:
+#: This goes for many: rxvt, putty, iTerm.
+#:
+#: These "mixins" are used for *all* terminals, regardless of their type.
+#:
+#: Furthermore, curses does not provide sequences sent by the keypad,
+#: at least, it does not provide a way to distinguish between keypad 0
+#: and numeric 0.
+DEFAULT_SEQUENCE_MIXIN = (
+ # these common control characters (and 127, ctrl+'?') mapped to
+ # an application key definition.
+ (six.unichr(10), curses.KEY_ENTER),
+ (six.unichr(13), curses.KEY_ENTER),
+ (six.unichr(8), curses.KEY_BACKSPACE),
+ (six.unichr(9), KEY_TAB), # noqa
+ (six.unichr(27), curses.KEY_EXIT),
+ (six.unichr(127), curses.KEY_BACKSPACE),
+
+ (u"\x1b[A", curses.KEY_UP),
+ (u"\x1b[B", curses.KEY_DOWN),
+ (u"\x1b[C", curses.KEY_RIGHT),
+ (u"\x1b[D", curses.KEY_LEFT),
+ (u"\x1b[F", curses.KEY_END),
+ (u"\x1b[H", curses.KEY_HOME),
+ # not sure where these are from .. please report
+ (u"\x1b[K", curses.KEY_END),
+ (u"\x1b[U", curses.KEY_NPAGE),
+ (u"\x1b[V", curses.KEY_PPAGE),
+
+ # keys sent after term.smkx (keypad_xmit) is emitted, source:
+ # http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys
+ # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes
+ #
+ # keypad, numlock on
+ (u"\x1bOM", curses.KEY_ENTER), # noqa return
+ (u"\x1bOj", KEY_KP_MULTIPLY), # noqa *
+ (u"\x1bOk", KEY_KP_ADD), # noqa +
+ (u"\x1bOl", KEY_KP_SEPARATOR), # noqa ,
+ (u"\x1bOm", KEY_KP_SUBTRACT), # noqa -
+ (u"\x1bOn", KEY_KP_DECIMAL), # noqa .
+ (u"\x1bOo", KEY_KP_DIVIDE), # noqa /
+ (u"\x1bOX", KEY_KP_EQUAL), # noqa =
+ (u"\x1bOp", KEY_KP_0), # noqa 0
+ (u"\x1bOq", KEY_KP_1), # noqa 1
+ (u"\x1bOr", KEY_KP_2), # noqa 2
+ (u"\x1bOs", KEY_KP_3), # noqa 3
+ (u"\x1bOt", KEY_KP_4), # noqa 4
+ (u"\x1bOu", KEY_KP_5), # noqa 5
+ (u"\x1bOv", KEY_KP_6), # noqa 6
+ (u"\x1bOw", KEY_KP_7), # noqa 7
+ (u"\x1bOx", KEY_KP_8), # noqa 8
+ (u"\x1bOy", KEY_KP_9), # noqa 9
+
+ # keypad, numlock off
+ (u"\x1b[1~", curses.KEY_FIND), # find
+ (u"\x1b[2~", curses.KEY_IC), # insert (0)
+ (u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute"
+ (u"\x1b[4~", curses.KEY_SELECT), # select
+ (u"\x1b[5~", curses.KEY_PPAGE), # pgup (9)
+ (u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3)
+ (u"\x1b[7~", curses.KEY_HOME), # home
+ (u"\x1b[8~", curses.KEY_END), # end
+ (u"\x1b[OA", curses.KEY_UP), # up (8)
+ (u"\x1b[OB", curses.KEY_DOWN), # down (2)
+ (u"\x1b[OC", curses.KEY_RIGHT), # right (6)
+ (u"\x1b[OD", curses.KEY_LEFT), # left (4)
+ (u"\x1b[OF", curses.KEY_END), # end (1)
+ (u"\x1b[OH", curses.KEY_HOME), # home (7)
+
+ # The vt220 placed F1-F4 above the keypad, in place of actual
+ # F1-F4 were local functions (hold screen, print screen,
+ # set up, data/talk, break).
+ (u"\x1bOP", curses.KEY_F1),
+ (u"\x1bOQ", curses.KEY_F2),
+ (u"\x1bOR", curses.KEY_F3),
+ (u"\x1bOS", curses.KEY_F4),
+)
+
+#: Override mixins for a few curses constants with easier
+#: mnemonics: there may only be a 1:1 mapping when only a
+#: keycode (int) is given, where these phrases are preferred.
+CURSES_KEYCODE_OVERRIDE_MIXIN = (
+ ('KEY_DELETE', curses.KEY_DC),
+ ('KEY_INSERT', curses.KEY_IC),
+ ('KEY_PGUP', curses.KEY_PPAGE),
+ ('KEY_PGDOWN', curses.KEY_NPAGE),
+ ('KEY_ESCAPE', curses.KEY_EXIT),
+ ('KEY_SUP', curses.KEY_SR),
+ ('KEY_SDOWN', curses.KEY_SF),
+ ('KEY_UP_LEFT', curses.KEY_A1),
+ ('KEY_UP_RIGHT', curses.KEY_A3),
+ ('KEY_CENTER', curses.KEY_B2),
+ ('KEY_BEGIN', curses.KEY_BEG),
+)
+
+__all__ = ('Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences',)
diff --git a/blessed/sequences.py b/blessed/sequences.py
new file mode 100644
index 00000000..dd15040c
--- /dev/null
+++ b/blessed/sequences.py
@@ -0,0 +1,433 @@
+# encoding: utf-8
+"""Module providing 'sequence awareness'."""
+# std imports
+import re
+import math
+import textwrap
+import functools
+
+# 3rd party
+import six
+import wcwidth
+
+# local
+from blessed._capabilities import CAPABILITIES_CAUSE_MOVEMENT
+
+__all__ = ('Sequence', 'SequenceTextWrapper', 'iter_parse', 'measure_length')
+
+
+class Termcap(object):
+ """Terminal capability of given variable name and pattern."""
+
+ def __init__(self, name, pattern, attribute):
+ """
+ Class initializer.
+
+ :arg str name: name describing capability.
+ :arg str pattern: regular expression string.
+ :arg str attribute: :class:`~.Terminal` attribute used to build
+ this terminal capability.
+ """
+ self.name = name
+ self.pattern = pattern
+ self.attribute = attribute
+ self._re_compiled = None
+
+ def __repr__(self):
+ # pylint: disable=redundant-keyword-arg
+ return ''.format(self=self)
+
+ @property
+ def named_pattern(self):
+ # pylint: disable=redundant-keyword-arg
+ return '(?P<{self.name}>{self.pattern})'.format(self=self)
+
+ @property
+ def re_compiled(self):
+ if self._re_compiled is None:
+ self._re_compiled = re.compile(self.pattern)
+ return self._re_compiled
+
+ @property
+ def will_move(self):
+ """Whether capability causes cursor movement."""
+ return self.name in CAPABILITIES_CAUSE_MOVEMENT
+
+ def horizontal_distance(self, text):
+ """
+ Horizontal carriage adjusted by capability, may be negative.
+
+ :rtype: int
+ :arg str text: for capabilities *parm_left_cursor*,
+ *parm_right_cursor*, provide the matching sequence
+ text, its interpreted distance is returned.
+
+ :returns: 0 except for matching '
+ """
+ value = {
+ 'cursor_left': -1,
+ 'backspace': -1,
+ 'cursor_right': 1,
+ 'tab': 8,
+ 'ascii_tab': 8,
+ }.get(self.name, None)
+ if value is not None:
+ return value
+
+ unit = {
+ 'parm_left_cursor': -1,
+ 'parm_right_cursor': 1
+ }.get(self.name, None)
+ if unit is not None:
+ value = int(self.re_compiled.match(text).group(1))
+ return unit * value
+
+ return 0
+
+ # pylint: disable=too-many-arguments
+ @classmethod
+ def build(cls, name, capability, attribute, nparams=0,
+ numeric=99, match_grouped=False, match_any=False,
+ match_optional=False):
+ r"""
+ Class factory builder for given capability definition.
+
+ :arg str name: Variable name given for this pattern.
+ :arg str capability: A unicode string representing a terminal
+ capability to build for. When ``nparams`` is non-zero, it
+ must be a callable unicode string (such as the result from
+ ``getattr(term, 'bold')``.
+ :arg attribute: The terminfo(5) capability name by which this
+ pattern is known.
+ :arg int nparams: number of positional arguments for callable.
+ :arg bool match_grouped: If the numeric pattern should be
+ grouped, ``(\d+)`` when ``True``, ``\d+`` default.
+ :arg bool match_any: When keyword argument ``nparams`` is given,
+ *any* numeric found in output is suitable for building as
+ pattern ``(\d+)``. Otherwise, only the first matching value of
+ range *(numeric - 1)* through *(numeric + 1)* will be replaced by
+ pattern ``(\d+)`` in builder.
+ :arg bool match_optional: When ``True``, building of numeric patterns
+ containing ``(\d+)`` will be built as optional, ``(\d+)?``.
+ """
+ _numeric_regex = r'\d+'
+ if match_grouped:
+ _numeric_regex = r'(\d+)'
+ if match_optional:
+ _numeric_regex = r'(\d+)?'
+ numeric = 99 if numeric is None else numeric
+
+ # basic capability attribute, not used as a callable
+ if nparams == 0:
+ return cls(name, re.escape(capability), attribute)
+
+ # a callable capability accepting numeric argument
+ _outp = re.escape(capability(*(numeric,) * nparams))
+ if not match_any:
+ for num in range(numeric - 1, numeric + 2):
+ if str(num) in _outp:
+ pattern = _outp.replace(str(num), _numeric_regex)
+ return cls(name, pattern, attribute)
+
+ if match_grouped:
+ pattern = re.sub(r'(\d+)', lambda x: _numeric_regex, _outp)
+ else:
+ pattern = re.sub(r'\d+', lambda x: _numeric_regex, _outp)
+ return cls(name, pattern, attribute)
+
+
+class SequenceTextWrapper(textwrap.TextWrapper):
+ """Docstring overridden."""
+
+ def __init__(self, width, term, **kwargs):
+ """
+ Class initializer.
+
+ This class supports the :meth:`~.Terminal.wrap` method.
+ """
+ self.term = term
+ textwrap.TextWrapper.__init__(self, width, **kwargs)
+
+ def _wrap_chunks(self, chunks):
+ """
+ Sequence-aware variant of :meth:`textwrap.TextWrapper._wrap_chunks`.
+
+ This simply ensures that word boundaries are not broken mid-sequence, as standard python
+ textwrap would incorrectly determine the length of a string containing sequences, and may
+ also break consider sequences part of a "word" that may be broken by hyphen (``-``), where
+ this implementation corrects both.
+ """
+ lines = []
+ if self.width <= 0 or not isinstance(self.width, int):
+ raise ValueError(
+ "invalid width {0!r}({1!r}) (must be integer > 0)"
+ .format(self.width, type(self.width)))
+
+ term = self.term
+ drop_whitespace = not hasattr(self, 'drop_whitespace'
+ ) or self.drop_whitespace
+ chunks.reverse()
+ while chunks:
+ cur_line = []
+ cur_len = 0
+ if lines:
+ indent = self.subsequent_indent
+ else:
+ indent = self.initial_indent
+ width = self.width - len(indent)
+ if drop_whitespace and (
+ Sequence(chunks[-1], term).strip() == '' and lines):
+ del chunks[-1]
+ while chunks:
+ chunk_len = Sequence(chunks[-1], term).length()
+ if cur_len + chunk_len <= width:
+ cur_line.append(chunks.pop())
+ cur_len += chunk_len
+ else:
+ break
+ if chunks and Sequence(chunks[-1], term).length() > width:
+ self._handle_long_word(chunks, cur_line, cur_len, width)
+ if drop_whitespace and (
+ cur_line and Sequence(cur_line[-1], term).strip() == ''):
+ del cur_line[-1]
+ if cur_line:
+ lines.append(indent + u''.join(cur_line))
+ return lines
+
+ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
+ """
+ Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`.
+
+ This simply ensures that word boundaries are not broken mid-sequence, as standard python
+ textwrap would incorrectly determine the length of a string containing sequences, and may
+ also break consider sequences part of a "word" that may be broken by hyphen (``-``), where
+ this implementation corrects both.
+ """
+ # Figure out when indent is larger than the specified width, and make
+ # sure at least one character is stripped off on every pass
+ if width < 1:
+ space_left = 1
+ else:
+ space_left = width - cur_len
+
+ # If we're allowed to break long words, then do so: put as much
+ # of the next chunk onto the current line as will fit.
+
+ if self.break_long_words:
+ term = self.term
+ chunk = reversed_chunks[-1]
+ idx = nxt = 0
+ for text, _ in iter_parse(term, chunk):
+ nxt += len(text)
+ if Sequence(chunk[:nxt], term).length() > space_left:
+ break
+ idx = nxt
+ cur_line.append(chunk[:idx])
+ reversed_chunks[-1] = chunk[idx:]
+
+ # Otherwise, we have to preserve the long word intact. Only add
+ # it to the current line if there's nothing already there --
+ # that minimizes how much we violate the width constraint.
+ elif not cur_line:
+ cur_line.append(reversed_chunks.pop())
+
+ # If we're not allowed to break long words, and there's already
+ # text on the current line, do nothing. Next time through the
+ # main loop of _wrap_chunks(), we'll wind up here again, but
+ # cur_len will be zero, so the next line will be entirely
+ # devoted to the long word that we can't handle right now.
+
+
+SequenceTextWrapper.__doc__ = textwrap.TextWrapper.__doc__
+
+
+class Sequence(six.text_type):
+ """
+ A "sequence-aware" version of the base :class:`str` class.
+
+ This unicode-derived class understands the effect of escape sequences
+ of printable length, allowing a properly implemented :meth:`rjust`,
+ :meth:`ljust`, :meth:`center`, and :meth:`length`.
+ """
+
+ def __new__(cls, sequence_text, term):
+ """
+ Class constructor.
+
+ :arg sequence_text: A string that may contain sequences.
+ :arg blessed.Terminal term: :class:`~.Terminal` instance.
+ """
+ new = six.text_type.__new__(cls, sequence_text)
+ new._term = term
+ return new
+
+ def ljust(self, width, fillchar=u' '):
+ """
+ Return string containing sequences, left-adjusted.
+
+ :arg int width: Total width given to right-adjust ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :arg str fillchar: String for padding right-of ``text``.
+ :returns: String of ``text``, right-aligned by ``width``.
+ :rtype: str
+ """
+ rightside = fillchar * int(
+ (max(0.0, float(width - self.length()))) / float(len(fillchar)))
+ return u''.join((self, rightside))
+
+ def rjust(self, width, fillchar=u' '):
+ """
+ Return string containing sequences, right-adjusted.
+
+ :arg int width: Total width given to right-adjust ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :arg str fillchar: String for padding left-of ``text``.
+ :returns: String of ``text``, right-aligned by ``width``.
+ :rtype: str
+ """
+ leftside = fillchar * int(
+ (max(0.0, float(width - self.length()))) / float(len(fillchar)))
+ return u''.join((leftside, self))
+
+ def center(self, width, fillchar=u' '):
+ """
+ Return string containing sequences, centered.
+
+ :arg int width: Total width given to center ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :arg str fillchar: String for padding left and right-of ``text``.
+ :returns: String of ``text``, centered by ``width``.
+ :rtype: str
+ """
+ split = max(0.0, float(width) - self.length()) / 2
+ leftside = fillchar * int(
+ (max(0.0, math.floor(split))) / float(len(fillchar)))
+ rightside = fillchar * int(
+ (max(0.0, math.ceil(split))) / float(len(fillchar)))
+ return u''.join((leftside, self, rightside))
+
+ def length(self):
+ r"""
+ Return the printable length of string containing sequences.
+
+ Strings containing ``term.left`` or ``\b`` will cause "overstrike",
+ but a length less than 0 is not ever returned. So ``_\b+`` is a
+ length of 1 (displays as ``+``), but ``\b`` alone is simply a
+ length of 0.
+
+ Some characters may consume more than one cell, mainly those CJK
+ Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode
+ as half or full-width characters.
+ """
+ # because control characters may return -1, "clip" their length to 0.
+ clip = functools.partial(max, 0)
+ return sum(clip(wcwidth.wcwidth(w_char))
+ for w_char in self.strip_seqs())
+
+ # we require ur"" for the docstring, but it is not supported by all
+ # python versions.
+ if length.__doc__ is not None:
+ length.__doc__ += (
+ u"""
+ For example:
+
+ >>> from blessed import Terminal
+ >>> from blessed.sequences import Sequence
+ >>> term = Terminal()
+ >>> msg = term.clear + term.red(u'コンニチハ'), term
+ >>> Sequence(msg).length()
+ 10
+
+ .. note:: Although accounted for, strings containing sequences such
+ as ``term.clear`` will not give accurate returns, it is not
+ considered lengthy (a length of 0).
+ """)
+
+ def strip(self, chars=None):
+ """
+ Return string of sequences, leading, and trailing whitespace removed.
+
+ :arg str chars: Remove characters in chars instead of whitespace.
+ :rtype: str
+ """
+ return self.strip_seqs().strip(chars)
+
+ def lstrip(self, chars=None):
+ """
+ Return string of all sequences and leading whitespace removed.
+
+ :arg str chars: Remove characters in chars instead of whitespace.
+ :rtype: str
+ """
+ return self.strip_seqs().lstrip(chars)
+
+ def rstrip(self, chars=None):
+ """
+ Return string of all sequences and trailing whitespace removed.
+
+ :arg str chars: Remove characters in chars instead of whitespace.
+ :rtype: str
+ """
+ return self.strip_seqs().rstrip(chars)
+
+ def strip_seqs(self):
+ """
+ Return ``text`` stripped of only its terminal sequences.
+
+ :rtype: str
+ """
+ gen = iter_parse(self._term, self.padd())
+ return u''.join(text for text, cap in gen if not cap)
+
+ def padd(self):
+ """
+ Return non-destructive horizontal movement as destructive spacing.
+
+ :rtype: str
+ """
+ outp = ''
+ for text, cap in iter_parse(self._term, self):
+ if not cap:
+ outp += text
+ continue
+
+ value = cap.horizontal_distance(text)
+ if value > 0:
+ outp += ' ' * value
+ elif value < 0:
+ outp = outp[:value]
+ else:
+ outp += text
+ return outp
+
+
+def iter_parse(term, text):
+ """
+ Generator yields (text, capability) for characters of ``text``.
+
+ value for ``capability`` may be ``None``, where ``text`` is
+ :class:`str` of length 1. Otherwise, ``text`` is a full
+ matching sequence of given capability.
+ """
+ for match in re.finditer(term._caps_compiled_any, text):
+ name = match.lastgroup
+ value = match.group(name)
+ if name == 'MISMATCH':
+ yield (value, None)
+ else:
+ yield value, term.caps[name]
+
+
+def measure_length(text, term):
+ """
+ .. deprecated:: 1.12.0.
+
+ :rtype: int
+ """
+ try:
+ text, capability = next(iter_parse(term, text))
+ if capability:
+ return len(text)
+ except StopIteration:
+ return 0
+ return 0
diff --git a/blessed/terminal.py b/blessed/terminal.py
new file mode 100644
index 00000000..7e47e8aa
--- /dev/null
+++ b/blessed/terminal.py
@@ -0,0 +1,1296 @@
+# encoding: utf-8
+"""Module containing :class:`Terminal`, the primary API entry point."""
+# std imports
+import io
+import os
+import re
+import sys
+import time
+import codecs
+import locale
+import select
+import struct
+import platform
+import warnings
+import functools
+import contextlib
+import collections
+
+# local
+from .color import COLOR_DISTANCE_ALGORITHMS
+from .keyboard import (_time_left,
+ _read_until,
+ resolve_sequence,
+ get_keyboard_codes,
+ get_leading_prefixes,
+ get_keyboard_sequences)
+from .sequences import Termcap, Sequence, SequenceTextWrapper
+from .colorspace import RGB_256TABLE
+from .formatters import (COLORS,
+ FormattingString,
+ NullCallableString,
+ ParameterizingString,
+ resolve_attribute,
+ resolve_capability)
+from ._capabilities import CAPABILITY_DATABASE, CAPABILITIES_ADDITIVES, CAPABILITIES_RAW_MIXIN
+
+try:
+ InterruptedError
+except NameError:
+ # alias py2 exception to py3
+ # pylint: disable=redefined-builtin
+ InterruptedError = select.error
+
+try:
+ from collections import OrderedDict
+except ImportError:
+ # python 2.6 requires 3rd party library (backport)
+ #
+ # pylint: disable=import-error
+ # Unable to import 'ordereddict'
+ from ordereddict import OrderedDict
+
+
+HAS_TTY = True
+if platform.system() == 'Windows':
+ import jinxed as curses # pylint: disable=import-error
+else:
+ import curses
+
+ try:
+ import termios
+ import fcntl
+ import tty
+ except ImportError:
+ _TTY_METHODS = ('setraw', 'cbreak', 'kbhit', 'height', 'width')
+ _MSG_NOSUPPORT = (
+ "One or more of the modules: 'termios', 'fcntl', and 'tty' "
+ "are not found on your platform '{platform}'. "
+ "The following methods of Terminal are dummy/no-op "
+ "unless a deriving class overrides them: {tty_methods}."
+ .format(platform=platform.system(),
+ tty_methods=', '.join(_TTY_METHODS)))
+ warnings.warn(_MSG_NOSUPPORT)
+ HAS_TTY = False
+
+_CUR_TERM = None # See comments at end of file
+
+
+class Terminal(object):
+ """
+ An abstraction for color, style, positioning, and input in the terminal.
+
+ This keeps the endless calls to ``tigetstr()`` and ``tparm()`` out of your code, acts
+ intelligently when somebody pipes your output to a non-terminal, and abstracts over the
+ complexity of unbuffered keyboard input. It uses the terminfo database to remain portable across
+ terminal types.
+ """
+ # pylint: disable=too-many-instance-attributes,too-many-public-methods
+ # Too many public methods (28/20)
+ # Too many instance attributes (12/7)
+
+ #: Sugary names for commonly-used capabilities
+ _sugar = dict(
+ save='sc',
+ restore='rc',
+ clear_eol='el',
+ clear_bol='el1',
+ clear_eos='ed',
+ enter_fullscreen='smcup',
+ exit_fullscreen='rmcup',
+ move='cup',
+ move_yx='cup',
+ move_x='hpa',
+ move_y='vpa',
+ move_left='cub1',
+ move_right='cuf1',
+ move_up='cuu1',
+ move_down='cud1',
+ hide_cursor='civis',
+ normal_cursor='cnorm',
+ reset_colors='op',
+ normal='sgr0',
+ reverse='rev',
+ italic='sitm',
+ no_italic='ritm',
+ shadow='sshm',
+ no_shadow='rshm',
+ standout='smso',
+ no_standout='rmso',
+ subscript='ssubm',
+ no_subscript='rsubm',
+ superscript='ssupm',
+ no_superscript='rsupm',
+ underline='smul',
+ no_underline='rmul',
+ cursor_report='u6',
+ cursor_request='u7',
+ terminal_answerback='u8',
+ terminal_enquire='u9',
+ )
+
+ def __init__(self, kind=None, stream=None, force_styling=False):
+ """
+ Initialize the terminal.
+
+ :arg str kind: A terminal string as taken by :func:`curses.setupterm`.
+ Defaults to the value of the ``TERM`` environment variable.
+
+ .. note:: Terminals withing a single process must share a common
+ ``kind``. See :obj:`_CUR_TERM`.
+
+ :arg file stream: A file-like object representing the Terminal output.
+ Defaults to the original value of :obj:`sys.__stdout__`, like
+ :func:`curses.initscr` does.
+
+ If ``stream`` is not a tty, empty Unicode strings are returned for
+ all capability values, so things like piping your program output to
+ a pipe or file does not emit terminal sequences.
+
+ :arg bool force_styling: Whether to force the emission of capabilities
+ even if :obj:`sys.__stdout__` does not seem to be connected to a
+ terminal. If you want to force styling to not happen, use
+ ``force_styling=None``.
+
+ This comes in handy if users are trying to pipe your output through
+ something like ``less -r`` or build systems which support decoding
+ of terminal sequences.
+ """
+ # pylint: disable=global-statement,too-many-branches
+ global _CUR_TERM
+ self._keyboard_fd = None
+
+ # Default stream is stdout, keyboard valid as stdin only when
+ # output stream is stdout or stderr and is a tty.
+ if stream is None:
+ stream = sys.__stdout__
+ if stream in (sys.__stdout__, sys.__stderr__):
+ self._keyboard_fd = sys.__stdin__.fileno()
+
+ # we assume our input stream to be line-buffered until either the
+ # cbreak of raw context manager methods are entered with an
+ # attached tty.
+ self._line_buffered = True
+
+ try:
+ stream_fd = (stream.fileno() if hasattr(stream, 'fileno') and
+ callable(stream.fileno) else None)
+ except (io.UnsupportedOperation, ValueError):
+ # The stream is not a file, such as the case of StringIO, or, when it has been
+ # "detached", such as might be the case of stdout in some test scenarios.
+ stream_fd = None
+
+ self._stream = stream
+ self._is_a_tty = stream_fd is not None and os.isatty(stream_fd)
+ self._does_styling = ((self.is_a_tty or force_styling) and
+ force_styling is not None)
+
+ # _keyboard_fd only non-None if both stdin and stdout is a tty.
+ self._keyboard_fd = (self._keyboard_fd
+ if self._keyboard_fd is not None and
+ self.is_a_tty and os.isatty(self._keyboard_fd)
+ else None)
+ self._normal = None # cache normal attr, preventing recursive lookups
+
+ # The descriptor to direct terminal initialization sequences to.
+ self._init_descriptor = (sys.__stdout__.fileno() if stream_fd is None
+ else stream_fd)
+
+ if platform.system() == 'Windows':
+ self._kind = kind or curses.get_term(self._init_descriptor)
+ else:
+ self._kind = kind or os.environ.get('TERM', 'unknown')
+
+ if self.does_styling:
+ # Initialize curses (call setupterm), so things like tigetstr() work.
+ try:
+ curses.setupterm(self._kind, open(os.devnull).fileno())
+ except curses.error as err:
+ warnings.warn('Failed to setupterm(kind={0!r}): {1}'
+ .format(self._kind, err))
+ self._kind = None
+ self._does_styling = False
+ else:
+ if _CUR_TERM is None or self._kind == _CUR_TERM:
+ _CUR_TERM = self._kind
+ else:
+ # termcap 'kind' is immutable in a python process! Once
+ # initialized by setupterm, it is unsupported by the
+ # 'curses' module to change the terminal type again. If you
+ # are a downstream developer and you need this
+ # functionality, consider sub-processing, instead.
+ warnings.warn(
+ 'A terminal of kind "%s" has been requested; due to an'
+ ' internal python curses bug, terminal capabilities'
+ ' for a terminal of kind "%s" will continue to be'
+ ' returned for the remainder of this process.' % (
+ self._kind, _CUR_TERM,))
+
+ self.__init__color_capabilities()
+ self.__init__capabilities()
+ self.__init__keycodes()
+
+ def __init__color_capabilities(self):
+ self._color_distance_algorithm = 'cie2000'
+ if not self.does_styling:
+ self.number_of_colors = 0
+ elif platform.system() == 'Windows' or (
+ os.environ.get('COLORTERM') in ('truecolor', '24bit')
+ ):
+ self.number_of_colors = 1 << 24
+ else:
+ self.number_of_colors = max(0, curses.tigetnum('colors') or -1)
+
+ def __clear_color_capabilities(self):
+ for cached_color_cap in set(dir(self)) & COLORS:
+ delattr(self, cached_color_cap)
+
+ def __init__capabilities(self):
+ # important that we lay these in their ordered direction, so that our
+ # preferred, 'color' over 'set_a_attributes1', for example.
+ self.caps = OrderedDict()
+
+ # some static injected patterns, esp. without named attribute access.
+ for name, (attribute, pattern) in CAPABILITIES_ADDITIVES.items():
+ self.caps[name] = Termcap(name, pattern, attribute)
+
+ for name, (attribute, kwds) in CAPABILITY_DATABASE.items():
+ if self.does_styling:
+ # attempt dynamic lookup
+ cap = getattr(self, attribute)
+ if cap:
+ self.caps[name] = Termcap.build(
+ name, cap, attribute, **kwds)
+ continue
+
+ # fall-back
+ pattern = CAPABILITIES_RAW_MIXIN.get(name)
+ if pattern:
+ self.caps[name] = Termcap(name, pattern, attribute)
+
+ # make a compiled named regular expression table
+ self.caps_compiled = re.compile(
+ '|'.join(cap.pattern for name, cap in self.caps.items()))
+
+ # for tokenizer, the '.lastgroup' is the primary lookup key for
+ # 'self.caps', unless 'MISMATCH'; then it is an unmatched character.
+ self._caps_compiled_any = re.compile('|'.join(
+ cap.named_pattern for name, cap in self.caps.items()
+ ) + '|(?P.)')
+ self._caps_unnamed_any = re.compile('|'.join(
+ '({0})'.format(cap.pattern) for name, cap in self.caps.items()
+ ) + '|(.)')
+
+ def __init__keycodes(self):
+ # Initialize keyboard data determined by capability.
+ # Build database of int code <=> KEY_NAME.
+ self._keycodes = get_keyboard_codes()
+
+ # Store attributes as: self.KEY_NAME = code.
+ for key_code, key_name in self._keycodes.items():
+ setattr(self, key_name, key_code)
+
+ # Build database of sequence <=> KEY_NAME.
+ self._keymap = get_keyboard_sequences(self)
+
+ # build set of prefixes of sequences
+ self._keymap_prefixes = get_leading_prefixes(self._keymap)
+
+ # keyboard stream buffer
+ self._keyboard_buf = collections.deque()
+
+ if self._keyboard_fd is not None:
+ # set input encoding and initialize incremental decoder
+ if platform.system() == 'Windows' and sys.version_info[0] < 3:
+ # Default for setlocale() has side effects for PY2 on Windows
+ pass
+ else:
+ locale.setlocale(locale.LC_ALL, '')
+ self._encoding = locale.getpreferredencoding() or 'ascii'
+
+ try:
+ self._keyboard_decoder = codecs.getincrementaldecoder(
+ self._encoding)()
+
+ except LookupError as err:
+ # encoding is illegal or unsupported, use 'ascii'
+ warnings.warn('LookupError: {0}, fallback to ASCII for '
+ 'keyboard.'.format(err))
+ self._encoding = 'ascii'
+ self._keyboard_decoder = codecs.getincrementaldecoder(
+ self._encoding)()
+
+ def __getattr__(self, attr):
+ r"""
+ Return a terminal capability as Unicode string.
+
+ For example, ``term.bold`` is a unicode string that may be prepended
+ to text to set the video attribute for bold, which should also be
+ terminated with the pairing :attr:`normal`. This capability
+ returns a callable, so you can use ``term.bold("hi")`` which
+ results in the joining of ``(term.bold, "hi", term.normal)``.
+
+ Compound formatters may also be used. For example::
+
+ >>> term.bold_blink_red_on_green("merry x-mas!")
+
+ For a parameterized capability such as ``move`` (or ``cup``), pass the
+ parameters as positional arguments::
+
+ >>> term.move(line, column)
+
+ See the manual page `terminfo(5)
+ `_ for a
+ complete list of capabilities and their arguments.
+ """
+ if not self.does_styling:
+ return NullCallableString()
+ # Fetch the missing 'attribute' into some kind of curses-resolved
+ # capability, and cache by attaching to this Terminal class instance.
+ #
+ # Note that this will prevent future calls to __getattr__(), but
+ # that's precisely the idea of the cache!
+ val = resolve_attribute(self, attr)
+ setattr(self, attr, val)
+ return val
+
+ @property
+ def kind(self):
+ """
+ Read-only property: Terminal kind determined on class initialization.
+
+ :rtype: str
+ """
+ return self._kind
+
+ @property
+ def does_styling(self):
+ """
+ Read-only property: Whether this class instance may emit sequences.
+
+ :rtype: bool
+ """
+ return self._does_styling
+
+ @property
+ def is_a_tty(self):
+ """
+ Read-only property: Whether :attr:`~.stream` is a terminal.
+
+ :rtype: bool
+ """
+ return self._is_a_tty
+
+ @property
+ def height(self):
+ """
+ Read-only property: Height of the terminal (in number of lines).
+
+ :rtype: int
+ """
+ return self._height_and_width().ws_row
+
+ @property
+ def width(self):
+ """
+ Read-only property: Width of the terminal (in number of columns).
+
+ :rtype: int
+ """
+ return self._height_and_width().ws_col
+
+ @staticmethod
+ def _winsize(fd):
+ """
+ Return named tuple describing size of the terminal by ``fd``.
+
+ If the given platform does not have modules :mod:`termios`,
+ :mod:`fcntl`, or :mod:`tty`, window size of 80 columns by 25
+ rows is always returned.
+
+ :arg int fd: file descriptor queries for its window size.
+ :raises IOError: the file descriptor ``fd`` is not a terminal.
+ :rtype: WINSZ
+
+ WINSZ is a :class:`collections.namedtuple` instance, whose structure
+ directly maps to the return value of the :const:`termios.TIOCGWINSZ`
+ ioctl return value. The return parameters are:
+
+ - ``ws_row``: width of terminal by its number of character cells.
+ - ``ws_col``: height of terminal by its number of character cells.
+ - ``ws_xpixel``: width of terminal by pixels (not accurate).
+ - ``ws_ypixel``: height of terminal by pixels (not accurate).
+ """
+ if HAS_TTY:
+ data = fcntl.ioctl(fd, termios.TIOCGWINSZ, WINSZ._BUF)
+ return WINSZ(*struct.unpack(WINSZ._FMT, data))
+ return WINSZ(ws_row=25, ws_col=80, ws_xpixel=0, ws_ypixel=0)
+
+ def _height_and_width(self):
+ """
+ Return a tuple of (terminal height, terminal width).
+
+ If :attr:`stream` or :obj:`sys.__stdout__` is not a tty or does not
+ support :func:`fcntl.ioctl` of :const:`termios.TIOCGWINSZ`, a window
+ size of 80 columns by 25 rows is returned for any values not
+ represented by environment variables ``LINES`` and ``COLUMNS``, which
+ is the default text mode of IBM PC compatibles.
+
+ :rtype: WINSZ
+
+ WINSZ is a :class:`collections.namedtuple` instance, whose structure
+ directly maps to the return value of the :const:`termios.TIOCGWINSZ`
+ ioctl return value. The return parameters are:
+
+ - ``ws_row``: width of terminal by its number of character cells.
+ - ``ws_col``: height of terminal by its number of character cells.
+ - ``ws_xpixel``: width of terminal by pixels (not accurate).
+ - ``ws_ypixel``: height of terminal by pixels (not accurate).
+ """
+ for fd in (self._init_descriptor, sys.__stdout__):
+ try:
+ if fd is not None:
+ return self._winsize(fd)
+ except (IOError, OSError, ValueError, TypeError):
+ pass
+
+ return WINSZ(ws_row=int(os.getenv('LINES', '25')),
+ ws_col=int(os.getenv('COLUMNS', '80')),
+ ws_xpixel=None,
+ ws_ypixel=None)
+
+ @contextlib.contextmanager
+ def location(self, x=None, y=None):
+ """
+ Context manager for temporarily moving the cursor.
+
+ Move the cursor to a certain position on entry, let you print stuff
+ there, then return the cursor to its original position::
+
+ term = Terminal()
+ with term.location(2, 5):
+ for x in xrange(10):
+ print('I can do it %i times!' % x)
+ print('We're back to the original location.')
+
+ Specify ``x`` to move to a certain column, ``y`` to move to a certain
+ row, both, or neither. If you specify neither, only the saving and
+ restoration of cursor position will happen. This can be useful if you
+ simply want to restore your place after doing some manual cursor
+ movement.
+
+ .. note:: The store- and restore-cursor capabilities used internally
+ provide no stack. This means that :meth:`location` calls cannot be
+ nested: only one should be entered at a time.
+ """
+ # pylint: disable=invalid-name
+ # Invalid argument name "x"
+
+ # Save position and move to the requested column, row, or both:
+ self.stream.write(self.save)
+ if x is not None and y is not None:
+ self.stream.write(self.move(y, x))
+ elif x is not None:
+ self.stream.write(self.move_x(x))
+ elif y is not None:
+ self.stream.write(self.move_y(y))
+ try:
+ self.stream.flush()
+ yield
+ finally:
+ # Restore original cursor position:
+ self.stream.write(self.restore)
+ self.stream.flush()
+
+ def get_location(self, timeout=None):
+ r"""
+ Return tuple (row, column) of cursor position.
+
+ :arg float timeout: Return after time elapsed in seconds with value
+ ``(-1, -1)`` indicating that the remote end did not respond.
+ :rtype: tuple
+ :returns: cursor position as tuple in form of (row, column).
+
+ The location of the cursor is determined by emitting the ``u7``
+ terminal capability, or VT100 `Query Cursor Position
+ `_ when such
+ capability is undefined, which elicits a response from a reply string
+ described by capability ``u6``, or again VT100's definition of
+ ``\x1b[%i%d;%dR`` when undefined.
+
+ The ``(row, col)`` return value matches the parameter order of the
+ ``move`` capability, so that the following sequence should cause the
+ cursor to not move at all::
+
+ >>> term = Terminal()
+ >>> term.move(*term.get_location()))
+
+ .. warning:: You might first test that a terminal is capable of
+ informing you of its location, while using a timeout, before
+ later calling. When a timeout is specified, always ensure the
+ return value is conditionally checked for ``(-1, -1)``.
+ """
+ # Local lines attached by termios and remote login protocols such as
+ # ssh and telnet both provide a means to determine the window
+ # dimensions of a connected client, but **no means to determine the
+ # location of the cursor**.
+ #
+ # from http://invisible-island.net/ncurses/terminfo.src.html,
+ #
+ # > The System V Release 4 and XPG4 terminfo format defines ten string
+ # > capabilities for use by applications, .... In this file,
+ # > we use certain of these capabilities to describe functions which
+ # > are not covered by terminfo. The mapping is as follows:
+ # >
+ # > u9 terminal enquire string (equiv. to ANSI/ECMA-48 DA)
+ # > u8 terminal answerback description
+ # > u7 cursor position request (equiv. to VT100/ANSI/ECMA-48 DSR 6)
+ # > u6 cursor position report (equiv. to ANSI/ECMA-48 CPR)
+ query_str = self.u7 or u'\x1b[6n'
+
+ # determine response format as a regular expression
+ response_re = self.caps['cursor_report'].re_compiled
+
+ # Avoid changing user's desired raw or cbreak mode if already entered,
+ # by entering cbreak mode ourselves. This is necessary to receive user
+ # input without awaiting a human to press the return key. This mode
+ # also disables echo, which we should also hide, as our input is an
+ # sequence that is not meaningful for display as an output sequence.
+
+ ctx = None
+ try:
+ if self._line_buffered:
+ ctx = self.cbreak()
+ ctx.__enter__()
+
+ # emit the 'query cursor position' sequence,
+ self.stream.write(query_str)
+ self.stream.flush()
+
+ # expect a response,
+ match, data = _read_until(term=self,
+ pattern=response_re,
+ timeout=timeout)
+
+ # ensure response sequence is excluded from subsequent input,
+ if match:
+ data = (data[:match.start()] + data[match.end():])
+
+ # re-buffer keyboard data, if any
+ self.ungetch(data)
+
+ if match:
+ # return matching sequence response, the cursor location.
+ row, col = (int(val) for val in match.groups())
+
+ # Per https://invisible-island.net/ncurses/terminfo.src.html
+ # The cursor position report () string must contain two
+ # scanf(3)-style %d format elements. The first of these must
+ # correspond to the Y coordinate and the second to the %d.
+ # If the string contains the sequence %i, it is taken as an
+ # instruction to decrement each value after reading it (this is
+ # the inverse sense from the cup string).
+ if '%i' in self.cursor_report:
+ row -= 1
+ col -= 1
+ return row, col
+
+ finally:
+ if ctx is not None:
+ ctx.__exit__(None, None, None)
+
+ # We chose to return an illegal value rather than an exception,
+ # favoring that users author function filters, such as max(0, y),
+ # rather than crowbarring such logic into an exception handler.
+ return -1, -1
+
+ @contextlib.contextmanager
+ def fullscreen(self):
+ """
+ Context manager that switches to secondary screen, restoring on exit.
+
+ Under the hood, this switches between the primary screen buffer and
+ the secondary one. The primary one is saved on entry and restored on
+ exit. Likewise, the secondary contents are also stable and are
+ faithfully restored on the next entry::
+
+ with term.fullscreen():
+ main()
+
+ .. note:: There is only one primary and one secondary screen buffer.
+ :meth:`fullscreen` calls cannot be nested, only one should be
+ entered at a time.
+ """
+ self.stream.write(self.enter_fullscreen)
+ self.stream.flush()
+ try:
+ yield
+ finally:
+ self.stream.write(self.exit_fullscreen)
+ self.stream.flush()
+
+ @contextlib.contextmanager
+ def hidden_cursor(self):
+ """
+ Context manager that hides the cursor, setting visibility on exit.
+
+ with term.hidden_cursor():
+ main()
+
+ .. note:: :meth:`hidden_cursor` calls cannot be nested: only one
+ should be entered at a time.
+ """
+ self.stream.write(self.hide_cursor)
+ self.stream.flush()
+ try:
+ yield
+ finally:
+ self.stream.write(self.normal_cursor)
+ self.stream.flush()
+
+ @property
+ def color(self):
+ """
+ A callable string that sets the foreground color.
+
+ :arg int num: The foreground color index. This should be within the
+ bounds of :attr:`~.number_of_colors`.
+ :rtype: ParameterizingString
+
+ The capability is unparameterized until called and passed a number,
+ 0-15, at which point it returns another string which represents a
+ specific color change. This second string can further be called to
+ color a piece of text and set everything back to normal afterward.
+ """
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._foreground_color,
+ self.normal, 'color')
+
+ def move_xy(self, x, y):
+ """
+ A callable string that moves the cursor to the given ``(x, y)`` screen coordinates.
+
+ :arg int x: horizontal position.
+ :arg int y: vertical position.
+ :rtype: ParameterizingString
+ """
+ # this is just a convenience alias to the built-in, but hidden 'move'
+ # attribute -- we encourage folks to use only (x, y) positional
+ # arguments, or, if they must use (y, x), then use the 'move_yx'
+ # alias.
+ return self.move(y, x)
+
+ def move_yx(self, y, x):
+ """
+ A callable string that moves the cursor to the given ``(y, x)`` screen coordinates.
+
+ :arg int x: horizontal position.
+ :arg int y: vertical position.
+ :rtype: ParameterizingString
+ """
+ return self.move(y, x)
+
+ def color_rgb(self, red, green, blue):
+ if self.number_of_colors == 1 << 24:
+ # "truecolor" 24-bit
+ fmt_attr = u'\x1b[38;2;{0};{1};{2}m'.format(red, green, blue)
+ return FormattingString(fmt_attr, self.normal)
+
+ # color by approximation to 256 or 16-color terminals
+ color_idx = self.rgb_downconvert(red, green, blue)
+ return FormattingString(self._foreground_color(color_idx), self.normal)
+
+ @property
+ def on_color(self):
+ """
+ A callable capability that sets the background color.
+
+ :arg int num: The background color index.
+ :rtype: ParameterizingString
+ """
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._background_color,
+ self.normal, 'on_color')
+
+ def on_color_rgb(self, red, green, blue):
+ if self.number_of_colors == 1 << 24:
+ fmt_attr = u'\x1b[48;2;{0};{1};{2}m'.format(red, green, blue)
+ return FormattingString(fmt_attr, self.normal)
+
+ color_idx = self.rgb_downconvert(red, green, blue)
+ return FormattingString(self._background_color(color_idx), self.normal)
+
+ def rgb_downconvert(self, red, green, blue):
+ """
+ Translate an RGB color to a color code of the terminal's color depth.
+
+ :arg int red: RGB value of Red (0-255).
+ :arg int green: RGB value of Green (0-255).
+ :arg int blue: RGB value of Blue (0-255).
+ :rtype: int
+ """
+ # Though pre-computing all 1 << 24 options is memory-intensive, a pre-computed
+ # "k-d tree" of 256 (x,y,z) vectors of a colorspace in 3 dimensions, such as a
+ # cone of HSV, or simply 255x255x255 RGB square, any given rgb value is just a
+ # nearest-neighbor search of 256 points, which k-d should be much faster by
+ # sub-dividing / culling search points, rather than our "search all 256 points
+ # always" approach.
+ fn_distance = COLOR_DISTANCE_ALGORITHMS[self.color_distance_algorithm]
+ color_idx = 7
+ shortest_distance = None
+ for cmp_depth, cmp_rgb in enumerate(RGB_256TABLE):
+ cmp_distance = fn_distance(cmp_rgb, (red, green, blue))
+ if shortest_distance is None or cmp_distance < shortest_distance:
+ shortest_distance = cmp_distance
+ color_idx = cmp_depth
+ if cmp_depth >= self.number_of_colors:
+ break
+ return color_idx
+
+ @property
+ def normal(self):
+ """
+ A capability that resets all video attributes.
+
+ :rtype: str
+
+ ``normal`` is an alias for ``sgr0`` or ``exit_attribute_mode``. Any
+ styling attributes previously applied, such as foreground or
+ background colors, reverse video, or bold are reset to defaults.
+ """
+ if self._normal:
+ return self._normal
+ self._normal = resolve_capability(self, 'normal')
+ return self._normal
+
+ @property
+ def stream(self):
+ """
+ Read-only property: stream the terminal outputs to.
+
+ This is a convenience attribute. It is used internally for implied
+ writes performed by context managers :meth:`~.hidden_cursor`,
+ :meth:`~.fullscreen`, :meth:`~.location`, and :meth:`~.keypad`.
+ """
+ return self._stream
+
+ @property
+ def number_of_colors(self):
+ """
+ Number of colors supported by terminal.
+
+ Common return values are 0, 8, 16, 256, or 1 << 24.
+
+ This may be used to test whether the terminal supports colors,
+ and at what depth, if that's a concern.
+ """
+ return self._number_of_colors
+
+ @number_of_colors.setter
+ def number_of_colors(self, value):
+ assert value in (0, 4, 8, 16, 256, 1 << 24)
+ self._number_of_colors = value
+ self.__clear_color_capabilities()
+
+ @property
+ def color_distance_algorithm(self):
+ """
+ Color distance algorithm used by :meth:`rgb_downconvert`.
+
+ The slowest, but most accurate, 'cie2000', is default. Other available options are 'rgb',
+ 'rgb-weighted', 'cie76', and 'cie94'.
+ """
+ return self._color_distance_algorithm
+
+ @color_distance_algorithm.setter
+ def color_distance_algorithm(self, value):
+ assert value in COLOR_DISTANCE_ALGORITHMS
+ self._color_distance_algorithm = value
+ self.__clear_color_capabilities()
+
+ @property
+ def _foreground_color(self):
+ """
+ Convenience capability to support :attr:`~.on_color`.
+
+ Prefers returning sequence for capability ``setaf``, "Set foreground color to #1, using ANSI
+ escape". If the given terminal does not support such sequence, fallback to returning
+ attribute ``setf``, "Set foreground color #1".
+ """
+ return self.setaf or self.setf
+
+ @property
+ def _background_color(self):
+ """
+ Convenience capability to support :attr:`~.on_color`.
+
+ Prefers returning sequence for capability ``setab``, "Set background color to #1, using ANSI
+ escape". If the given terminal does not support such sequence, fallback to returning
+ attribute ``setb``, "Set background color #1".
+ """
+ return self.setab or self.setb
+
+ def ljust(self, text, width=None, fillchar=u' '):
+ """
+ Left-align ``text``, which may contain terminal sequences.
+
+ :arg str text: String to be aligned
+ :arg int width: Total width to fill with aligned text. If
+ unspecified, the whole width of the terminal is filled.
+ :arg str fillchar: String for padding the right of ``text``
+ :rtype: str
+ """
+ # Left justification is different from left alignment, but we continue
+ # the vocabulary error of the str method for polymorphism.
+ if width is None:
+ width = self.width
+ return Sequence(text, self).ljust(width, fillchar)
+
+ def rjust(self, text, width=None, fillchar=u' '):
+ """
+ Right-align ``text``, which may contain terminal sequences.
+
+ :arg str text: String to be aligned
+ :arg int width: Total width to fill with aligned text. If
+ unspecified, the whole width of the terminal is used.
+ :arg str fillchar: String for padding the left of ``text``
+ :rtype: str
+ """
+ if width is None:
+ width = self.width
+ return Sequence(text, self).rjust(width, fillchar)
+
+ def center(self, text, width=None, fillchar=u' '):
+ """
+ Center ``text``, which may contain terminal sequences.
+
+ :arg str text: String to be centered
+ :arg int width: Total width in which to center text. If
+ unspecified, the whole width of the terminal is used.
+ :arg str fillchar: String for padding the left and right of ``text``
+ :rtype: str
+ """
+ if width is None:
+ width = self.width
+ return Sequence(text, self).center(width, fillchar)
+
+ def length(self, text):
+ u"""
+ Return printable length of a string containing sequences.
+
+ :arg str text: String to measure. May contain terminal sequences.
+ :rtype: int
+ :returns: The number of terminal character cells the string will occupy
+ when printed
+
+ Wide characters that consume 2 character cells are supported:
+
+ >>> term = Terminal()
+ >>> term.length(term.clear + term.red(u'コンニチハ'))
+ 10
+
+ .. note:: Sequences such as 'clear', which is considered as a
+ "movement sequence" because it would move the cursor to
+ (y, x)(0, 0), are evaluated as a printable length of
+ *0*.
+ """
+ return Sequence(text, self).length()
+
+ def strip(self, text, chars=None):
+ r"""
+ Return ``text`` without sequences and leading or trailing whitespace.
+
+ :rtype: str
+
+ >>> term.strip(u' \x1b[0;3m xyz ')
+ u'xyz'
+ """
+ return Sequence(text, self).strip(chars)
+
+ def rstrip(self, text, chars=None):
+ r"""
+ Return ``text`` without terminal sequences or trailing whitespace.
+
+ :rtype: str
+
+ >>> term.rstrip(u' \x1b[0;3m xyz ')
+ u' xyz'
+ """
+ return Sequence(text, self).rstrip(chars)
+
+ def lstrip(self, text, chars=None):
+ r"""
+ Return ``text`` without terminal sequences or leading whitespace.
+
+ :rtype: str
+
+ >>> term.lstrip(u' \x1b[0;3m xyz ')
+ u'xyz '
+ """
+ return Sequence(text, self).lstrip(chars)
+
+ def strip_seqs(self, text):
+ r"""
+ Return ``text`` stripped of only its terminal sequences.
+
+ :rtype: str
+
+ >>> term.strip_seqs(u'\x1b[0;3mxyz')
+ u'xyz'
+ >>> term.strip_seqs(term.cuf(5) + term.red(u'test'))
+ u' test'
+
+ .. note:: Non-destructive sequences that adjust horizontal distance
+ (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive
+ space or erasing.
+ """
+ return Sequence(text, self).strip_seqs()
+
+ def split_seqs(self, text, **kwds):
+ r"""
+ Return ``text`` split by individual character elements and sequences.
+
+ :arg kwds: remaining keyword arguments for :func:`re.split`.
+ :rtype: list[str]
+
+ >>> term.split_seqs(term.underline(u'xyz'))
+ ['\x1b[4m', 'x', 'y', 'z', '\x1b(B', '\x1b[m']
+ """
+ pattern = self._caps_unnamed_any
+ return list(filter(None, re.split(pattern, text, **kwds)))
+
+ def wrap(self, text, width=None, **kwargs):
+ """
+ Text-wrap a string, returning a list of wrapped lines.
+
+ :arg str text: Unlike :func:`textwrap.wrap`, ``text`` may contain
+ terminal sequences, such as colors, bold, or underline. By
+ default, tabs in ``text`` are expanded by
+ :func:`string.expandtabs`.
+ :arg int width: Unlike :func:`textwrap.wrap`, ``width`` will
+ default to the width of the attached terminal.
+ :rtype: list
+
+ See :class:`textwrap.TextWrapper` for keyword arguments that can
+ customize wrapping behaviour.
+ """
+ width = self.width if width is None else width
+ lines = []
+ for line in text.splitlines():
+ lines.extend(
+ (_linewrap for _linewrap in SequenceTextWrapper(
+ width=width, term=self, **kwargs).wrap(line))
+ if line.strip() else (u'',))
+
+ return lines
+
+ def getch(self):
+ """
+ Read, decode, and return the next byte from the keyboard stream.
+
+ :rtype: unicode
+ :returns: a single unicode character, or ``u''`` if a multi-byte
+ sequence has not yet been fully received.
+
+ This method name and behavior mimics curses ``getch(void)``, and
+ it supports :meth:`inkey`, reading only one byte from
+ the keyboard string at a time. This method should always return
+ without blocking if called after :meth:`kbhit` has returned True.
+
+ Implementors of alternate input stream methods should override
+ this method.
+ """
+ assert self._keyboard_fd is not None
+ byte = os.read(self._keyboard_fd, 1)
+ return self._keyboard_decoder.decode(byte, final=False)
+
+ def ungetch(self, text):
+ """
+ Buffer input data to be discovered by next call to :meth:`~.inkey`.
+
+ :arg str ucs: String to be buffered as keyboard input.
+ """
+ self._keyboard_buf.extendleft(text)
+
+ def kbhit(self, timeout=None):
+ """
+ Return whether a keypress has been detected on the keyboard.
+
+ This method is used by :meth:`inkey` to determine if a byte may
+ be read using :meth:`getch` without blocking. The standard
+ implementation simply uses the :func:`select.select` call on stdin.
+
+ :arg float timeout: When ``timeout`` is 0, this call is
+ non-blocking, otherwise blocking indefinitely until keypress
+ is detected when None (default). When ``timeout`` is a
+ positive number, returns after ``timeout`` seconds have
+ elapsed (float).
+ :rtype: bool
+ :returns: True if a keypress is awaiting to be read on the keyboard
+ attached to this terminal. When input is not a terminal, False is
+ always returned.
+ """
+ stime = time.time()
+ ready_r = [None, ]
+ check_r = [self._keyboard_fd] if self._keyboard_fd is not None else []
+
+ while HAS_TTY and True:
+ try:
+ ready_r, _, _ = select.select(check_r, [], [], timeout)
+ except InterruptedError:
+ # Beginning with python3.5, IntrruptError is no longer thrown
+ # https://www.python.org/dev/peps/pep-0475/
+ #
+ # For previous versions of python, we take special care to
+ # retry select on InterruptedError exception, namely to handle
+ # a custom SIGWINCH handler. When installed, it would cause
+ # select() to be interrupted with errno 4 (EAGAIN).
+ #
+ # Just as in python3.5, it is ignored, and a new timeout value
+ # is derived from the previous unless timeout becomes negative.
+ # because the signal handler has blocked beyond timeout, then
+ # False is returned. Otherwise, when timeout is None, we
+ # continue to block indefinitely (default).
+ if timeout is not None:
+ # subtract time already elapsed,
+ timeout -= time.time() - stime
+ if timeout > 0:
+ continue
+ # no time remains after handling exception (rare)
+ ready_r = [] # pragma: no cover
+ break # pragma: no cover
+ else:
+ break
+
+ return False if self._keyboard_fd is None else check_r == ready_r
+
+ @contextlib.contextmanager
+ def cbreak(self):
+ """
+ Allow each keystroke to be read immediately after it is pressed.
+
+ This is a context manager for :func:`tty.setcbreak`.
+
+ This context manager activates 'rare' mode, the opposite of 'cooked'
+ mode: On entry, :func:`tty.setcbreak` mode is activated disabling
+ line-buffering of keyboard input and turning off automatic echo of
+ input as output.
+
+ .. note:: You must explicitly print any user input you would like
+ displayed. If you provide any kind of editing, you must handle
+ backspace and other line-editing control functions in this mode
+ as well!
+
+ **Normally**, characters received from the keyboard cannot be read
+ by Python until the *Return* key is pressed. Also known as *cooked* or
+ *canonical input* mode, it allows the tty driver to provide
+ line-editing before shuttling the input to your program and is the
+ (implicit) default terminal mode set by most unix shells before
+ executing programs.
+
+ Technically, this context manager sets the :mod:`termios` attributes
+ of the terminal attached to :obj:`sys.__stdin__`.
+
+ .. note:: :func:`tty.setcbreak` sets ``VMIN = 1`` and ``VTIME = 0``,
+ see http://www.unixwiz.net/techtips/termios-vmin-vtime.html
+ """
+ if HAS_TTY and self._keyboard_fd is not None:
+ # Save current terminal mode:
+ save_mode = termios.tcgetattr(self._keyboard_fd)
+ save_line_buffered = self._line_buffered
+ tty.setcbreak(self._keyboard_fd, termios.TCSANOW)
+ try:
+ self._line_buffered = False
+ yield
+ finally:
+ # Restore prior mode:
+ termios.tcsetattr(self._keyboard_fd,
+ termios.TCSAFLUSH,
+ save_mode)
+ self._line_buffered = save_line_buffered
+ else:
+ yield
+
+ @contextlib.contextmanager
+ def raw(self):
+ r"""
+ A context manager for :func:`tty.setraw`.
+
+ Although both :meth:`break` and :meth:`raw` modes allow each keystroke
+ to be read immediately after it is pressed, Raw mode disables
+ processing of input and output.
+
+ In cbreak mode, special input characters such as ``^C`` or ``^S`` are
+ interpreted by the terminal driver and excluded from the stdin stream.
+ In raw mode these values are receive by the :meth:`inkey` method.
+
+ Because output processing is not done, the newline ``'\n'`` is not
+ enough, you must also print carriage return to ensure that the cursor
+ is returned to the first column::
+
+ with term.raw():
+ print("printing in raw mode", end="\r\n")
+ """
+ if HAS_TTY and self._keyboard_fd is not None:
+ # Save current terminal mode:
+ save_mode = termios.tcgetattr(self._keyboard_fd)
+ save_line_buffered = self._line_buffered
+ tty.setraw(self._keyboard_fd, termios.TCSANOW)
+ try:
+ self._line_buffered = False
+ yield
+ finally:
+ # Restore prior mode:
+ termios.tcsetattr(self._keyboard_fd,
+ termios.TCSAFLUSH,
+ save_mode)
+ self._line_buffered = save_line_buffered
+ else:
+ yield
+
+ @contextlib.contextmanager
+ def keypad(self):
+ r"""
+ Context manager that enables directional keypad input.
+
+ On entrying, this puts the terminal into "keyboard_transmit" mode by
+ emitting the keypad_xmit (smkx) capability. On exit, it emits
+ keypad_local (rmkx).
+
+ On an IBM-PC keyboard with numeric keypad of terminal-type *xterm*,
+ with numlock off, the lower-left diagonal key transmits sequence
+ ``\\x1b[F``, translated to :class:`~.Terminal` attribute
+ ``KEY_END``.
+
+ However, upon entering :meth:`keypad`, ``\\x1b[OF`` is transmitted,
+ translating to ``KEY_LL`` (lower-left key), allowing you to determine
+ diagonal direction keys.
+ """
+ try:
+ self.stream.write(self.smkx)
+ self.stream.flush()
+ yield
+ finally:
+ self.stream.write(self.rmkx)
+ self.stream.flush()
+
+ def inkey(self, timeout=None, esc_delay=0.35):
+ """
+ Read and return the next keyboard event within given timeout.
+
+ Generally, this should be used inside the :meth:`raw` context manager.
+
+ :arg float timeout: Number of seconds to wait for a keystroke before
+ returning. When ``None`` (default), this method may block
+ indefinitely.
+ :arg float esc_delay: To distinguish between the keystroke of
+ ``KEY_ESCAPE``, and sequences beginning with escape, the parameter
+ ``esc_delay`` specifies the amount of time after receiving escape
+ (``chr(27)``) to seek for the completion of an application key
+ before returning a :class:`~.Keystroke` instance for
+ ``KEY_ESCAPE``.
+ :rtype: :class:`~.Keystroke`.
+ :returns: :class:`~.Keystroke`, which may be empty (``u''``) if
+ ``timeout`` is specified and keystroke is not received.
+
+ .. note:: When used without the context manager :meth:`cbreak`, or
+ :meth:`raw`, :obj:`sys.__stdin__` remains line-buffered, and this
+ function will block until the return key is pressed!
+ """
+ resolve = functools.partial(resolve_sequence,
+ mapper=self._keymap,
+ codes=self._keycodes)
+
+ stime = time.time()
+
+ # re-buffer previously received keystrokes,
+ ucs = u''
+ while self._keyboard_buf:
+ ucs += self._keyboard_buf.pop()
+
+ # receive all immediately available bytes
+ while self.kbhit(timeout=0):
+ ucs += self.getch()
+
+ # decode keystroke, if any
+ ks = resolve(text=ucs)
+
+ # so long as the most immediately received or buffered keystroke is
+ # incomplete, (which may be a multibyte encoding), block until until
+ # one is received.
+ while not ks and self.kbhit(timeout=_time_left(stime, timeout)):
+ ucs += self.getch()
+ ks = resolve(text=ucs)
+
+ # handle escape key (KEY_ESCAPE) vs. escape sequence (like those
+ # that begin with \x1b[ or \x1bO) up to esc_delay when
+ # received. This is not optimal, but causes least delay when
+ # "meta sends escape" is used, or when an unsupported sequence is
+ # sent.
+ #
+ # The statement, "ucs in self._keymap_prefixes" has an effect on
+ # keystrokes such as Alt + Z ("\x1b[z" with metaSendsEscape): because
+ # no known input sequences begin with such phrasing to allow it to be
+ # returned more quickly than esc_delay otherwise blocks for.
+ if ks.code == self.KEY_ESCAPE:
+ esctime = time.time()
+ while (ks.code == self.KEY_ESCAPE and
+ ucs in self._keymap_prefixes and
+ self.kbhit(timeout=_time_left(esctime, esc_delay))):
+ ucs += self.getch()
+ ks = resolve(text=ucs)
+
+ # buffer any remaining text received
+ self.ungetch(ucs[len(ks):])
+ return ks
+
+
+class WINSZ(collections.namedtuple('WINSZ', (
+ 'ws_row', 'ws_col', 'ws_xpixel', 'ws_ypixel'))):
+ """
+ Structure represents return value of :const:`termios.TIOCGWINSZ`.
+
+ .. py:attribute:: ws_row
+
+ rows, in characters
+
+ .. py:attribute:: ws_col
+
+ columns, in characters
+
+ .. py:attribute:: ws_xpixel
+
+ horizontal size, pixels
+
+ .. py:attribute:: ws_ypixel
+
+ vertical size, pixels
+ """
+ #: format of termios structure
+ _FMT = 'hhhh'
+ #: buffer of termios structure appropriate for ioctl argument
+ _BUF = '\x00' * struct.calcsize(_FMT)
+
+
+#: _CUR_TERM = None
+#: From libcurses/doc/ncurses-intro.html (ESR, Thomas Dickey, et. al)::
+#:
+#: "After the call to setupterm(), the global variable cur_term is set to
+#: point to the current structure of terminal capabilities. By calling
+#: setupterm() for each terminal, and saving and restoring cur_term, it
+#: is possible for a program to use two or more terminals at once."
+#:
+#: However, if you study Python's ``./Modules/_cursesmodule.c``, you'll find::
+#:
+#: if (!initialised_setupterm && setupterm(termstr,fd,&err) == ERR) {
+#:
+#: Python - perhaps wrongly - will not allow for re-initialisation of new
+#: terminals through :func:`curses.setupterm`, so the value of cur_term cannot
+#: be changed once set: subsequent calls to :func:`curses.setupterm` have no
+#: effect.
+#:
+#: Therefore, the :attr:`Terminal.kind` of each :class:`Terminal` is
+#: essentially a singleton. This global variable reflects that, and a warning
+#: is emitted if somebody expects otherwise.
diff --git a/blessed/win_terminal.py b/blessed/win_terminal.py
new file mode 100644
index 00000000..68cdabe2
--- /dev/null
+++ b/blessed/win_terminal.py
@@ -0,0 +1,161 @@
+# encoding: utf-8
+"""Module containing Windows version of :class:`Terminal`."""
+
+from __future__ import absolute_import
+
+# std imports
+import time
+import msvcrt # pylint: disable=import-error
+import contextlib
+
+# 3rd party
+import jinxed.win32 as win32 # pylint: disable=import-error
+
+# local
+from .terminal import WINSZ
+from .terminal import Terminal as _Terminal
+
+
+class Terminal(_Terminal):
+ """Windows subclass of :class:`Terminal`."""
+
+ def getch(self):
+ r"""
+ Read, decode, and return the next byte from the keyboard stream.
+
+ :rtype: unicode
+ :returns: a single unicode character, or ``u''`` if a multi-byte
+ sequence has not yet been fully received.
+
+ For versions of Windows 10.0.10586 and later, the console is expected
+ to be in ENABLE_VIRTUAL_TERMINAL_INPUT mode and the default method is
+ called.
+
+ For older versions of Windows, msvcrt.getwch() is used. If the received
+ character is ``\x00`` or ``\xe0``, the next character is
+ automatically retrieved.
+ """
+ if win32.VTMODE_SUPPORTED:
+ return super(Terminal, self).getch()
+
+ rtn = msvcrt.getwch()
+ if rtn in ('\x00', '\xe0'):
+ rtn += msvcrt.getwch()
+ return rtn
+
+ def kbhit(self, timeout=None, **_kwargs):
+ """
+ Return whether a keypress has been detected on the keyboard.
+
+ This method is used by :meth:`inkey` to determine if a byte may
+ be read using :meth:`getch` without blocking. This is implemented
+ by wrapping msvcrt.kbhit() in a timeout.
+
+ :arg float timeout: When ``timeout`` is 0, this call is
+ non-blocking, otherwise blocking indefinitely until keypress
+ is detected when None (default). When ``timeout`` is a
+ positive number, returns after ``timeout`` seconds have
+ elapsed (float).
+ :rtype: bool
+ :returns: True if a keypress is awaiting to be read on the keyboard
+ attached to this terminal.
+ """
+ end = time.time() + (timeout or 0)
+ while True:
+
+ if msvcrt.kbhit():
+ return True
+
+ if timeout is not None and end < time.time():
+ break
+
+ return False
+
+ @staticmethod
+ def _winsize(fd):
+ """
+ Return named tuple describing size of the terminal by ``fd``.
+
+ :arg int fd: file descriptor queries for its window size.
+ :rtype: WINSZ
+
+ WINSZ is a :class:`collections.namedtuple` instance, whose structure
+ directly maps to the return value of the :const:`termios.TIOCGWINSZ`
+ ioctl return value. The return parameters are:
+
+ - ``ws_row``: width of terminal by its number of character cells.
+ - ``ws_col``: height of terminal by its number of character cells.
+ - ``ws_xpixel``: width of terminal by pixels (not accurate).
+ - ``ws_ypixel``: height of terminal by pixels (not accurate).
+ """
+ window = win32.get_terminal_size(fd)
+ return WINSZ(ws_row=window.lines, ws_col=window.columns,
+ ws_xpixel=0, ws_ypixel=0)
+
+ @contextlib.contextmanager
+ def cbreak(self):
+ """
+ Allow each keystroke to be read immediately after it is pressed.
+
+ This is a context manager for ``jinxed.w32.setcbreak()``.
+
+ .. note:: You must explicitly print any user input you would like
+ displayed. If you provide any kind of editing, you must handle
+ backspace and other line-editing control functions in this mode
+ as well!
+
+ **Normally**, characters received from the keyboard cannot be read
+ by Python until the *Return* key is pressed. Also known as *cooked* or
+ *canonical input* mode, it allows the tty driver to provide
+ line-editing before shuttling the input to your program and is the
+ (implicit) default terminal mode set by most unix shells before
+ executing programs.
+ """
+ if self._keyboard_fd is not None:
+
+ filehandle = msvcrt.get_osfhandle(self._keyboard_fd)
+
+ # Save current terminal mode:
+ save_mode = win32.get_console_mode(filehandle)
+ save_line_buffered = self._line_buffered
+ win32.setcbreak(filehandle)
+ try:
+ self._line_buffered = False
+ yield
+ finally:
+ win32.set_console_mode(filehandle, save_mode)
+ self._line_buffered = save_line_buffered
+
+ else:
+ yield
+
+ @contextlib.contextmanager
+ def raw(self):
+ """
+ A context manager for ``jinxed.w32.setcbreak()``.
+
+ Although both :meth:`break` and :meth:`raw` modes allow each keystroke
+ to be read immediately after it is pressed, Raw mode disables
+ processing of input and output.
+
+ In cbreak mode, special input characters such as ``^C`` are
+ interpreted by the terminal driver and excluded from the stdin stream.
+ In raw mode these values are receive by the :meth:`inkey` method.
+ """
+ if self._keyboard_fd is not None:
+
+ filehandle = msvcrt.get_osfhandle(self._keyboard_fd)
+
+ # Save current terminal mode:
+ save_mode = win32.get_console_mode(filehandle)
+ save_line_buffered = self._line_buffered
+ win32.setraw(filehandle)
+ try:
+ self._line_buffered = False
+ yield
+ finally:
+ win32.set_console_mode(filehandle, save_mode)
+ self._line_buffered = save_line_buffered
+
+ else:
+ yield
diff --git a/blessings/__init__.py b/blessings/__init__.py
deleted file mode 100644
index a2254b1b..00000000
--- a/blessings/__init__.py
+++ /dev/null
@@ -1,560 +0,0 @@
-"""A thin, practical wrapper around terminal coloring, styling, and
-positioning"""
-
-from contextlib import contextmanager
-import curses
-from curses import setupterm, tigetnum, tigetstr, tparm
-from fcntl import ioctl
-
-try:
- from io import UnsupportedOperation as IOUnsupportedOperation
-except ImportError:
- class IOUnsupportedOperation(Exception):
- """A dummy exception to take the place of Python 3's
- ``io.UnsupportedOperation`` in Python 2"""
-
-from os import isatty, environ
-from platform import python_version_tuple
-import struct
-import sys
-from termios import TIOCGWINSZ
-
-
-__all__ = ['Terminal']
-
-
-if ('3', '0', '0') <= python_version_tuple() < ('3', '2', '2+'): # Good till
- # 3.2.10
- # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string.
- raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 '
- 'support due to http://bugs.python.org/issue10570.')
-
-
-class Terminal(object):
- """An abstraction around terminal capabilities
-
- Unlike curses, this doesn't require clearing the screen before doing
- anything, and it's friendlier to use. It keeps the endless calls to
- ``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently
- when somebody pipes your output to a non-terminal.
-
- Instance attributes:
-
- ``stream``
- The stream the terminal outputs to. It's convenient to pass the stream
- around with the terminal; it's almost always needed when the terminal
- is and saves sticking lots of extra args on client functions in
- practice.
-
- """
- def __init__(self, kind=None, stream=None, force_styling=False):
- """Initialize the terminal.
-
- If ``stream`` is not a tty, I will default to returning an empty
- Unicode string for all capability values, so things like piping your
- output to a file won't strew escape sequences all over the place. The
- ``ls`` command sets a precedent for this: it defaults to columnar
- output when being sent to a tty and one-item-per-line when not.
-
- :arg kind: A terminal string as taken by ``setupterm()``. Defaults to
- the value of the ``TERM`` environment variable.
- :arg stream: A file-like object representing the terminal. Defaults to
- the original value of stdout, like ``curses.initscr()`` does.
- :arg force_styling: Whether to force the emission of capabilities, even
- if we don't seem to be in a terminal. This comes in handy if users
- are trying to pipe your output through something like ``less -r``,
- which supports terminal codes just fine but doesn't appear itself
- to be a terminal. Just expose a command-line option, and set
- ``force_styling`` based on it. Terminal initialization sequences
- will be sent to ``stream`` if it has a file descriptor and to
- ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them
- somewhere, and stdout is probably where the output is ultimately
- headed. If not, stderr is probably bound to the same terminal.)
-
- If you want to force styling to not happen, pass
- ``force_styling=None``.
-
- """
- if stream is None:
- stream = sys.__stdout__
- try:
- stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno')
- and callable(stream.fileno)
- else None)
- except IOUnsupportedOperation:
- stream_descriptor = None
-
- self._is_a_tty = (stream_descriptor is not None and
- isatty(stream_descriptor))
- self._does_styling = ((self.is_a_tty or force_styling) and
- force_styling is not None)
-
- # The descriptor to direct terminal initialization sequences to.
- # sys.__stdout__ seems to always have a descriptor of 1, even if output
- # is redirected.
- self._init_descriptor = (sys.__stdout__.fileno()
- if stream_descriptor is None
- else stream_descriptor)
- if self.does_styling:
- # Make things like tigetstr() work. Explicit args make setupterm()
- # work even when -s is passed to nosetests. Lean toward sending
- # init sequences to the stream if it has a file descriptor, and
- # send them to stdout as a fallback, since they have to go
- # somewhere.
- setupterm(kind or environ.get('TERM', 'unknown'),
- self._init_descriptor)
-
- self.stream = stream
-
- # Sugary names for commonly-used capabilities, intended to help avoid trips
- # to the terminfo man page and comments in your code:
- _sugar = dict(
- # Don't use "on" or "bright" as an underscore-separated chunk in any of
- # these (e.g. on_cology or rock_on) so we don't interfere with
- # __getattr__.
- save='sc',
- restore='rc',
-
- clear_eol='el',
- clear_bol='el1',
- clear_eos='ed',
- # 'clear' clears the whole screen.
- position='cup', # deprecated
- enter_fullscreen='smcup',
- exit_fullscreen='rmcup',
- move='cup',
- move_x='hpa',
- move_y='vpa',
- move_left='cub1',
- move_right='cuf1',
- move_up='cuu1',
- move_down='cud1',
-
- hide_cursor='civis',
- normal_cursor='cnorm',
-
- reset_colors='op', # oc doesn't work on my OS X terminal.
-
- normal='sgr0',
- reverse='rev',
- # 'bold' is just 'bold'. Similarly...
- # blink
- # dim
- # flash
- italic='sitm',
- no_italic='ritm',
- shadow='sshm',
- no_shadow='rshm',
- standout='smso',
- no_standout='rmso',
- subscript='ssubm',
- no_subscript='rsubm',
- superscript='ssupm',
- no_superscript='rsupm',
- underline='smul',
- no_underline='rmul')
-
- def __getattr__(self, attr):
- """Return a terminal capability, like bold.
-
- For example, you can say ``term.bold`` to get the string that turns on
- bold formatting and ``term.normal`` to get the string that turns it off
- again. Or you can take a shortcut: ``term.bold('hi')`` bolds its
- argument and sets everything to normal afterward. You can even combine
- things: ``term.bold_underline_red_on_bright_green('yowzers!')``.
-
- For a parametrized capability like ``cup``, pass the parameters too:
- ``some_term.cup(line, column)``.
-
- ``man terminfo`` for a complete list of capabilities.
-
- Return values are always Unicode.
-
- """
- resolution = (self._resolve_formatter(attr) if self.does_styling
- else NullCallableString())
- setattr(self, attr, resolution) # Cache capability codes.
- return resolution
-
- @property
- def does_styling(self):
- """Whether attempt to emit capabilities
-
- This is influenced by the ``is_a_tty`` property and by the
- ``force_styling`` argument to the constructor. You can examine
- this value to decide whether to draw progress bars or other frippery.
-
- """
- return self._does_styling
-
- @property
- def is_a_tty(self):
- """Whether my ``stream`` appears to be associated with a terminal"""
- return self._is_a_tty
-
- @property
- def height(self):
- """The height of the terminal in characters
-
- If no stream or a stream not representing a terminal was passed in at
- construction, return the dimension of the controlling terminal so
- piping to things that eventually display on the terminal (like ``less
- -R``) work. If a stream representing a terminal was passed in, return
- the dimensions of that terminal. If there somehow is no controlling
- terminal, return ``None``. (Thus, you should check that the property
- ``is_a_tty`` is true before doing any math on the result.)
-
- """
- return self._height_and_width()[0]
-
- @property
- def width(self):
- """The width of the terminal in characters
-
- See ``height()`` for some corner cases.
-
- """
- return self._height_and_width()[1]
-
- def _height_and_width(self):
- """Return a tuple of (terminal height, terminal width).
-
- Start by trying TIOCGWINSZ (Terminal I/O-Control: Get Window Size),
- falling back to environment variables (LINES, COLUMNS), and returning
- (None, None) if those are unavailable or invalid.
-
- """
- # tigetnum('lines') and tigetnum('cols') update only if we call
- # setupterm() again.
- for descriptor in self._init_descriptor, sys.__stdout__:
- try:
- return struct.unpack(
- 'hhhh', ioctl(descriptor, TIOCGWINSZ, '\000' * 8))[0:2]
- except IOError:
- # when the output stream or init descriptor is not a tty, such
- # as when when stdout is piped to another program, fe. tee(1),
- # these ioctls will raise IOError
- pass
- try:
- return int(environ.get('LINES')), int(environ.get('COLUMNS'))
- except TypeError:
- return None, None
-
- @contextmanager
- def location(self, x=None, y=None):
- """Return a context manager for temporarily moving the cursor.
-
- Move the cursor to a certain position on entry, let you print stuff
- there, then return the cursor to its original position::
-
- term = Terminal()
- with term.location(2, 5):
- print 'Hello, world!'
- for x in xrange(10):
- print 'I can do it %i times!' % x
-
- Specify ``x`` to move to a certain column, ``y`` to move to a certain
- row, both, or neither. If you specify neither, only the saving and
- restoration of cursor position will happen. This can be useful if you
- simply want to restore your place after doing some manual cursor
- movement.
-
- """
- # Save position and move to the requested column, row, or both:
- self.stream.write(self.save)
- if x is not None and y is not None:
- self.stream.write(self.move(y, x))
- elif x is not None:
- self.stream.write(self.move_x(x))
- elif y is not None:
- self.stream.write(self.move_y(y))
- try:
- yield
- finally:
- # Restore original cursor position:
- self.stream.write(self.restore)
-
- @contextmanager
- def fullscreen(self):
- """Return a context manager that enters fullscreen mode while inside it
- and restores normal mode on leaving."""
- self.stream.write(self.enter_fullscreen)
- try:
- yield
- finally:
- self.stream.write(self.exit_fullscreen)
-
- @contextmanager
- def hidden_cursor(self):
- """Return a context manager that hides the cursor while inside it and
- makes it visible on leaving."""
- self.stream.write(self.hide_cursor)
- try:
- yield
- finally:
- self.stream.write(self.normal_cursor)
-
- @property
- def color(self):
- """Return a capability that sets the foreground color.
-
- The capability is unparametrized until called and passed a number
- (0-15), at which point it returns another string which represents a
- specific color change. This second string can further be called to
- color a piece of text and set everything back to normal afterward.
-
- :arg num: The number, 0-15, of the color
-
- """
- return ParametrizingString(self._foreground_color, self.normal)
-
- @property
- def on_color(self):
- """Return a capability that sets the background color.
-
- See ``color()``.
-
- """
- return ParametrizingString(self._background_color, self.normal)
-
- @property
- def number_of_colors(self):
- """Return the number of colors the terminal supports.
-
- Common values are 0, 8, 16, 88, and 256.
-
- Though the underlying capability returns -1 when there is no color
- support, we return 0. This lets you test more Pythonically::
-
- if term.number_of_colors:
- ...
-
- We also return 0 if the terminal won't tell us how many colors it
- supports, which I think is rare.
-
- """
- # This is actually the only remotely useful numeric capability. We
- # don't name it after the underlying capability, because we deviate
- # slightly from its behavior, and we might someday wish to give direct
- # access to it.
- colors = tigetnum('colors') # Returns -1 if no color support, -2 if no
- # such cap.
- # self.__dict__['colors'] = ret # Cache it. It's not changing.
- # (Doesn't work.)
- return colors if colors >= 0 else 0
-
- def _resolve_formatter(self, attr):
- """Resolve a sugary or plain capability name, color, or compound
- formatting function name into a callable capability.
-
- Return a ``ParametrizingString`` or a ``FormattingString``.
-
- """
- if attr in COLORS:
- return self._resolve_color(attr)
- elif attr in COMPOUNDABLES:
- # Bold, underline, or something that takes no parameters
- return self._formatting_string(self._resolve_capability(attr))
- else:
- formatters = split_into_formatters(attr)
- if all(f in COMPOUNDABLES for f in formatters):
- # It's a compound formatter, like "bold_green_on_red". Future
- # optimization: combine all formatting into a single escape
- # sequence.
- return self._formatting_string(
- u''.join(self._resolve_formatter(s) for s in formatters))
- else:
- return ParametrizingString(self._resolve_capability(attr))
-
- def _resolve_capability(self, atom):
- """Return a terminal code for a capname or a sugary name, or an empty
- Unicode.
-
- The return value is always Unicode, because otherwise it is clumsy
- (especially in Python 3) to concatenate with real (Unicode) strings.
-
- """
- code = tigetstr(self._sugar.get(atom, atom))
- if code:
- # See the comment in ParametrizingString for why this is latin1.
- return code.decode('latin1')
- return u''
-
- def _resolve_color(self, color):
- """Resolve a color like red or on_bright_green into a callable
- capability."""
- # TODO: Does curses automatically exchange red and blue and cyan and
- # yellow when a terminal supports setf/setb rather than setaf/setab?
- # I'll be blasted if I can find any documentation. The following
- # assumes it does.
- color_cap = (self._background_color if 'on_' in color else
- self._foreground_color)
- # 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]
- return self._formatting_string(
- color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset))
-
- @property
- def _foreground_color(self):
- return self.setaf or self.setf
-
- @property
- def _background_color(self):
- return self.setab or self.setb
-
- def _formatting_string(self, formatting):
- """Return a new ``FormattingString`` which implicitly receives my
- notion of "normal"."""
- return FormattingString(formatting, self.normal)
-
-
-def derivative_colors(colors):
- """Return the names of valid color variants, given the base colors."""
- return set([('on_' + c) for c in colors] +
- [('bright_' + c) for c in colors] +
- [('on_bright_' + c) for c in colors])
-
-
-COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan',
- 'white'])
-COLORS.update(derivative_colors(COLORS))
-COMPOUNDABLES = (COLORS |
- set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic',
- 'shadow', 'standout', 'subscript', 'superscript']))
-
-
-class ParametrizingString(unicode):
- """A Unicode string which can be called to parametrize it as a terminal
- capability"""
-
- def __new__(cls, formatting, normal=None):
- """Instantiate.
-
- :arg normal: If non-None, indicates that, once parametrized, this can
- be used as a ``FormattingString``. The value is used as the
- "normal" capability.
-
- """
- new = unicode.__new__(cls, formatting)
- new._normal = normal
- return new
-
- def __call__(self, *args):
- 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.
- #
- # We use *latin1* encoding so that bytes emitted by tparm are
- # encoded to their native value: some terminal kinds, such as
- # 'avatar' or 'kermit', emit 8-bit bytes in range 0x7f to 0xff.
- # latin1 leaves these values unmodified in their conversion to
- # unicode byte values. The terminal emulator will "catch" and
- # handle these values, even if emitting utf8-encoded text, where
- # these bytes would otherwise be illegal utf8 start bytes.
- parametrized = tparm(self.encode('latin1'), *args).decode('latin1')
- return (parametrized if self._normal is None else
- FormattingString(parametrized, self._normal))
- except curses.error:
- # Catch "must call (at least) setupterm() first" errors, as when
- # running simply `nosetests` (without progressive) on nose-
- # progressive. Perhaps the terminal has gone away between calling
- # tigetstr and calling tparm.
- return u''
- except TypeError:
- # If the first non-int (i.e. incorrect) arg was a string, suggest
- # something intelligent:
- if len(args) == 1 and isinstance(args[0], basestring):
- raise TypeError(
- 'A native or nonexistent capability template received '
- '%r when it was expecting ints. You probably misspelled a '
- 'formatting call like bright_red_on_white(...).' % args)
- else:
- # Somebody passed a non-string; I don't feel confident
- # guessing what they were trying to do.
- raise
-
-
-class FormattingString(unicode):
- """A Unicode string which can be called upon a piece of text to wrap it in
- formatting"""
-
- def __new__(cls, formatting, normal):
- new = unicode.__new__(cls, formatting)
- new._normal = normal
- return new
-
- def __call__(self, text):
- """Return a new string that is ``text`` formatted with my contents.
-
- At the beginning of the string, I prepend the formatting that is my
- contents. At the end, I append the "normal" sequence to set everything
- back to defaults. The return value is always a Unicode.
-
- """
- return self + text + self._normal
-
-
-class NullCallableString(unicode):
- """A dummy callable Unicode to stand in for ``FormattingString`` and
- ``ParametrizingString``
-
- We use this when there is no tty and thus all capabilities should be blank.
-
- """
- def __new__(cls):
- new = unicode.__new__(cls, u'')
- return new
-
- def __call__(self, *args):
- """Return a Unicode or whatever you passed in as the first arg
- (hopefully a string of some kind).
-
- When called with an int as the first arg, return an empty Unicode. An
- int is a good hint that I am a ``ParametrizingString``, as there are
- only about half a dozen string-returning capabilities on OS X's
- terminfo man page which take any param that's not an int, and those are
- seldom if ever used on modern terminal emulators. (Most have to do with
- programming function keys. Blessings' story for supporting
- non-string-returning caps is undeveloped.) And any parametrized
- capability in a situation where all capabilities themselves are taken
- to be blank are, of course, themselves blank.
-
- When called with a non-int as the first arg (no no args at all), return
- the first arg. I am acting as a ``FormattingString``.
-
- """
- if len(args) != 1 or isinstance(args[0], int):
- # I am acting as a ParametrizingString.
-
- # tparm can take not only ints but also (at least) strings as its
- # second...nth args. But we don't support callably parametrizing
- # caps that take non-ints yet, so we can cheap out here. TODO: Go
- # through enough of the motions in the capability resolvers to
- # determine which of 2 special-purpose classes,
- # NullParametrizableString or NullFormattingString, to return, and
- # retire this one.
- return u''
- return args[0] # Should we force even strs in Python 2.x to be
- # unicodes? No. How would I know what encoding to use
- # to convert it?
-
-
-def split_into_formatters(compound):
- """Split a possibly compound format string into segments.
-
- >>> split_into_formatters('bold_underline_bright_blue_on_red')
- ['bold', 'underline', 'bright_blue', 'on_red']
-
- """
- merged_segs = []
- # These occur only as prefixes, so they can always be merged:
- mergeable_prefixes = ['on', 'bright', 'on_bright']
- for s in compound.split('_'):
- if merged_segs and merged_segs[-1] in mergeable_prefixes:
- merged_segs[-1] += '_' + s
- else:
- merged_segs.append(s)
- return merged_segs
diff --git a/blessings/tests.py b/blessings/tests.py
deleted file mode 100644
index aff3a2f2..00000000
--- a/blessings/tests.py
+++ /dev/null
@@ -1,270 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Automated tests (as opposed to human-verified test patterns)
-
-It was tempting to mock out curses to get predictable output from ``tigetstr``,
-but there are concrete integration-testing benefits in not doing so. For
-instance, ``tigetstr`` changed its return type in Python 3.2.3. So instead, we
-simply create all our test ``Terminal`` instances with a known terminal type.
-All we require from the host machine is that a standard terminfo definition of
-xterm-256color exists.
-
-"""
-from __future__ import with_statement # Make 2.5-compatible
-from curses import tigetstr, tparm
-from functools import partial
-from StringIO import StringIO
-import sys
-
-from nose import SkipTest
-from nose.tools import eq_
-
-# This tests that __all__ is correct, since we use below everything that should
-# be imported:
-from blessings import *
-
-
-TestTerminal = partial(Terminal, kind='xterm-256color')
-
-
-def unicode_cap(cap):
- """Return the result of ``tigetstr`` except as Unicode."""
- return tigetstr(cap).decode('latin1')
-
-
-def unicode_parm(cap, *parms):
- """Return the result of ``tparm(tigetstr())`` except as Unicode."""
- return tparm(tigetstr(cap), *parms).decode('latin1')
-
-
-def test_capability():
- """Check that a capability lookup works.
-
- Also test that Terminal grabs a reasonable default stream. This test
- assumes it will be run from a tty.
-
- """
- t = TestTerminal()
- sc = unicode_cap('sc')
- eq_(t.save, sc)
- eq_(t.save, sc) # Make sure caching doesn't screw it up.
-
-
-def test_capability_without_tty():
- """Assert capability templates are '' when stream is not a tty."""
- t = TestTerminal(stream=StringIO())
- eq_(t.save, u'')
- eq_(t.red, u'')
-
-
-def test_capability_with_forced_tty():
- """If we force styling, capabilities had better not (generally) be
- empty."""
- t = TestTerminal(stream=StringIO(), force_styling=True)
- eq_(t.save, unicode_cap('sc'))
-
-
-def test_parametrization():
- """Test parametrizing a capability."""
- eq_(TestTerminal().cup(3, 4), unicode_parm('cup', 3, 4))
-
-
-def test_height_and_width():
- """Assert that ``height_and_width()`` returns ints."""
- t = TestTerminal() # kind shouldn't matter.
- assert isinstance(t.height, int)
- assert isinstance(t.width, int)
-
-
-def test_stream_attr():
- """Make sure Terminal exposes a ``stream`` attribute that defaults to
- something sane."""
- eq_(Terminal().stream, sys.__stdout__)
-
-
-def test_location():
- """Make sure ``location()`` does what it claims."""
- t = TestTerminal(stream=StringIO(), force_styling=True)
-
- with t.location(3, 4):
- t.stream.write(u'hi')
-
- eq_(t.stream.getvalue(), unicode_cap('sc') +
- unicode_parm('cup', 4, 3) +
- u'hi' +
- unicode_cap('rc'))
-
-
-def test_horizontal_location():
- """Make sure we can move the cursor horizontally without changing rows."""
- t = TestTerminal(stream=StringIO(), force_styling=True)
- with t.location(x=5):
- pass
- eq_(t.stream.getvalue(), unicode_cap('sc') +
- unicode_parm('hpa', 5) +
- unicode_cap('rc'))
-
-
-def test_null_location():
- """Make sure ``location()`` with no args just does position restoration."""
- t = TestTerminal(stream=StringIO(), force_styling=True)
- with t.location():
- pass
- eq_(t.stream.getvalue(), unicode_cap('sc') +
- unicode_cap('rc'))
-
-
-def test_zero_location():
- """Make sure ``location()`` pays attention to 0-valued args."""
- t = TestTerminal(stream=StringIO(), force_styling=True)
- with t.location(0, 0):
- pass
- eq_(t.stream.getvalue(), unicode_cap('sc') +
- unicode_parm('cup', 0, 0) +
- unicode_cap('rc'))
-
-
-def test_null_fileno():
- """Make sure ``Terminal`` works when ``fileno`` is ``None``.
-
- This simulates piping output to another program.
-
- """
- out = StringIO()
- out.fileno = None
- t = TestTerminal(stream=out)
- eq_(t.save, u'')
-
-
-def test_mnemonic_colors():
- """Make sure color shortcuts work."""
- def color(num):
- return unicode_parm('setaf', num)
-
- def on_color(num):
- return unicode_parm('setab', num)
-
- # Avoid testing red, blue, yellow, and cyan, since they might someday
- # change depending on terminal type.
- t = TestTerminal()
- eq_(t.white, color(7))
- eq_(t.green, color(2)) # Make sure it's different than white.
- eq_(t.on_black, on_color(0))
- eq_(t.on_green, on_color(2))
- eq_(t.bright_black, color(8))
- eq_(t.bright_green, color(10))
- eq_(t.on_bright_black, on_color(8))
- eq_(t.on_bright_green, on_color(10))
-
-
-def test_callable_numeric_colors():
- """``color(n)`` should return a formatting wrapper."""
- t = TestTerminal()
- eq_(t.color(5)('smoo'), t.magenta + 'smoo' + t.normal)
- eq_(t.color(5)('smoo'), t.color(5) + 'smoo' + t.normal)
- eq_(t.on_color(2)('smoo'), t.on_green + 'smoo' + t.normal)
- eq_(t.on_color(2)('smoo'), t.on_color(2) + 'smoo' + t.normal)
-
-
-def test_null_callable_numeric_colors():
- """``color(n)`` should be a no-op on null terminals."""
- t = TestTerminal(stream=StringIO())
- eq_(t.color(5)('smoo'), 'smoo')
- eq_(t.on_color(6)('smoo'), 'smoo')
-
-
-def test_naked_color_cap():
- """``term.color`` should return a stringlike capability."""
- t = TestTerminal()
- eq_(t.color + '', t.setaf + '')
-
-
-def test_number_of_colors_without_tty():
- """``number_of_colors`` should return 0 when there's no tty."""
- # Hypothesis: once setupterm() has run and decided the tty supports 256
- # colors, it never changes its mind.
- raise SkipTest
-
- t = TestTerminal(stream=StringIO())
- eq_(t.number_of_colors, 0)
- t = TestTerminal(stream=StringIO(), force_styling=True)
- eq_(t.number_of_colors, 0)
-
-
-def test_number_of_colors_with_tty():
- """``number_of_colors`` should work."""
- t = TestTerminal()
- eq_(t.number_of_colors, 256)
-
-
-def test_formatting_functions():
- """Test crazy-ass formatting wrappers, both simple and compound."""
- t = TestTerminal()
- # By now, it should be safe to use sugared attributes. Other tests test
- # those.
- eq_(t.bold(u'hi'), t.bold + u'hi' + t.normal)
- eq_(t.green('hi'), t.green + u'hi' + t.normal) # Plain strs for Python 2.x
- # Test some non-ASCII chars, probably not necessary:
- eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö' + t.normal)
- eq_(t.bold_underline_green_on_red('boo'),
- t.bold + t.underline + t.green + t.on_red + u'boo' + t.normal)
- # Don't spell things like this:
- eq_(t.on_bright_red_bold_bright_green_underline('meh'),
- t.on_bright_red + t.bold + t.bright_green + t.underline + u'meh' +
- t.normal)
-
-
-def test_formatting_functions_without_tty():
- """Test crazy-ass formatting wrappers when there's no tty."""
- t = TestTerminal(stream=StringIO())
- eq_(t.bold(u'hi'), u'hi')
- eq_(t.green('hi'), u'hi')
- # Test non-ASCII chars, no longer really necessary:
- eq_(t.bold_green(u'boö'), u'boö')
- eq_(t.bold_underline_green_on_red('loo'), u'loo')
- eq_(t.on_bright_red_bold_bright_green_underline('meh'), u'meh')
-
-
-def test_nice_formatting_errors():
- """Make sure you get nice hints if you misspell a formatting wrapper."""
- t = TestTerminal()
- try:
- t.bold_misspelled('hey')
- except TypeError, e:
- assert 'probably misspelled' in e.args[0]
-
- try:
- t.bold_misspelled(u'hey') # unicode
- except TypeError, e:
- assert 'probably misspelled' in e.args[0]
-
- try:
- t.bold_misspelled(None) # an arbitrary non-string
- except TypeError, e:
- assert 'probably misspelled' not in e.args[0]
-
- try:
- t.bold_misspelled('a', 'b') # >1 string arg
- except TypeError, e:
- assert 'probably misspelled' not in e.args[0]
-
-
-def test_init_descriptor_always_initted():
- """We should be able to get a height and width even on no-tty Terminals."""
- t = Terminal(stream=StringIO())
- eq_(type(t.height), int)
-
-
-def test_force_styling_none():
- """If ``force_styling=None`` is passed to the constructor, don't ever do
- styling."""
- t = TestTerminal(force_styling=None)
- eq_(t.save, '')
-
-
-def test_null_callable_string():
- """Make sure NullCallableString tolerates all numbers and kinds of args it
- might receive."""
- t = TestTerminal(stream=StringIO())
- eq_(t.clear, '')
- eq_(t.move(1, 2), '')
- eq_(t.move_x(1), '')
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index c1d668a3..00000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,130 +0,0 @@
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS =
-SPHINXBUILD = sphinx-build
-PAPER =
-BUILDDIR = _build
-
-# Internal variables.
-PAPEROPT_a4 = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
-
-help:
- @echo "Please use \`make ' where is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @echo " singlehtml to make a single large HTML file"
- @echo " pickle to make pickle files"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " devhelp to make HTML files and a Devhelp project"
- @echo " epub to make an epub"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " latexpdf to make LaTeX files and run them through pdflatex"
- @echo " text to make text files"
- @echo " man to make manual pages"
- @echo " changes to make an overview of all changed/added/deprecated items"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
-
-clean:
- -rm -rf $(BUILDDIR)/*
-
-html:
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
- $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
- $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
- @echo
- @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
- @echo
- @echo "Build finished; now you can process the pickle files."
-
-json:
- $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
- @echo
- @echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
- @echo
- @echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
- $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
- @echo
- @echo "Build finished; now you can run "qcollectiongenerator" with the" \
- ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/blessings.qhcp"
- @echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/blessings.qhc"
-
-devhelp:
- $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
- @echo
- @echo "Build finished."
- @echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/blessings"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/blessings"
- @echo "# devhelp"
-
-epub:
- $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
- @echo
- @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo
- @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make' in that directory to run these through (pdf)latex" \
- "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through pdflatex..."
- make -C $(BUILDDIR)/latex all-pdf
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
- $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
- @echo
- @echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
- $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
- @echo
- @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-changes:
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
- @echo
- @echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
- @echo
- @echo "Link check complete; look for any errors in the above output " \
- "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
- $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
- @echo "Testing of doctests in the sources finished, look at the " \
- "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/_static/soulburner-ru-family-encodings.jpg b/docs/_static/soulburner-ru-family-encodings.jpg
new file mode 100644
index 00000000..ecb964f8
Binary files /dev/null and b/docs/_static/soulburner-ru-family-encodings.jpg differ
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 00000000..77f24cec
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,59 @@
+API Documentation
+=================
+
+color.py
+--------
+
+.. automodule:: blessed.color
+ :members:
+ :undoc-members:
+ :private-members:
+
+colorspace.py
+-------------
+
+.. automodule:: blessed.colorspace
+ :members:
+ :undoc-members:
+ :private-members:
+
+formatters.py
+-------------
+
+.. automodule:: blessed.formatters
+ :members:
+ :undoc-members:
+ :private-members:
+ :special-members: __call__
+.. autodata:: COLORS
+.. autodata:: COMPOUNDABLES
+
+keyboard.py
+-----------
+
+.. automodule:: blessed.keyboard
+ :members:
+ :undoc-members:
+ :private-members:
+ :special-members: __new__
+.. autofunction:: _alternative_left_right
+.. autodata:: DEFAULT_SEQUENCE_MIXIN
+.. autodata:: CURSES_KEYCODE_OVERRIDE_MIXIN
+.. autodata:: _CURSES_KEYCODE_ADDINS
+
+sequences.py
+------------
+
+.. automodule:: blessed.sequences
+ :members:
+ :undoc-members:
+ :private-members:
+
+terminal.py
+-----------
+
+.. automodule:: blessed.terminal
+ :members:
+ :undoc-members:
+ :special-members: __getattr__
+.. autodata:: _CUR_TERM
diff --git a/docs/conf.py b/docs/conf.py
index e9ed9ebf..1af36bc9 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,33 +1,71 @@
-# -*- coding: utf-8 -*-
-#
-# blessings documentation build configuration file, created by
-# sphinx-quickstart on Thu Mar 31 13:40:27 2011.
+# std imports
+import os
+import sys
+import json
+import functools
+
+# local
+import sphinx_rtd_theme
+import sphinx.environment
+from docutils.utils import get_source_line
+
+# for github.py
+HERE = os.path.dirname(__file__)
+sys.path.insert(0, os.path.abspath('sphinxext'))
+github_project_url = "https://github.com/jquast/blessed"
+
+
+# ! Monkey Patching!
#
-# This file is execfile()d with the current directory set to its containing dir.
+# Seems many folks beside ourselves would like to see external image urls
+# **not** generated as a warning. "nonlocal image URI found": we want our
+# "badge icons" on pypi and github, but we don't want to miss out on any
+# other "warnings as errors" which we wish to fail the build if it happens.
#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
+# https://github.com/SuperCowPowers/workbench/issues/172
+# https://groups.google.com/forum/#!topic/sphinx-users/GNx7PVXoZIU
+# http://stackoverflow.com/a/28778969
#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
+def _warn_node(self, msg, node):
+ if not msg.startswith('nonlocal image URI found:'):
+ self._warnfunc(msg, '%s:%s' % get_source_line(node))
+
+
+sphinx.environment.BuildEnvironment.warn_node = _warn_node
-import sys, os
-import blessings
+def no_op_wraps(func):
+ """Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation."""
+ if func.__module__ is None or 'blessed' not in func.__module__:
+ return functools.orig_wraps(func)
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
+ def wrapper(decorator):
+ sys.stderr.write('patched for function signature: {0!r}\n'.format(func))
+ return func
+ return wrapper
-# -- General configuration -----------------------------------------------------
+
+# Monkey-patch functools.wraps and contextlib.wraps
+# https://github.com/sphinx-doc/sphinx/issues/1711#issuecomment-93126473
+functools.orig_wraps = functools.wraps
+functools.wraps = no_op_wraps
+import contextlib # isort:skip # noqa
+contextlib.wraps = no_op_wraps
+from blessed.terminal import * # isort:skip # noqa
+
+# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
+# needs_sphinx = '1.0'
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.viewcode',
+ 'github',
+ 'sphinx_paramlinks',
+ ]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -36,183 +74,199 @@
source_suffix = '.rst'
# The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
-project = u'Blessings'
-copyright = u'2011, Erik Rose'
+project = u'Blessed'
+copyright = u'2011 Erik Rose, Jeff Quast'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
-version = '1.6'
+version = json.load(
+ open(os.path.join(HERE, os.pardir, 'version.json'), 'r')
+)['version']
+
# The full version, including alpha/beta/rc tags.
release = version
-# The language for content autogenerated by Sphinx. Refer to documentation
+# The language for content auto-generated by Sphinx. Refer to documentation
# for a list of supported languages.
-#language = None
+# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
-#today = ''
+# today = ''
# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
-# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
-#add_module_names = True
+add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
-#show_authors = False
+# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
-# -- Options for HTML output ---------------------------------------------------
+# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'default'
+html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
-#html_theme_options = {}
+# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
-#html_title = None
+# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
-#html_logo = None
+# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
-#html_favicon = None
+# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = []
+# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
# If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
# If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
# If true, the index is split into individual pages for each letter.
-#html_split_index = False
+html_split_index = True
# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
# Output file base name for HTML help builder.
-htmlhelp_basename = 'blessingsdoc'
+htmlhelp_basename = 'blesseddoc'
-# -- Options for LaTeX output --------------------------------------------------
+# -- Options for LaTeX output -------------------------------------------------
# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
+# (source start file, target name, title, author, documentclass
+# [howto/manual]).
latex_documents = [
- ('index', 'blessings.tex', u'Blessings Documentation',
- u'Erik Rose', 'manual'),
+ ('index', 'blessed.tex', u'Blessed Documentation',
+ u'Erik Rose, Jeff Quast', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
-#latex_logo = None
+# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
# If true, show page references after internal links.
-#latex_show_pagerefs = False
+# latex_show_pagerefs = False
# If true, show URL addresses after external links.
-#latex_show_urls = False
+# latex_show_urls = False
# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
# Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
# If false, no module index is generated.
-#latex_domain_indices = True
+# latex_domain_indices = True
-# -- Options for manual page output --------------------------------------------
+# -- Options for manual page output -------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- ('index', 'Blessings', u'Blessings Documentation',
- [u'Erik Rose'], 1)
+ ('index', 'Blessed', u'Blessed Documentation',
+ [u'Erik Rose, Jeff Quast'], 1)
]
+
+# sort order of API documentation is by their appearance in source code
+autodoc_member_order = 'bysource'
+
+# when linking to standard python library, use and prefer python 3
+# documentation.
+intersphinx_mapping = {'https://docs.python.org/3/': None}
+
+# Both the class’ and the __init__ method’s docstring are concatenated and
+# inserted.
+autoclass_content = "both"
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 120000
index 00000000..798f2aa2
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1 @@
+../CONTRIBUTING.rst
\ No newline at end of file
diff --git a/docs/examples.rst b/docs/examples.rst
new file mode 100644
index 00000000..8092a257
--- /dev/null
+++ b/docs/examples.rst
@@ -0,0 +1,89 @@
+Examples
+========
+
+A few programs are provided with blessed to help interactively
+test the various API features, but also serve as examples of using
+blessed to develop applications.
+
+These examples are not distributed with the package -- they are
+only available in the github repository. You can retrieve them
+by cloning the repository, or simply downloading the "raw" file
+link.
+
+editor.py
+---------
+https://github.com/jquast/blessed/blob/master/bin/editor.py
+
+This program demonstrates using the directional keys and noecho input
+mode. It acts as a (very dumb) fullscreen editor, with support for
+saving a file, as well as including a rudimentary line-editor.
+
+keymatrix.py
+------------
+https://github.com/jquast/blessed/blob/master/bin/keymatrix.py
+
+This program displays a "gameboard" of all known special KEY_NAME
+constants. When the key is depressed, it is highlighted, as well
+as displaying the unicode sequence, integer code, and friendly-name
+of any key pressed.
+
+on_resize.py
+------------
+https://github.com/jquast/blessed/blob/master/bin/on_resize.py
+
+This program installs a SIGWINCH signal handler, which detects
+screen resizes while also polling for input, displaying keypresses.
+
+This demonstrates how a program can react to screen resize events.
+
+progress_bar.py
+---------------
+https://github.com/jquast/blessed/blob/master/bin/progress_bar.py
+
+This program demonstrates a simple progress bar. All text is written
+to stderr, to avoid the need to "flush" or emit newlines, and makes
+use of the move_x (hpa) capability to "overstrike" the display a
+scrolling progress bar.
+
+.. _tprint.py:
+
+tprint.py
+---------
+https://github.com/jquast/blessed/blob/master/bin/tprint.py
+
+This program demonstrates how users may customize FormattingString
+styles. Accepting a string style, such as "bold" or "bright_red"
+as the first argument, all subsequent arguments are displayed by
+the given style. This shows how a program could provide
+user-customizable compound formatting names to configure a program's
+styling.
+
+worms.py
+--------
+https://github.com/jquast/blessed/blob/master/bin/worms.py
+
+This program demonstrates how an interactive game could be made
+with blessed. It is similar to `NIBBLES.BAS
+`_
+or "snake" of early mobile platforms.
+
+resize.py
+---------
+https://github.com/jquast/blessed/blob/master/bin/resize.py
+
+This program demonstrates the :meth:`~.get_location` method,
+behaving similar to `resize(1)
+`_
+: set environment and terminal settings to current window size.
+The window size is determined by eliciting an answerback
+sequence from the connecting terminal emulator.
+
+.. _detect-multibyte.py:
+
+detect-multibyte.py
+-------------------
+https://github.com/jquast/blessed/blob/master/bin/detect-multibyte.py
+
+This program also demonstrates how the :meth:`~.get_location` method
+can be used to reliably test whether the terminal emulator of the connecting
+client is capable of rendering multibyte characters as a single cell.
diff --git a/docs/further.rst b/docs/further.rst
new file mode 100644
index 00000000..9e93d078
--- /dev/null
+++ b/docs/further.rst
@@ -0,0 +1,103 @@
+Further Reading
+===============
+
+As a developer's API, blessed is often bundled with frameworks and toolsets
+that dive deeper into Terminal I/O programming than :class:`~.Terminal` offers.
+Here are some recommended readings to help you along:
+
+- `terminfo(5)
+ `_
+ manpage of your preferred posix-like operating system. The capabilities
+ available as attributes of :class:`~.Terminal` are directly mapped to those
+ listed in the column **Cap-name**.
+
+- `termios(4)
+ `_
+ of your preferred posix-like operating system.
+
+- `The TTY demystified
+ `_
+ by Linus Åkesson.
+
+- `A Brief Introduction to Termios
+ `_ by
+ Nelson Elhage.
+
+- Richard Steven's `Advance Unix Programming
+ `_
+ ("AUP") provides two very good chapters, "Terminal I/O" and
+ "Pseudo Terminals".
+
+- GNU's `The Termcap Manual
+ `_
+ by Richard M. Stallman.
+
+- `Chapter 4 `_
+ of CUNY's course material for *Introduction to System Programming*, by
+ `Stewart Weiss `_
+
+- `Chapter 11
+ `_
+ of the IEEE Open Group Base Specifications Issue 7, "General Terminal
+ Interface"
+
+- The GNU C Library documentation, section `Low-Level Terminal Interface
+ `_
+
+- The source code of many popular terminal emulators. If there is ever any
+ question of "the meaning of a terminal capability", or whether or not your
+ preferred terminal emulator actually handles them, read the source!
+
+ These are often written in the C language, and directly map the
+ "Control Sequence Inducers" (CSI, literally ``\x1b[`` for most modern
+ terminal types) emitted by most terminal capabilities to an action in a
+ series of ``case`` switch statements.
+
+ - Many modern libraries are now based on `libvte
+ `_ (or just 'vte'): Gnome Terminal,
+ sakura, Terminator, Lilyterm, ROXTerm, evilvte, Termit, Termite, Tilda,
+ tinyterm, lxterminal.
+ - xterm, urxvt, SyncTerm, and EtherTerm.
+ - There are far too many to name, Chose one you like!
+
+
+- The source code of the tty(4), pty(4), and the given "console driver" for
+ any posix-like operating system. If you search thoroughly enough, you will
+ eventually discover a terminal sequence decoder, usually a ``case`` switch
+ that translates ``\x1b[0m`` into a "reset color" action towards the video
+ driver. Though ``tty.c`` is linked here (the only kernel file common among
+ them), it is probably not the most interesting, but it can get you started:
+
+ - `FreeBSD `_
+ - `OpenBSD `_
+ - `Illumos (Solaris) `_
+ - `Minix `_
+ - `Linux `_
+
+ The TTY driver is a great introduction to Kernel and Systems programming,
+ because familiar components may be discovered and experimented with. It is
+ available on all operating systems, and because of its critical nature,
+ examples of efficient file I/O, character buffers (often implemented as
+ "ring buffers") and even fine-grained kernel locking can be found.
+
+- `Thomas E. Dickey `_ has been maintaining
+ `xterm `_, as well as a
+ primary maintainer of many related packages such as `ncurses
+ `_ for quite a long
+ while.
+
+- `termcap & terminfo (O'Reilly Nutshell)
+ `_
+ by Linda Mui, Tim O'Reilly, and John Strang.
+
+- Note that System-V systems, also known as `Unix98
+ `_ (SunOS, HP-UX,
+ AIX and others) use a `Streams `_
+ interface. On these systems, the `ioctl(2)
+ `_
+ interface provides the ``PUSH`` and ``POP`` parameters to communicate with
+ a Streams device driver, which differs significantly from Linux and BSD.
+
+ Many of these systems provide compatible interfaces for Linux, but they may
+ not always be as complete as the counterpart they emulate, most especially
+ in regards to managing pseudo-terminals.
diff --git a/docs/history.rst b/docs/history.rst
new file mode 100644
index 00000000..9a5aa750
--- /dev/null
+++ b/docs/history.rst
@@ -0,0 +1,281 @@
+Version History
+===============
+1.17
+ * introduced: 24-bit color support, detected by ``term.number_of_colors == 1 << 24``, and 24-bit
+ color foreground method :meth:`~Terminal.color_rgb` and background method
+ :meth:`~Terminal.on_color_rgb`, as well as 676 common X11 color attribute names are now
+ possible, such as ``term.aquamarine_on_wheat``.
+ * bugfix: prevent error condition, ``ValueError: underlying buffer has been detached`` in rare
+ conditions where sys.__stdout__ has been detached in test frameworks. :ghissue:`126`.
+ * bugfix: off-by-one error in :meth:`~.Terminal.get_location`, now accounts for ``%i`` in
+ cursor_report, :ghissue:`94`.
+ * bugfix :meth:`~Terminal.split_seqs` and related functions failed to match when the color index
+ was greater than 15, :ghissue:`101`.
+ * bugfix: Context Managers, :meth:`~.Terminal.fullscreen`, :meth:`~.Terminal.hidden_cursor`, and
+ :meth:`~Terminal.keypad` now flush the stream after writing their sequences.
+ * bugfix: ``chr(127)``, ``\x7f`` has changed from keycode ``term.DELETE`` to the more common
+ match, ``term.BACKSPACE``, :ghissue:115` by :ghuser:`jwezel`.
+ * deprecated: the direct curses ``move()`` capability is no longer recommended, suggest to use
+ :meth:`~.Terminal.move_xy()`, which matches the return value of :meth:`~.Terminal.get_location`.
+ * deprecated: ``superscript``, ``subscript``, ``shadow``, and ``dim`` are no longer "compoundable"
+ with colors, such as in phrase ``Terminal.blue_subscript('a')``. These attributes are not
+ typically supported, anyway. Use Unicode text or 256 or 24-bit color codes instead.
+ * deprecated: additional key names, such as ``KEY_TAB``, are no longer "injected" into the curses
+ module namespace.
+ * deprecated: :func:`curses.setupterm` is now called with :attr:`os.devnull` as the file
+ descriptor, let us know if this causes any issues. :ghissue:`59`.
+ * deprecated: :meth:`~Terminal.inkey` no longer raises RuntimeError when :attr:`~Terminal.stream`
+ is not a terminal, programs using :meth:`~Terminal.inkey` to block indefinitely if a keyboard is
+ not attached. :ghissue:`69`.
+ * deprecated: using argument ``_intr_continue`` to method :meth:`~Terminal.kbhit`, behavior is as
+ though such value is always True since 1.9.
+
+1.16
+ * introduced: Windows support?! :ghissue:`110` by :ghuser:`avylove`.
+
+1.15
+ * enhancement: disable timing integration tests for keyboard routines.
+
+ They work perfectly fine for regression testing for contributing
+ developers, but people run our tests on build farms and open issues when
+ they fail. So we comment out these useful tests. :ghissue:`100`.
+ * enhancement: Support python 3.7. :ghissue:`102`.
+ * enhancement: Various fixes to test automation :ghissue:`108`
+
+1.14
+ * bugfix: :meth:`~.Terminal.wrap` misbehaved for text containing newlines,
+ :ghissue:`74`.
+ * bugfix: TypeError when using ``PYTHONOPTIMIZE=2`` environment variable,
+ :ghissue:`84`.
+ * bugfix: define ``blessed.__version__`` value,
+ :ghissue:`92`.
+ * bugfix: detect sequences ``\x1b[0K`` and ``\x1b2K``,
+ :ghissue:`95`.
+
+1.13
+ * enhancement: :meth:`~.Terminal.split_seqs` introduced, and 4x cost
+ reduction in related sequence-aware functions, :ghissue:`29`.
+ * deprecated: ``blessed.sequences.measure_length`` function superseded by
+ :func:`~.iter_parse` if necessary.
+ * deprecated: warnings about "binary-packed capabilities" are no longer
+ emitted on strange terminal types, making best effort.
+
+1.12
+ * enhancement: :meth:`~.Terminal.get_location` returns the ``(row, col)``
+ position of the cursor at the time of call for attached terminal.
+ * enhancement: a keyboard now detected as *stdin* when
+ :paramref:`~.Terminal.__init__.stream` is :obj:`sys.stderr`.
+
+1.11
+ * enhancement: :meth:`~.Terminal.inkey` can return more quickly for
+ combinations such as ``Alt + Z`` when ``MetaSendsEscape`` is enabled,
+ :ghissue:`30`.
+ * enhancement: :class:`~.FormattingString` may now be nested, such as
+ ``t.red('red', t.underline('rum'))``, :ghissue:`61`
+
+1.10
+ * workaround: provide ``sc`` and ``rc`` for Terminals of ``kind='ansi'``,
+ repairing :meth:`~.Terminal.location` :ghissue:`44`.
+ * bugfix: length of simple SGR reset sequence ``\x1b[m`` was not correctly
+ determined on all terminal types, :ghissue:`45`.
+ * deprecated: ``_intr_continue`` arguments introduced in 1.8 are now marked
+ deprecated in 1.10: beginning with python 3.5, the default behavior is as
+ though this argument is always True, `PEP-475
+ `_, blessed does the same.
+
+1.9
+ * enhancement: :paramref:`~.Terminal.wrap.break_long_words` now supported by
+ :meth:`Terminal.wrap`
+ * Ignore :class:`curses.error` message ``'tparm() returned NULL'``:
+ this occurs on win32 or other platforms using a limited curses
+ implementation, such as PDCurses_, where :func:`curses.tparm` is
+ not implemented, or no terminal capability database is available.
+ * Context manager :meth:`~.keypad` emits sequences that enable
+ "application keys" such as the diagonal keys on the numpad.
+ This is equivalent to :meth:`curses.window.keypad`.
+ * bugfix: translate keypad application keys correctly.
+ * enhancement: no longer depend on the '2to3' tool for python 3 support.
+ * enhancement: allow ``civis`` and ``cnorm`` (*hide_cursor*, *normal_hide*)
+ to work with terminal-type *ansi* by emulating support by proxy.
+ * enhancement: new public attribute: :attr:`~.kind`: the very same as given
+ :paramref:`Terminal.__init__.kind` keyword argument. Or, when not given,
+ determined by and equivalent to the ``TERM`` Environment variable.
+
+1.8
+ * enhancement: export keyboard-read function as public method ``getch()``,
+ so that it may be overridden by custom terminal implementers.
+ * enhancement: allow :meth:`~.inkey` and :meth:`~.kbhit` to return early
+ when interrupted by signal by passing argument ``_intr_continue=False``.
+ * enhancement: allow ``hpa`` and ``vpa`` (*move_x*, *move_y*) to work on
+ tmux(1) or screen(1) by emulating support by proxy.
+ * enhancement: add :meth:`~.Terminal.rstrip` and :meth:`~.Terminal.lstrip`,
+ strips both sequences and trailing or leading whitespace, respectively.
+ * enhancement: include wcwidth_ library support for
+ :meth:`~.Terminal.length`: the printable width of many kinds of CJK
+ (Chinese, Japanese, Korean) ideographs and various combining characters
+ may now be determined.
+ * enhancement: better support for detecting the length or sequences of
+ externally-generated *ecma-48* codes when using ``xterm`` or ``aixterm``.
+ * bugfix: when :func:`locale.getpreferredencoding` returns empty string or
+ an encoding that is not valid for ``codecs.getincrementaldecoder``,
+ fallback to ASCII and emit a warning.
+ * bugfix: ensure :class:`~.FormattingString` and
+ :class:`~.ParameterizingString` may be pickled.
+ * bugfix: allow `~.inkey` and related to be called without a keyboard.
+ * **change**: ``term.keyboard_fd`` is set ``None`` if ``stream`` or
+ ``sys.stdout`` is not a tty, making ``term.inkey()``, ``term.cbreak()``,
+ ``term.raw()``, no-op.
+ * bugfix: ``\x1bOH`` (KEY_HOME) was incorrectly mapped as KEY_LEFT.
+
+1.7
+ * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this
+ project was previously known as **blessings** version 1.6 and prior.
+ * introduced: context manager :meth:`~.cbreak`, which is equivalent to
+ entering terminal state by :func:`tty.setcbreak` and returning
+ on exit, as well as the lesser recommended :meth:`~.raw`,
+ pairing from :func:`tty.setraw`.
+ * introduced: :meth:`~.inkey`, which will return one or more characters
+ received by the keyboard as a unicode sequence, with additional attributes
+ :attr:`~.Keystroke.code` and :attr:`~.Keystroke.name`. This allows
+ application keys (such as the up arrow, or home key) to be detected.
+ Optional value :paramref:`~.inkey.timeout` allows for timed poll.
+ * introduced: :meth:`~.Terminal.center`, :meth:`~.Terminal.rjust`,
+ :meth:`~.Terminal.ljust`, allowing text containing sequences to be aligned
+ to detected horizontal screen width, or by
+ :paramref:`~.Terminal.center.width` specified.
+ * introduced: :meth:`~.wrap` method. Allows text containing sequences to be
+ word-wrapped without breaking mid-sequence, honoring their printable width.
+ * introduced: :meth:`~.Terminal.strip`, strips all sequences *and*
+ whitespace.
+ * introduced: :meth:`~.Terminal.strip_seqs` strip only sequences.
+ * introduced: :meth:`~.Terminal.rstrip` and :meth:`~.Terminal.lstrip` strips
+ both sequences and trailing or leading whitespace, respectively.
+ * bugfix: cannot call :func:`curses.setupterm` more than once per process
+ (from :meth:`Terminal.__init__`): Previously, blessed pretended
+ to support several instances of different Terminal :attr:`~.kind`, but was
+ actually using the :attr:`~.kind` specified by the first instantiation of
+ :class:`~.Terminal`. A warning is now issued. Although this is
+ misbehavior is still allowed, a :class:`warnings.WarningMessage` is now
+ emitted to notify about subsequent terminal misbehavior.
+ * bugfix: resolved issue where :attr:`~.number_of_colors` fails when
+ :attr:`~.does_styling` is ``False``. Resolves issue where piping tests
+ output would fail.
+ * bugfix: warn and set :attr:`~.does_styling` to ``False`` when the given
+ :attr:`~.kind` is not found in the terminal capability database.
+ * bugfix: allow unsupported terminal capabilities to be callable just as
+ supported capabilities, so that the return value of
+ :attr:`~.color`\(n) may be called on terminals without color
+ capabilities.
+ * bugfix: for terminals without underline, such as vt220,
+ ``term.underline('text')`` would emit ``'text' + term.normal``.
+ Now it emits only ``'text'``.
+ * enhancement: some attributes are now properties, raise exceptions when
+ assigned.
+ * enhancement: pypy is now a supported python platform implementation.
+ * enhancement: removed pokemon ``curses.error`` exceptions.
+ * enhancement: do not ignore :class:`curses.error` exceptions, unhandled
+ curses errors are legitimate errors and should be reported as a bug.
+ * enhancement: converted nose tests to pytest, merged travis and tox.
+ * enhancement: pytest fixtures, paired with a new ``@as_subprocess``
+ decorator
+ are used to test a multitude of terminal types.
+ * enhancement: test accessories ``@as_subprocess`` resolves various issues
+ with different terminal types that previously went untested.
+ * deprecation: python2.5 is no longer supported (as tox does not supported).
+
+1.6
+ * Add :attr:`~.does_styling`. This takes :attr:`~.force_styling`
+ into account and should replace most uses of :attr:`~.is_a_tty`.
+ * Make :attr:`~.is_a_tty` a read-only property like :attr:`~.does_styling`.
+ Writing to it never would have done anything constructive.
+ * Add :meth:`~.fullscreen`` and :meth:`hidden_cursor` to the
+ auto-generated docs.
+
+1.5.1
+ * Clean up fabfile, removing the redundant ``test`` command.
+ * Add Travis support.
+ * Make ``python setup.py test`` work without spurious errors on 2.6.
+ * Work around a tox parsing bug in its config file.
+ * Make context managers clean up after themselves even if there's an
+ exception (Vitja Makarov :ghpull:`29`).
+ * Parameterizing a capability no longer crashes when there is no tty
+ (Vitja Makarov :ghpull:`31`)
+
+1.5
+ * Add syntactic sugar and documentation for ``enter_fullscreen``
+ and ``exit_fullscreen``.
+ * Add context managers :meth:`~.fullscreen` and :meth:`~.hidden_cursor`.
+ * Now you can force a :class:`~.Terminal` to never to emit styles by
+ passing keyword argument ``force_styling=None``.
+
+1.4
+ * Add syntactic sugar for cursor visibility control and single-space-movement
+ capabilities.
+ * Endorse the :meth:`~.location` context manager for restoring cursor
+ position after a series of manual movements.
+ * Fix a bug in which :meth:`~.location` that wouldn't do anything when
+ passed zeros.
+ * Allow tests to be run with ``python setup.py test``.
+
+1.3
+ * Added :attr:`~.number_of_colors`, which tells you how many colors the
+ terminal supports.
+ * Made :attr:`~.color`\(n) and :attr:`~.on_color`\(n) callable to wrap a
+ string, like the named colors can. Also, make them both fall back to the
+ ``setf`` and ``setb`` capabilities (like the named colors do) if the
+ termcap entries for ``setaf`` and ``setab`` are not available.
+ * Allowed :attr:`~.color` to act as an unparametrized string, not just a
+ callable.
+ * Made :attr:`~.height` and :attr:`~.width` examine any passed-in stream
+ before falling back to stdout (This rarely if ever affects actual behavior;
+ it's mostly philosophical).
+ * Made caching simpler and slightly more efficient.
+ * Got rid of a reference cycle between :class:`~.Terminal` and
+ :class:`~.FormattingString`.
+ * Updated docs to reflect that terminal addressing (as in :meth:`~location`)
+ is 0-based.
+
+1.2
+ * Added support for Python 3! We need 3.2.3 or greater, because the curses
+ library couldn't decide whether to accept strs or bytes before that
+ (http://bugs.python.org/issue10570).
+ * Everything that comes out of the library is now unicode. This lets us
+ support Python 3 without making a mess of the code, and Python 2 should
+ continue to work unless you were testing types (and badly). Please file a
+ bug if this causes trouble for you.
+ * Changed to the MIT License for better world domination.
+ * Added Sphinx docs.
+
+1.1
+ * Added nicely named attributes for colors.
+ * Introduced compound formatting.
+ * Added wrapper behavior for styling and colors.
+ * Let you force capabilities to be non-empty, even if the output stream is
+ not a terminal.
+ * Added :attr:`~.is_a_tty` to determine whether the output stream is a
+ terminal.
+ * Sugared the remaining interesting string capabilities.
+ * Allow :meth:`~.location` to operate on just an x *or* y coordinate.
+
+1.0
+ * Extracted Blessed from `nose-progressive`_.
+
+
+.. _`nose-progressive`: http://pypi.python.org/pypi/nose-progressive/
+.. _`erikrose/blessings`: https://github.com/erikrose/blessings
+.. _`jquast/blessed`: https://github.com/jquast/blessed
+.. _`issue tracker`: https://github.com/jquast/blessed/issues/
+.. _curses: https://docs.python.org/library/curses.html
+.. _couleur: https://pypi.python.org/pypi/couleur
+.. _colorama: https://pypi.python.org/pypi/colorama
+.. _wcwidth: https://pypi.python.org/pypi/wcwidth
+.. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3
+.. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=curs_getch&apropos=0&sektion=3
+.. _`termios(4)`: http://www.openbsd.org/cgi-bin/man.cgi?query=termios&apropos=0&sektion=4
+.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5
+.. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi?query=tigetstr&sektion=3
+.. _tparm: http://www.openbsd.org/cgi-bin/man.cgi?query=tparm&sektion=3
+.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH
+.. _`API Documentation`: http://blessed.rtfd.org
+.. _`PDCurses`: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses
+.. _`ansi`: https://github.com/tehmaze/ansi
diff --git a/docs/index.rst b/docs/index.rst
index 63ee5be9..b0ee657d 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,20 +1,25 @@
-=======================
-Blessings API Reference
-=======================
+=================================
+Welcome to Blessed documentation!
+=================================
-Read The Readme First
-=====================
+Contents:
-This is the API documentation for the Blessings terminal library. Because
-Blessings uses quite a bit of dynamism, you should `read the readme first`_, as
-not all the useful stuff shows up in these autogenerated docs. However, if
-you're looking for seldom-used keyword args or a peek at the internals, you're
-in the right place.
+.. toctree::
+ :maxdepth: 3
+ :glob:
-.. _`read the readme first`: http://pypi.python.org/pypi/blessings/
+ intro
+ overview
+ examples
+ further
+ pains
+ api
+ contributing
+ history
-Then Read This If You Want
-==========================
+=======
+Indexes
+=======
-.. autoclass:: blessings.Terminal
- :members: __init__, __getattr__, location, height, width, color, on_color, number_of_colors, fullscreen, hidden_cursor
+* :ref:`genindex`
+* :ref:`modindex`
diff --git a/docs/intro.rst b/docs/intro.rst
new file mode 100644
index 00000000..e1275137
--- /dev/null
+++ b/docs/intro.rst
@@ -0,0 +1,224 @@
+| |docs| |travis| |codecov|
+| |pypi| |downloads| |gitter|
+| |linux| |windows| |mac| |bsd|
+
+.. |docs| image:: https://img.shields.io/readthedocs/blessed.svg?logo=read-the-docs
+ :target: https://blessed.readthedocs.org
+ :alt: Documentation Status
+
+.. |travis| image:: https://img.shields.io/travis/jquast/blessed/master.svg?logo=travis
+ :alt: Travis Continuous Integration
+ :target: https://travis-ci.org/jquast/blessed/
+
+.. |codecov| image:: https://codecov.io/gh/jquast/blessed/branch/master/graph/badge.svg
+ :alt: codecov.io Code Coverage
+ :target: https://codecov.io/gh/jquast/blessed
+
+.. |pypi| image:: https://img.shields.io/pypi/v/blessed.svg?logo=pypi
+ :alt: Latest Version
+ :target: https://pypi.python.org/pypi/blessed
+
+.. |downloads| image:: https://img.shields.io/pypi/dm/blessed.svg?logo=pypi
+ :alt: Downloads
+ :target: https://pypi.python.org/pypi/blessed
+
+.. |gitter| image:: https://img.shields.io/badge/gitter-Join%20Chat-mediumaquamarine?logo=gitter
+ :alt: Join Chat
+ :target: https://gitter.im/jquast/blessed
+
+.. |linux| image:: https://img.shields.io/badge/Linux-yes-success?logo=linux
+ :alt: Linux supported
+ :target: https://pypi.python.org/pypi/blessed
+
+.. |windows| image:: https://img.shields.io/badge/Windows-NEW-success?logo=windows
+ :alt: Windows supported
+ :target: https://pypi.python.org/pypi/blessed
+
+.. |mac| image:: https://img.shields.io/badge/MacOS-yes-success?logo=apple
+ :alt: MacOS supported
+ :target: https://pypi.python.org/pypi/blessed
+
+.. |bsd| image:: https://img.shields.io/badge/BSD-yes-success?logo=freebsd
+ :alt: BSD supported
+ :target: https://pypi.python.org/pypi/blessed
+
+Introduction
+============
+
+Blessed is a thin, practical wrapper around terminal capabilities in Python.
+
+Coding with *Blessed* looks like this...
+
+.. code-block:: python
+
+ from blessed import Terminal
+
+ t = Terminal()
+
+ print(t.bold('Hi there!'))
+ print(t.bold_red_on_bright_green('It hurts my eyes!'))
+
+ with t.location(0, t.height - 1):
+ print(t.center(t.blink('press any key to continue.')))
+
+ with t.cbreak():
+ inp = t.inkey()
+ print('You pressed ' + repr(inp))
+
+
+Brief Overview
+--------------
+
+*Blessed* is a more simplified wrapper around curses_, providing :
+
+* Styles, color, and maybe a little positioning without necessarily
+ clearing the whole screen first.
+* Works great with standard Python string formatting.
+* Provides up-to-the-moment terminal height and width, so you can respond to
+ terminal size changes.
+* Avoids making a mess if the output gets piped to a non-terminal:
+ outputs to any file-like object such as *StringIO*, files, or pipes.
+* Uses the `terminfo(5)`_ database so it works with any terminal type
+ and supports any terminal capability: No more C-like calls to tigetstr_
+ and tparm_.
+* Keeps a minimum of internal state, so you can feel free to mix and match with
+ calls to curses or whatever other terminal libraries you like.
+* Provides plenty of context managers to safely express terminal modes,
+ automatically restoring the terminal to a safe state on exit.
+* Act intelligently when somebody redirects your output to a file, omitting
+ all of the terminal sequences such as styling, colors, or positioning.
+* Dead-simple keyboard handling: safely decoding unicode input in your
+ system's preferred locale and supports application/arrow keys.
+* Allows the printable length of strings containing sequences to be
+ determined.
+
+Before And After
+----------------
+
+With the built-in curses_ module, this is how you would typically
+print some underlined text at the bottom of the screen:
+
+.. code-block:: python
+
+ from curses import tigetstr, setupterm, tparm
+ from fcntl import ioctl
+ from os import isatty
+ import struct
+ import sys
+ from termios import TIOCGWINSZ
+
+ # If we want to tolerate having our output piped to other commands or
+ # files without crashing, we need to do all this branching:
+ if hasattr(sys.stdout, 'fileno') and isatty(sys.stdout.fileno()):
+ setupterm()
+ sc = tigetstr('sc')
+ cup = tigetstr('cup')
+ rc = tigetstr('rc')
+ underline = tigetstr('smul')
+ normal = tigetstr('sgr0')
+ else:
+ sc = cup = rc = underline = normal = ''
+
+ # Save cursor position.
+ print(sc)
+
+ if cup:
+ # tigetnum('lines') doesn't always update promptly, hence this:
+ height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0]
+
+ # Move cursor to bottom.
+ print(tparm(cup, height - 1, 0))
+
+ print('This is {under}underlined{normal}!'
+ .format(under=underline, normal=normal))
+
+ # Restore cursor position.
+ print(rc)
+
+The same program with *Blessed* is simply:
+
+.. code-block:: python
+
+ from blessed import Terminal
+
+ term = Terminal()
+ with term.location(0, term.height - 1):
+ print('This is' + term.underline('underlined') + '!')
+
+Requirements
+------------
+
+*Blessed* is tested with Python 2.7, 3.4, 3.5, 3.6, and 3.7 on Linux, Mac, and
+FreeBSD. Windows support was just added in October 2019, give it a try, and
+please report any strange issues!
+
+Further Documentation
+---------------------
+
+More documentation can be found at http://blessed.readthedocs.org/en/latest/
+
+Bugs, Contributing, Support
+---------------------------
+
+**Bugs** or suggestions? Visit the `issue tracker`_ and file an issue.
+We welcome your bug reports and feature suggestions!
+
+Would you like to **contribute**? That's awesome! We've written a
+`guide `_
+to help you.
+
+Are you stuck and need **support**? Give `stackoverflow`_ a try. If
+you're still having trouble, we'd like to hear about it! Open an issue
+in the `issue tracker`_ with a well-formed question.
+
+License
+-------
+
+*Blessed* is under the MIT License. See the LICENSE file.
+
+Forked
+------
+
+*Blessed* is a fork of `blessings `_.
+Changes since 1.7 have all been proposed but unaccepted upstream.
+
+Enhancements only in *Blessed*:
+ * 24-bit color support with `Terminal.color_rgb()`_ and `Terminal.on_color_rgb()`_ methods
+ * X11 color name attributes
+ * Windows support
+ * `Terminal.length()`_ to determine printable length of text containing sequences
+ * `Terminal.strip()`_, `Terminal.rstrip()`_, `Terminal.lstrip()`_,
+ and `Terminal.strip_seqs()`_ for removing sequences from text
+ * `Terminal.wrap()`_ for wrapping text containing sequences at a specified width
+ * `Terminal.center()`_, `Terminal.rjust()`_, and `Terminal.ljust()`_
+ for alignment of text containing sequences
+ * `Terminal.cbreak()`_ and `Terminal.raw()`_ context managers for keyboard input
+ * `Terminal.inkey()`_ for keyboard event detection
+
+Furthermore, a project in the node.js language of the `same name
+`_ is **not** related, or a fork
+of each other in any way.
+
+.. _`issue tracker`: https://github.com/jquast/blessed/issues/
+.. _curses: https://docs.python.org/3/library/curses.html
+.. _tigetstr: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/tigetstr.3
+.. _tparm: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/tparm.3
+.. _ansi: https://github.com/tehmaze/ansi
+.. _colorama: https://pypi.python.org/pypi/colorama
+.. _PDCurses: http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses
+.. _`terminfo(5)`: http://invisible-island.net/ncurses/man/terminfo.5.html
+.. _`stackoverflow`: http://stackoverflow.com/
+.. _`Terminal.color_rgb()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.color_rgb
+.. _`Terminal.on_color_rgb()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.on_color_rgb
+.. _`Terminal.length()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.length
+.. _`Terminal.strip()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.strip
+.. _`Terminal.rstrip()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.rstrip
+.. _`Terminal.lstrip()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.lstrip
+.. _`Terminal.strip_seqs()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.strip_seqs
+.. _`Terminal.wrap()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.wrap
+.. _`Terminal.center()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.center
+.. _`Terminal.rjust()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.rjust
+.. _`Terminal.ljust()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.ljust
+.. _`Terminal.cbreak()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.cbreak
+.. _`Terminal.raw()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.raw
+.. _`Terminal.inkey()`: https://blessed.readthedocs.io/en/stable/api.html#blessed.terminal.Terminal.inkey
diff --git a/docs/make.bat b/docs/make.bat
deleted file mode 100644
index c24653f0..00000000
--- a/docs/make.bat
+++ /dev/null
@@ -1,170 +0,0 @@
-@ECHO OFF
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
- set SPHINXBUILD=sphinx-build
-)
-set BUILDDIR=_build
-set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
-if NOT "%PAPER%" == "" (
- set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
-)
-
-if "%1" == "" goto help
-
-if "%1" == "help" (
- :help
- echo.Please use `make ^` where ^ is one of
- echo. html to make standalone HTML files
- echo. dirhtml to make HTML files named index.html in directories
- echo. singlehtml to make a single large HTML file
- echo. pickle to make pickle files
- echo. json to make JSON files
- echo. htmlhelp to make HTML files and a HTML help project
- echo. qthelp to make HTML files and a qthelp project
- echo. devhelp to make HTML files and a Devhelp project
- echo. epub to make an epub
- echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
- echo. text to make text files
- echo. man to make manual pages
- echo. changes to make an overview over all changed/added/deprecated items
- echo. linkcheck to check all external links for integrity
- echo. doctest to run all doctests embedded in the documentation if enabled
- goto end
-)
-
-if "%1" == "clean" (
- for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
- del /q /s %BUILDDIR%\*
- goto end
-)
-
-if "%1" == "html" (
- %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The HTML pages are in %BUILDDIR%/html.
- goto end
-)
-
-if "%1" == "dirhtml" (
- %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
- goto end
-)
-
-if "%1" == "singlehtml" (
- %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
- goto end
-)
-
-if "%1" == "pickle" (
- %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can process the pickle files.
- goto end
-)
-
-if "%1" == "json" (
- %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can process the JSON files.
- goto end
-)
-
-if "%1" == "htmlhelp" (
- %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can run HTML Help Workshop with the ^
-.hhp project file in %BUILDDIR%/htmlhelp.
- goto end
-)
-
-if "%1" == "qthelp" (
- %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can run "qcollectiongenerator" with the ^
-.qhcp project file in %BUILDDIR%/qthelp, like this:
- echo.^> qcollectiongenerator %BUILDDIR%\qthelp\blessings.qhcp
- echo.To view the help file:
- echo.^> assistant -collectionFile %BUILDDIR%\qthelp\blessings.ghc
- goto end
-)
-
-if "%1" == "devhelp" (
- %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished.
- goto end
-)
-
-if "%1" == "epub" (
- %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The epub file is in %BUILDDIR%/epub.
- goto end
-)
-
-if "%1" == "latex" (
- %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
- goto end
-)
-
-if "%1" == "text" (
- %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The text files are in %BUILDDIR%/text.
- goto end
-)
-
-if "%1" == "man" (
- %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The manual pages are in %BUILDDIR%/man.
- goto end
-)
-
-if "%1" == "changes" (
- %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
- if errorlevel 1 exit /b 1
- echo.
- echo.The overview file is in %BUILDDIR%/changes.
- goto end
-)
-
-if "%1" == "linkcheck" (
- %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
- if errorlevel 1 exit /b 1
- echo.
- echo.Link check complete; look for any errors in the above output ^
-or in %BUILDDIR%/linkcheck/output.txt.
- goto end
-)
-
-if "%1" == "doctest" (
- %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
- if errorlevel 1 exit /b 1
- echo.
- echo.Testing of doctests in the sources finished, look at the ^
-results in %BUILDDIR%/doctest/output.txt.
- goto end
-)
-
-:end
diff --git a/docs/overview.rst b/docs/overview.rst
new file mode 100644
index 00000000..4e85fb90
--- /dev/null
+++ b/docs/overview.rst
@@ -0,0 +1,574 @@
+Overview
+========
+
+Blessed provides just **one** top-level object: :class:`~.Terminal`.
+Instantiating a :class:`~.Terminal` figures out whether you're on a terminal at
+all and, if so, does any necessary setup:
+
+ >>> term = Terminal()
+
+After that, you can proceed to ask it all sorts of things about the terminal,
+such as its size:
+
+ >>> term.height, term.width
+ (34, 102)
+
+Its color support:
+
+ >>> term.number_of_colors
+ 256
+
+And use construct strings containing color and styling:
+
+ >>> term.green_reverse('ALL SYSTEMS GO')
+ '\x1b[32m\x1b[7mALL SYSTEMS GO\x1b[m'
+
+Furthermore, the special sequences inserted with application keys
+(arrow and function keys) are understood and decoded, as well as your
+locale-specific encoded multibyte input, such as utf-8 characters.
+
+
+Styling and Formatting
+----------------------
+
+Lots of handy formatting codes are available as attributes on a
+:class:`~.Terminal` class instance. For example::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ print('I am ' + term.bold + 'bold' + term.normal + '!')
+
+These capabilities (*bold*, *normal*) are translated to their sequences, which
+when displayed simply change the video attributes. And, when used as a
+callable, automatically wraps the given string with this sequence, and
+terminates it with *normal*.
+
+The same can be written as::
+
+ print('I am' + term.bold('bold') + '!')
+
+You may also use the :class:`~.Terminal` instance as an argument for
+the :meth:`str.format`` method, so that capabilities can be displayed in-line
+for more complex strings::
+
+ print('{t.red_on_yellow}Candy corn{t.normal} for everyone!'.format(t=term))
+
+
+Capabilities
+~~~~~~~~~~~~
+
+Capabilities supported by most terminals are:
+
+``bold``
+ Turn on 'extra bright' mode.
+``reverse``
+ Switch fore and background attributes.
+``blink``
+ Turn on blinking.
+``normal``
+ Reset attributes to default.
+``underline``
+ Enable underline mode.
+
+Note that, while the inverse of *underline* is *no_underline*, the only way
+to turn off *bold* or *reverse* is *normal*, which also cancels any custom
+colors.
+
+Many of these are aliases, their true capability names (such as 'smul' for
+'begin underline mode') may still be used. Any capability in the `terminfo(5)`_
+manual, under column **Cap-name**, may be used as an attribute of a
+:class:`~.Terminal` instance. If it is not a supported capability for the
+current terminal, or a non-tty is used as output stream, an empty string is
+always returned.
+
+Colors
+~~~~~~
+
+XXX TODO XXX
+
+all X11 colors are available,
+rgb(int, int, int),
+truecolor is automatically detected,
+or, downsampled to 256 or 16, 8, etc colorspace.
+
+Color terminals are capable of at least 8 basic colors.
+
+* ``black``
+* ``red``
+* ``green``
+* ``yellow``
+* ``blue``
+* ``magenta``
+* ``cyan``
+* ``white``
+
+The same colors, prefixed with *bright_* (synonymous with *bold_*),
+such as *bright_blue*, provides 16 colors in total.
+
+Prefixed with *on_*, the given color is used as the background color.
+Some terminals also provide an additional 8 high-intensity versions using
+*on_bright*, some example compound formats::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ print(term.on_bright_blue('Blue skies!'))
+
+ print(term.bright_red_on_bright_yellow('Pepperoni Pizza!'))
+
+You may also specify the :meth:`~.Terminal.color` index by number, which
+should be within the bounds of value returned by
+:attr:`~.Terminal.number_of_colors`::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ for idx in range(term.number_of_colors):
+ print(term.color(idx)('Color {0}'.format(idx)))
+
+You can check whether the terminal definition used supports colors, and how
+many, using the :attr:`~.Terminal.number_of_colors` property, which returns
+any of *0*, *8* or *256* for terminal types such as *vt220*, *ansi*, and
+*xterm-256color*, respectively.
+
+Colorless Terminals
+~~~~~~~~~~~~~~~~~~~
+
+If the terminal defined by the Environment variable **TERM** does not support
+colors, these simply return empty strings. When used as a callable, the string
+passed as an argument is returned as-is. Most sequences emitted to a terminal
+that does not support them are usually harmless and have no effect.
+
+Colorless terminals (such as the amber or green monochrome *vt220*) do not
+support colors but do support reverse video. For this reason, it may be
+desirable in some applications to simply select a foreground color, followed
+by reverse video to achieve the desired background color effect::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ print(term.green_reverse('some terminals standout more than others'))
+
+Which appears as *black on green* on color terminals, but *black text
+on amber or green* on monochrome terminals. Whereas the more declarative
+formatter *black_on_green* would remain colorless.
+
+.. note:: On most color terminals, *bright_black* is not invisible -- it is
+ actually a very dark shade of grey!
+
+Compound Formatting
+~~~~~~~~~~~~~~~~~~~
+
+If you want to do lots of crazy formatting all at once, you can just mash it
+all together::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ print(term.bold_underline_green_on_yellow('Woo'))
+
+I'd be remiss if I didn't credit couleur_, where I probably got the idea for
+all this mashing.
+
+This compound notation comes in handy if you want to allow users to customize
+formatting, just allow compound formatters, like *bold_green*, as a command
+line argument or configuration item such as in the :ref:`tprint.py`
+demonstration script.
+
+Moving The Cursor
+-----------------
+
+If you just want to move and aren't worried about returning, do something like
+this::
+
+ from blessed import Terminal
+
+ term = Terminal()
+ print(term.move_xy(10, 1) + 'Hi, mom!')
+
+There are three basic movement capabilities:
+
+``move_xy(x, y)``
+ Position cursor at given **x**, **y**.
+``move_x(x)``
+ Position cursor at column **x**.
+``move_y(y)``
+ Position cursor at row **y**.
+
+A context manager, :meth:`~.Terminal.location` is provided to move the cursor
+to an *(x, y)* screen position and restore the previous position upon exit::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ with term.location(0, term.height - 1):
+ print('Here is the bottom.')
+
+ print('This is back where I came from.')
+
+Parameters to :meth:`~.Terminal.location` are the **optional** *x* and/or *y*
+keyword arguments::
+
+ with term.location(y=10):
+ print('We changed just the row.')
+
+When omitted, it saves the current cursor position, and restore it upon exit::
+
+ with term.location():
+ print(term.move_xy(1, 1) + 'Hi')
+ print(term.move_xy(9, 9) + 'Mom')
+
+.. note:: calls to :meth:`~.Terminal.location` may not be nested.
+
+Finding The Cursor
+------------------
+
+We can determine the cursor's current position at anytime using
+:meth:`~.get_location`, returning the current (y, x) location. This uses a
+kind of "answer back" sequence that your terminal emulator responds to. If
+the terminal may not respond, the :paramref:`~.get_location.timeout` keyword
+argument can be specified to return coordinates (-1, -1) after a blocking
+timeout::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ row, col = term.get_location(timeout=5)
+
+ if row < term.height:
+ print(term.move_y(term.height) + 'Get down there!')
+
+One-Notch Movement
+~~~~~~~~~~~~~~~~~~
+
+Finally, there are some parameterless movement capabilities that move the
+cursor one character in various directions:
+
+* ``move_left``
+* ``move_right``
+* ``move_up``
+* ``move_down``
+
+.. note:: *move_down* is often valued as *\\n*, which additionally returns
+ the carriage to column 0, depending on your terminal emulator, and may
+ also destructively destroy any characters at the given position to the
+ end of margin.
+
+
+Height And Width
+----------------
+
+Use the :attr:`~.Terminal.height` and :attr:`~.Terminal.width` properties to
+determine the size of the window::
+
+ from blessed import Terminal
+
+ term = Terminal()
+ height, width = term.height, term.width
+ with term.location(x=term.width / 3, y=term.height / 3):
+ print('1/3 ways in!')
+
+These values are always current. To detect when the size of the window
+changes, you may author a callback for SIGWINCH_ signals::
+
+ import signal
+ from blessed import Terminal
+
+ term = Terminal()
+
+ def on_resize(sig, action):
+ print('height={t.height}, width={t.width}'.format(t=term))
+
+ signal.signal(signal.SIGWINCH, on_resize)
+
+ # wait for keypress
+ term.inkey()
+
+
+Clearing The Screen
+-------------------
+
+Blessed provides syntactic sugar over some screen-clearing capabilities:
+
+``clear``
+ Clear the whole screen.
+``clear_eol``
+ Clear to the end of the line.
+``clear_bol``
+ Clear backward to the beginning of the line.
+``clear_eos``
+ Clear to the end of screen.
+
+
+Full-Screen Mode
+----------------
+
+If you've ever noticed a program, such as an editor, restores the previous
+screen (such as your shell prompt) after exiting, you're seeing the
+*enter_fullscreen* and *exit_fullscreen* attributes in effect.
+
+``enter_fullscreen``
+ Switch to alternate screen, previous screen is stored by terminal driver.
+``exit_fullscreen``
+ Switch back to standard screen, restoring the same terminal state.
+
+There's also a context manager you can use as a shortcut::
+
+ from __future__ import division
+ from blessed import Terminal
+
+ term = Terminal()
+ with term.fullscreen():
+ print(term.move_y(term.height // 2) +
+ term.center('press any key').rstrip())
+ term.inkey()
+
+
+Pipe Savvy
+----------
+
+If your program isn't attached to a terminal, such as piped to a program
+like *less(1)* or redirected to a file, all the capability attributes on
+:class:`~.Terminal` will return empty strings. You'll get a nice-looking
+file without any formatting codes gumming up the works.
+
+If you want to override this, such as when piping output to *less -r*, pass
+argument value *True* to the :paramref:`~.Terminal.force_styling` parameter.
+
+In any case, there is a :attr:`~.Terminal.does_styling` attribute that lets
+you see whether the terminal attached to the output stream is capable of
+formatting. If it is *False*, you may refrain from drawing progress
+bars and other frippery and just stick to content::
+
+ from blessed import Terminal
+
+ term = Terminal()
+ if term.does_styling:
+ with term.location(x=0, y=term.height - 1):
+ print('Progress: [=======> ]')
+ print(term.bold("60%"))
+
+
+Sequence Awareness
+------------------
+
+Blessed may measure the printable width of strings containing sequences,
+providing :meth:`~.Terminal.center`, :meth:`~.Terminal.ljust`, and
+:meth:`~.Terminal.rjust` methods, using the terminal screen's width as
+the default *width* value::
+
+ from __future__ import division
+ from blessed import Terminal
+
+ term = Terminal()
+ with term.location(y=term.height // 2):
+ print(term.center(term.bold('bold and centered')))
+
+Any string containing sequences may have its printable length measured using
+the :meth:`~.Terminal.length` method.
+
+Additionally, a sequence-aware version of :func:`textwrap.wrap` is supplied as
+class as method :meth:`~.Terminal.wrap` that is also sequence-aware, so now you
+may word-wrap strings containing sequences. The following example displays a
+poem word-wrapped to 25 columns::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ poem = (term.bold_cyan('Plan difficult tasks'),
+ term.cyan('through the simplest tasks'),
+ term.bold_cyan('Achieve large tasks'),
+ term.cyan('through the smallest tasks'))
+
+ for line in poem:
+ print('\n'.join(term.wrap(line, width=25, subsequent_indent=' ' * 4)))
+
+Sometimes it is necessary to make sense of sequences, and to distinguish them
+from plain text. The :meth:`~.Terminal.split_seqs` method can allow us to
+iterate over a terminal string by its characters or sequences::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ phrase = term.bold('bbq')
+ print(term.split_seqs(phrase))
+
+Will display something like, ``['\x1b[1m', 'b', 'b', 'q', '\x1b(B', '\x1b[m']``
+
+Similarly, the method :meth:`~.Terminal.strip_seqs` may be used on a string to
+remove all occurrences of terminal sequences::
+
+ from blessed import Terminal
+
+ term = Terminal()
+ phrase = term.bold_black('coffee')
+ print(repr(term.strip_seqs(phrase)))
+
+Will display only ``'coffee'``
+
+Keyboard Input
+--------------
+
+The built-in python function :func:`raw_input` does not return a value until
+the return key is pressed, and is not suitable for detecting each individual
+keypress, much less arrow or function keys.
+
+Furthermore, when calling :func:`os.read` on input stream, only bytes are
+received, which must be decoded to unicode using the locale-preferred encoding.
+Finally, multiple bytes may be emitted which must be paired with some verb like
+``KEY_LEFT``: blessed handles all of these special cases for you!
+
+cbreak
+~~~~~~
+
+The context manager :meth:`~.Terminal.cbreak` can be used to enter
+*key-at-a-time* mode: Any keypress by the user is immediately consumed by read
+calls::
+
+ from blessed import Terminal
+ import sys
+
+ term = Terminal()
+
+ with term.cbreak():
+ # block until any single key is pressed.
+ sys.stdin.read(1)
+
+The mode entered using :meth:`~.Terminal.cbreak` is called
+`cbreak(3)`_ in curses:
+
+ The cbreak routine disables line buffering and erase/kill
+ character-processing (interrupt and flow control characters are unaffected),
+ making characters typed by the user immediately available to the program.
+
+raw
+~~~
+
+:meth:`~.Terminal.raw` is similar to cbreak, except that control-C and
+other keystrokes are "ignored", and received as their keystroke value
+rather than interrupting the program with signals.
+
+Output processing is also disabled, you must print phrases with carriage
+return after newline. Without raw mode::
+
+ print("hello, world.")
+
+With raw mode::
+
+ print("hello, world.", endl="\r\n")
+
+inkey
+~~~~~
+
+The method :meth:`~.Terminal.inkey` combined with cbreak_
+completes the circle of providing key-at-a-time keyboard input with multibyte
+encoding and awareness of application keys.
+
+:meth:`~.Terminal.inkey` resolves many issues with terminal input by
+returning a unicode-derived :class:`~.Keystroke` instance. Its return value
+may be printed, joined with, or compared like any other unicode strings, it
+also provides the special attributes :attr:`~.Keystroke.is_sequence`,
+:attr:`~.Keystroke.code`, and :attr:`~.Keystroke.name`::
+
+ from blessed import Terminal
+
+ term = Terminal()
+
+ print("press 'q' to quit.")
+ with term.cbreak():
+ val = ''
+ while val.lower() != 'q':
+ val = term.inkey(timeout=5)
+ if not val:
+ # timeout
+ print("It sure is quiet in here ...")
+ elif val.is_sequence:
+ print("got sequence: {0}.".format((str(val), val.name, val.code)))
+ elif val:
+ print("got {0}.".format(val))
+ print('bye!')
+
+Its output might appear as::
+
+ got sequence: ('\x1b[A', 'KEY_UP', 259).
+ got sequence: ('\x1b[1;2A', 'KEY_SUP', 337).
+ got sequence: ('\x1b[17~', 'KEY_F6', 270).
+ got sequence: ('\x1b', 'KEY_ESCAPE', 361).
+ got sequence: ('\n', 'KEY_ENTER', 343).
+ got /.
+ It sure is quiet in here ...
+ got sequence: ('\x1bOP', 'KEY_F1', 265).
+ It sure is quiet in here ...
+ got q.
+ bye!
+
+A :paramref:`~.Terminal.inkey.timeout` value of *None* (default) will block
+forever until a keypress is received. Any other value specifies the length of
+time to poll for input: if no input is received after the given time has
+elapsed, an empty string is returned. A :paramref:`~.Terminal.inkey.timeout`
+value of *0* is non-blocking.
+
+keyboard codes
+~~~~~~~~~~~~~~
+
+When the :attr:`~.Keystroke.is_sequence` property tests *True*, the value
+is a special application key of the keyboard. The :attr:`~.Keystroke.code`
+attribute may then be compared with attributes of :class:`~.Terminal`,
+which are duplicated from those found in `curs_getch(3)`_, or those
+`constants `_
+in :mod:`curses` beginning with phrase *KEY_*.
+
+Some of these mnemonics are shorthand or predate modern PC terms and
+are difficult to recall. The following helpful aliases are provided
+instead:
+
+=================== ============= ====================
+blessed curses note
+=================== ============= ====================
+``KEY_DELETE`` ``KEY_DC`` chr(127).
+``KEY_TAB`` chr(9)
+``KEY_INSERT`` ``KEY_IC``
+``KEY_PGUP`` ``KEY_PPAGE``
+``KEY_PGDOWN`` ``KEY_NPAGE``
+``KEY_ESCAPE`` ``KEY_EXIT``
+``KEY_SUP`` ``KEY_SR`` (shift + up)
+``KEY_SDOWN`` ``KEY_SF`` (shift + down)
+``KEY_DOWN_LEFT`` ``KEY_C1`` (keypad lower-left)
+``KEY_UP_RIGHT`` ``KEY_A1`` (keypad upper-left)
+``KEY_DOWN_RIGHT`` ``KEY_C3`` (keypad lower-left)
+``KEY_UP_RIGHT`` ``KEY_A3`` (keypad lower-right)
+``KEY_CENTER`` ``KEY_B2`` (keypad center)
+``KEY_BEGIN`` ``KEY_BEG``
+=================== ============= ====================
+
+The :attr:`~.Keystroke.name` property will prefer these
+aliases over the built-in :mod:`curses` names.
+
+The following are **not** available in the :mod:`curses` module, but are
+provided for keypad support, especially where the :meth:`~.Terminal.keypad`
+context manager is used with numlock on:
+
+* ``KEY_KP_MULTIPLY``
+* ``KEY_KP_ADD``
+* ``KEY_KP_SEPARATOR``
+* ``KEY_KP_SUBTRACT``
+* ``KEY_KP_DECIMAL``
+* ``KEY_KP_DIVIDE``
+* ``KEY_KP_0`` through ``KEY_KP_9``
+
+.. _couleur: https://pypi.python.org/pypi/couleur
+.. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/cbreak.3
+.. _`raw(3)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/raw.3
+.. _`curs_getch(3)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man3/curs_getch.3
+.. _`terminfo(5)`: http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man4/termios.3
+.. _SIGWINCH: https://en.wikipedia.org/wiki/SIGWINCH
diff --git a/docs/pains.rst b/docs/pains.rst
new file mode 100644
index 00000000..ac7454d6
--- /dev/null
+++ b/docs/pains.rst
@@ -0,0 +1,351 @@
+Growing Pains
+=============
+
+When making terminal applications, there are a surprisingly number of
+portability issues and edge cases. Although Blessed provides an abstraction
+for the full curses capability database, it is not sufficient to secure
+you from several considerations shared here.
+
+8 and 16 colors
+---------------
+
+XXX
+
+https://en.wikipedia.org/wiki/ANSI_escape_code#3/4_bit
+
+As a curses library, I think we should safely assume to map our colorspace
+to rgb values that match xterm.
+
+XXX
+
+Where 8 and 16 colors are used, they should be assumed to be the
+`CGA Color Palette`_. Though there is no terminal standard that proclaims
+that the CGA colors are used, their values are the best approximations
+across all common hardware terminals and terminal emulators.
+
+A recent phenomenon of users is to customize their base 16 colors to provide
+(often, more "washed out") color schemes. Furthermore, we are only recently
+getting LCD displays of colorspaces that achieve close approximation to the
+original video terminals. Some find these values uncomfortably intense: in
+their original CRT form, their contrast and brightness was lowered by hardware
+dials, whereas today's LCD's typically display colors well near full intensity.
+
+Though we may not *detect* the colorspace of the remote terminal, **we can**:
+
+- Trust that a close approximation of the `CGA Color Palette`_ for the base
+ 16 colors will be displayed for **most** users.
+
+- Trust that users who have made the choice to adjust their palette have made
+ the choice to do so, and are able to re-adjust such palettes as necessary
+ to accommodate different programs (such as through the use of "Themes").
+
+.. note::
+
+ It has become popular to use dynamic system-wide color palette adjustments
+ in software such as `f.lux`_, "Dark Mode", "Night Mode", and others,
+ which adjust the system-wide "Color Profile" of the entire graphics display
+ depending on the time of day. One might assume that ``term.blue("text")``
+ may become **completely** invisible to such users during the night!
+
+
+Where is brown, purple, or grey?
+--------------------------------
+
+There are **only 8 color names** on a 16-color terminal: The second set of
+eight colors are "high intensity" versions of the first in direct series.
+
+The colors *brown*, *purple*, and *grey* are not named in the first series,
+though they are available:
+
+- **brown**: *yellow is brown*, only high-intensity yellow
+ (``bright_yellow``) is yellow!
+
+- **purple**: *magenta is purple*. In earlier, 4-bit color spaces, there
+ were only black, cyan, magenta, and white of low and high intensity, such
+ as found on common home computers like the `ZX Spectrum`_.
+
+ Additional "colors" were only possible through dithering. The color names
+ cyan and magenta on later graphics adapters are carried over from its
+ predecessors. Although the color cyan remained true in RGB value on
+ 16-color to its predecessor, magenta shifted farther towards blue from red
+ becoming purple (as true red was introduced as one of the new base 8
+ colors).
+
+- **grey**: there are actually **three shades of grey** (or American spelling,
+ 'gray'), though the color attribute named 'grey' does not exist!
+
+ In ascending order of intensity, the shades of grey are:
+
+ - ``bold_black``: in lieu of the uselessness of an "intense black", this is
+ color is instead mapped to "dark grey".
+ - ``white``: white is actually mild compared to the true color 'white': this
+ is more officially mapped to "common grey", and is often the default
+ foreground color.
+ - ``bright_white``: is pure white (``#ffffff``).
+
+
+white-on-black
+~~~~~~~~~~~~~~
+
+The default foreground and background should be assumed as *white-on-black*.
+
+For quite some time, the families of terminals produced by DEC, IBM, and
+Tektronix dominated the computing world with the default color scheme of
+*green-on-black* and less commonly *amber-on-black* monochrome displays:
+The inverse was a non-default configuration. The IBM 3270 clients exclusively
+used *green-on-black* in both hardware and software emulators, and is likely
+a driving factor of the default *white-on-black* appearance of the first IBM
+Personal Computer.
+
+The less common *black-on-white* "ink paper" style of emulators is a valid
+concern for those designing terminal interfaces. The color scheme of
+*black-on-white* directly conflicts with the intention of `bold is bright`_,
+where ``term.bright_red('ATTENTION!')`` may become difficult to read,
+as it appears as *pink on white*!
+
+
+History of ink-paper inspired black-on-white
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Early home computers with color video adapters, such as the Commodore 64
+provided *white-on-blue* as their basic video terminal configuration. One can
+only assume such appearances were provided to demonstrate their color
+capabilities over competitors (such as the Apple ][).
+
+More common, X11's xterm and the software HyperTerm bundle with MS Windows
+provided an "ink on paper" *black-on-white* appearance as their default
+configuration. Two popular emulators continue to supply *black-on-white* by
+default to this day: Xorg's xterm and Apple's Terminal.app.
+
+.. note:: Windows no longer supplies a terminal emulator: the "command prompt"
+ as we know it now uses the MSVCRT API routines to interact and does not
+ make use of terminal sequences, even ignoring those sequences that MS-DOS
+ family of systems previously interpreted through the ANSI.SYS driver,
+ though it continues to default to *white-on-black*.
+
+
+Bold is bright
+--------------
+
+**Where Bold is used, it should be assumed to be *Bright***.
+
+Due to the influence of early graphics adapters providing a set of 8
+"low-intensity" and 8 "high intensity" versions of the first, the term
+"bold" for terminals sequences is synonymous with "high intensity" in
+almost all circumstances.
+
+
+History of bold as "wide stroke"
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In typography, the true translation of "bold" is that a font should be
+displayed *with emphasis*. In classical terms, this would be achieved by
+pen be re-writing over the same letters. On a teletype or printer, this was
+similarly achieved by writing a character, backspacing, then repeating the
+same character in a form called **overstriking**.
+
+To bold a character, ``C``, one would emit the sequence ``C^HC`` where
+``^H`` is backspace (0x08). To underline ``C``, one would would emit
+``C^H_``.
+
+**Video terminals do not support overstriking**. Though the mdoc format for
+manual pages continue to emit overstriking sequences for bold and underline,
+translators such as mandoc will instead emit an appropriate terminal sequence.
+
+Many characters previously displayable by combining using overstriking of
+ASCII characters on teletypes, such as: ±, ≠, or ⩝ were delegated to a
+`code page`_ or lost entirely until the introduction of multibyte encodings.
+
+Much like the "ink paper" introduction in windowing systems for terminal
+emulators, "wide stroke" bold was introduced only much later when combined
+with operating systems that provided font routines such as TrueType.
+
+
+Enforcing white-on-black
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+In conclusion, *white-on-black* should be considered the default. If there is
+a need to **enforce** *white-on-black* for terminal clients suspected to be
+defaulted as *black-on-white*, one would want to trust that a combination of
+``term.home + term.white_on_black + term.clear`` should repaint the entire
+emulator's window with the desired effect.
+
+However, this cannot be trusted to **all** terminal emulators to perform
+correctly! Depending on your audience, you may instead ensure that the
+entire screen (including whitespace) is painted using the ``on_black``
+mnemonic.
+
+Beware of customized color schemes
+----------------------------------
+
+A recent phenomenon is for users to customize these first 16 colors of their
+preferred emulator to colors of their own liking. Though this has always been
+possible with ``~/.XResources``, the introduction of PuTTy and iTerm2 to
+interactively adjustment these colors have made this much more common.
+
+This may cause your audience to see your intended interface in a wildly
+different form. Your intended presentation may appear mildly unreadable.
+
+Users are certainly free to customize their colors however they like, but it
+should be known that displaying ``term.black_on_red("DANGER!")`` may appear
+as "grey on pastel red" to your audience, reducing the intended effect of
+intensity.
+
+
+256 colors can avoid customization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first instinct of a user who aliases ls(1) to ``ls -G`` or ``colorls``,
+when faced with the particularly low intensity of the default ``blue``
+attribute is **to adjust their terminal emulator's color scheme of the base
+16 colors**.
+
+This is not necessary: the environment variable ``LSCOLORS`` may be redefined
+to map an alternative color for blue, or to use ``bright_blue`` in its place.
+
+Furthermore, all common terminal text editors such as emacs or vim may be
+configured with "colorschemes" to make use of the 256-color support found in
+most modern emulators. Many readable shades of blue are available, and many
+programs that emit such colors can be configured to emit a higher or lower
+intensity variant from the full 256 color space through program configuration.
+
+
+Monochrome and reverse
+----------------------
+
+Note that ``reverse`` takes the current foreground and background colors and
+reverses them. In contrast, the compound formatter ``black_on_red`` would
+fail to set the background *or* foreground color on a monochrome display,
+resulting in the same stylization as ``normal`` -- it would not appear any
+different!
+
+If your userbase consists of monochrome terminals, you may wish to provide
+"lightbars" and other such effects using the compound formatter
+``red_reverse``. In the literal sense of "set foreground color to red, then
+swap foreground and background", this produces a similar effect on
+**both** color and monochrome displays.
+
+For text, very few ``{color}_on_{color}`` formatters are visible with the
+base 16 colors, so you should generally wish for ``black_on_{color}``
+anyway. By using ``{color}_reverse`` you may be portable with monochrome
+displays as well.
+
+
+Multibyte Encodings and Code pages
+----------------------------------
+
+A terminal that supports both multibyte encodings (UTF-8) and legacy 8-bit
+code pages (ISO 2022) may instruct the terminal to switch between both
+modes using the following sequences:
+
+ - ``\x1b%G`` activates UTF-8 with an unspecified implementation level
+ from ISO 2022 in a way that allows to go back to ISO 2022 again.
+ - ``\x1b%@`` goes back from UTF-8 to ISO 2022 in case UTF-8 had been
+ entered via ``\x1b%G``.
+ - ``\x1b%/G`` switches to UTF-8 Level 1 with no return.
+ - ``\x1b%/H`` switches to UTF-8 Level 2 with no return.
+ - ``\x1b%/I`` switches to UTF-8 Level 3 with no return.
+
+When a terminal is in ISO 2022 mode, you may use a sequence
+to request a terminal to change its `code page`_. It begins by ``\x1b(``,
+followed by an ASCII character representing a code page selection. For
+example ``\x1b(U`` on the legacy VGA Linux console switches to the `IBM CP437`_
+`code page`_, allowing North American MS-DOS artwork to be displayed in its
+natural 8-bit byte encoding. A list of standard codes and the expected code
+page may be found on Thomas E. Dickey's xterm control sequences section on
+sequences following the `Control-Sequence-Inducer`_.
+
+For more information, see `What are the issues related to UTF-8 terminal
+emulators? `_ by
+`Markus Kuhn `_ of the University of
+Cambridge.
+
+One can be assured that the connecting client is capable of representing
+UTF-8 and other multibyte character encodings by the Environment variable
+``LANG``. If this is not possible or reliable, there is an intrusive detection
+method demonstrated in the example program :ref:`detect-multibyte.py`.
+
+Alt or meta sends Escape
+------------------------
+
+Programs using GNU readline such as bash continue to provide default mappings
+such as *ALT+u* to uppercase the word after cursor. This is achieved
+by the configuration option altSendsEscape or `metaSendsEscape
+`_
+
+The default for most terminals, however, is that the meta key is bound by
+the operating system (such as *META + F* for find), and that *ALT* is used
+for inserting international keys (where the combination *ALT+u, a* is used
+to insert the character ``ä``).
+
+It is therefore a recommendation to **avoid alt or meta keys entirely** in
+applications, and instead prefer the ctrl-key combinations, so as to avoid
+instructing your users to configure their terminal emulators to communicate
+such sequences.
+
+If you wish to allow them optionally (such as through readline), the ability
+to detect alt or meta key combinations is achieved by prefacing the combining
+character with escape, so that *ALT+z* becomes *Escape + z* (or, in raw form
+``\x1bz``). Blessings currently provides no further assistance in detecting
+these key combinations.
+
+
+Backspace sends delete
+----------------------
+
+Typically, backspace is ``^H`` (8, or 0x08) and delete is ^? (127, or 0x7f).
+
+On some systems however, the key for backspace is actually labeled and
+transmitted as "delete", though its function in the operating system behaves
+just as backspace.
+
+It is highly recommend to accept **both** ``KEY_DELETE`` and ``KEY_BACKSPACE``
+as having the same meaning except when implementing full screen editors,
+and provide a choice to enable the delete mode by configuration.
+
+The misnomer of ANSI
+--------------------
+
+When people say 'ANSI Sequence', they are discussing:
+
+- Standard `ECMA-48`_: Control Functions for Coded Character Sets
+
+- `ANSI X3.64 `_ from
+ 1981, when the `American National Standards Institute
+ `_ adopted the `ECMA-48`_ as standard, which was later
+ withdrawn in 1997 (so in this sense it is *not* an ANSI standard).
+
+- The `ANSI.SYS`_ driver provided in MS-DOS and
+ clones. The popularity of the IBM Personal Computer and MS-DOS of the era,
+ and its ability to display colored text further populated the idea that such
+ text "is ANSI".
+
+- The various code pages used in MS-DOS Personal Computers,
+ providing "block art" characters in the 8th bit (int 127-255), paired
+ with `ECMA-48`_ sequences supported by the MS-DOS `ANSI.SYS`_ driver
+ to create artwork, known as `ANSI art `_.
+
+- The ANSI terminal database entry and its many descendants in the
+ `terminfo database
+ `_. This is mostly
+ due to terminals compatible with SCO UNIX, which was the successor of
+ Microsoft's Xenix, which brought some semblance of the Microsoft DOS
+ `ANSI.SYS`_ driver capabilities.
+
+- `Select Graphics Rendition (SGR) `_
+ on vt100 clones, which include many of the common sequences in `ECMA-48`_.
+
+- Any sequence started by the `Control-Sequence-Inducer`_ is often
+ mistakenly termed as an "ANSI Escape Sequence" though not appearing in
+ `ECMA-48`_ or interpreted by the `ANSI.SYS`_ driver. The adjoining phrase
+ "Escape Sequence" is so termed because it follows the ASCII character
+ for the escape key (ESC, ``\x1b``).
+
+.. _code page: https://en.wikipedia.org/wiki/Code_page
+.. _IBM CP437: https://en.wikipedia.org/wiki/Code_page_437
+.. _CGA Color Palette: https://en.wikipedia.org/wiki/Color_Graphics_Adapter#With_an_RGBI_monitor
+.. _f.lux: https://justgetflux.com/
+.. _ZX Spectrum: https://en.wikipedia.org/wiki/List_of_8-bit_computer_hardware_palettes#ZX_Spectrum
+.. _Control-Sequence-Inducer: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Controls-beginning-with-ESC
+.. _ANSI.SYS: http://www.kegel.com/nansi/
+.. _ECMA-48: http://www.ecma-international.org/publications/standards/Ecma-048.htm
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..1eaa2ed3
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,4 @@
+Sphinx==2.3.1
+sphinx_rtd_theme
+sphinx-paramlinks
+jinxed>=0.5.4
diff --git a/docs/sphinxext/github.py b/docs/sphinxext/github.py
new file mode 100644
index 00000000..40ae5339
--- /dev/null
+++ b/docs/sphinxext/github.py
@@ -0,0 +1,160 @@
+"""Define text roles for GitHub
+
+* ghissue - Issue
+* ghpull - Pull Request
+* ghuser - User
+
+Adapted from bitbucket example here:
+https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py
+
+Authors
+-------
+
+* Doug Hellmann
+* Min RK
+"""
+#
+# Original Copyright (c) 2010 Doug Hellmann. All rights reserved.
+#
+
+# local
+from docutils import nodes, utils
+from sphinx.util import logging
+from docutils.parsers.rst.roles import set_classes
+
+LOGGER = logging.getLogger(__name__)
+
+
+def make_link_node(rawtext, app, type, slug, options):
+ """Create a link to a github resource.
+
+ :param rawtext: Text being replaced with link node.
+ :param app: Sphinx application context
+ :param type: Link type (issues, changeset, etc.)
+ :param slug: ID of the thing to link to
+ :param options: Options dictionary passed to role func.
+ """
+
+ try:
+ base = app.config.github_project_url
+ if not base:
+ raise AttributeError
+ if not base.endswith('/'):
+ base += '/'
+ except AttributeError as err:
+ raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
+
+ ref = base + type + '/' + slug + '/'
+ set_classes(options)
+ prefix = "#"
+ if type == 'pull':
+ prefix = "PR " + prefix
+ node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref,
+ **options)
+ return node
+
+def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+ """Link to a GitHub issue.
+
+ Returns 2 part tuple containing list of nodes to insert into the
+ document and a list of system messages. Both are allowed to be
+ empty.
+
+ :param name: The role name used in the document.
+ :param rawtext: The entire markup snippet, with role.
+ :param text: The text marked with the role.
+ :param lineno: The line number where rawtext appears in the input.
+ :param inliner: The inliner instance that called us.
+ :param options: Directive options for customization.
+ :param content: The directive content for customization.
+ """
+
+ try:
+ issue_num = int(text)
+ if issue_num <= 0:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ 'GitHub issue number must be a number greater than or equal to 1; '
+ '"%s" is invalid.' % text, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ app = inliner.document.settings.env.app
+ #LOGGER.info('issue %r' % text)
+ if 'pull' in name.lower():
+ category = 'pull'
+ elif 'issue' in name.lower():
+ category = 'issues'
+ else:
+ msg = inliner.reporter.error(
+ 'GitHub roles include "ghpull" and "ghissue", '
+ '"%s" is invalid.' % name, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ node = make_link_node(rawtext, app, category, str(issue_num), options)
+ return [node], []
+
+def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+ """Link to a GitHub user.
+
+ Returns 2 part tuple containing list of nodes to insert into the
+ document and a list of system messages. Both are allowed to be
+ empty.
+
+ :param name: The role name used in the document.
+ :param rawtext: The entire markup snippet, with role.
+ :param text: The text marked with the role.
+ :param lineno: The line number where rawtext appears in the input.
+ :param inliner: The inliner instance that called us.
+ :param options: Directive options for customization.
+ :param content: The directive content for customization.
+ """
+ app = inliner.document.settings.env.app
+ #LOGGER.info('user link %r' % text)
+ ref = 'https://github.com/' + text
+ node = nodes.reference(rawtext, text, refuri=ref, **options)
+ return [node], []
+
+def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+ """Link to a GitHub commit.
+
+ Returns 2 part tuple containing list of nodes to insert into the
+ document and a list of system messages. Both are allowed to be
+ empty.
+
+ :param name: The role name used in the document.
+ :param rawtext: The entire markup snippet, with role.
+ :param text: The text marked with the role.
+ :param lineno: The line number where rawtext appears in the input.
+ :param inliner: The inliner instance that called us.
+ :param options: Directive options for customization.
+ :param content: The directive content for customization.
+ """
+ app = inliner.document.settings.env.app
+ #LOGGER.info('user link %r' % text)
+ try:
+ base = app.config.github_project_url
+ if not base:
+ raise AttributeError
+ if not base.endswith('/'):
+ base += '/'
+ except AttributeError as err:
+ raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
+
+ ref = base + text
+ node = nodes.reference(rawtext, text[:6], refuri=ref, **options)
+ return [node], []
+
+
+def setup(app):
+ """Install the plugin.
+
+ :param app: Sphinx application context.
+ """
+ LOGGER.info('Initializing GitHub plugin')
+ app.add_role('ghissue', ghissue_role)
+ app.add_role('ghpull', ghissue_role)
+ app.add_role('ghuser', ghuser_role)
+ app.add_role('ghcommit', ghcommit_role)
+ app.add_config_value('github_project_url', None, 'env')
+ return
diff --git a/fabfile.py b/fabfile.py
deleted file mode 100644
index af98c0d8..00000000
--- a/fabfile.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""Run this using ``fabric``.
-
-I can't remember any of this syntax on my own.
-
-"""
-from functools import partial
-from os import environ
-from os.path import abspath, dirname
-
-from fabric.api import local, cd
-
-
-local = partial(local, capture=False)
-
-ROOT = abspath(dirname(__file__))
-
-environ['PYTHONPATH'] = (((environ['PYTHONPATH'] + ':') if
- environ.get('PYTHONPATH') else '') + ROOT)
-
-
-def doc(kind='html'):
- """Build Sphinx docs.
-
- Requires Sphinx to be installed.
-
- """
- with cd('docs'):
- local('make clean %s' % kind)
-
-
-def updoc():
- """Build Sphinx docs and upload them to packages.python.org.
-
- Requires Sphinx-PyPI-upload to be installed.
-
- """
- doc('html')
- local('python setup.py upload_sphinx --upload-dir=docs/_build/html')
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..917d2a45
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+wcwidth>=0.1.4
+six>=1.9.0
+# support python2.6 by using backport of 'orderedict'
+ordereddict==1.1; python_version < "2.7"
+# support python2.7 by using backport of 'functools.lru_cache'
+backports.functools-lru-cache>=1.2.1; python_version < "3.2"
+# Windows requires jinxed
+jinxed>=0.5.4; platform_system == "Windows"
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..2a9acf13
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
old mode 100644
new mode 100755
index ec4cfd10..16600b5e
--- a/setup.py
+++ b/setup.py
@@ -1,32 +1,49 @@
-import sys
+#!/usr/bin/env python
+"""Distutils setup script."""
+# std imports
+import os
-# Prevent spurious errors during `python setup.py test`, a la
-# http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html:
-try:
- import multiprocessing
-except ImportError:
- pass
+# 3rd party
+import setuptools
-from setuptools import setup, find_packages
+def _get_install_requires(fname):
+ result = [req_line.strip() for req_line in open(fname)
+ if req_line.strip() and not req_line.startswith('#')]
-extra_setup = {}
-if sys.version_info >= (3,):
- extra_setup['use_2to3'] = True
+ return result
-setup(
- name='blessings',
- version='1.6',
- description='A thin, practical wrapper around terminal coloring, styling, and positioning',
- long_description=open('README.rst').read(),
- author='Erik Rose',
- author_email='erikrose@grinchcentral.com',
+
+def _get_version(fname):
+ import json
+ return json.load(open(fname, 'r'))['version']
+
+
+def _get_long_description(fname):
+ import codecs
+ return codecs.open(fname, 'r', 'utf8').read()
+
+
+HERE = os.path.dirname(__file__)
+
+setuptools.setup(
+ name='blessed',
+ version=_get_version(
+ fname=os.path.join(HERE, 'version.json')),
+ install_requires=_get_install_requires(
+ fname=os.path.join(HERE, 'requirements.txt')),
+ long_description=_get_long_description(
+ fname=os.path.join(HERE, 'docs', 'intro.rst')),
+ description=('A thin, practical wrapper around terminal styling, '
+ 'screen positioning, and keyboard input.'),
+ author='Jeff Quast, Erik Rose',
+ author_email='contact@jeffquast.com',
license='MIT',
- packages=find_packages(exclude=['ez_setup']),
- tests_require=['nose'],
- test_suite='nose.collector',
- url='https://github.com/erikrose/blessings',
+ packages=['blessed', ],
+ url='https://github.com/jquast/blessed',
+ project_urls={'Documentation': 'https://blessed.readthedocs.io'},
include_package_data=True,
+ zip_safe=True,
classifiers=[
'Intended Audience :: Developers',
'Natural Language :: English',
@@ -35,16 +52,22 @@
'Environment :: Console :: Curses',
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX',
+ 'Operating System :: Microsoft :: Windows',
'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.5',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: User Interfaces',
'Topic :: Terminals'
- ],
- keywords=['terminal', 'tty', 'curses', 'ncurses', 'formatting', 'style', 'color', 'console'],
- **extra_setup
+ ],
+ keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses',
+ 'formatting', 'style', 'color', 'console', 'keyboard',
+ 'ansi', 'xterm'],
)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/accessories.py b/tests/accessories.py
new file mode 100644
index 00000000..73a6eedd
--- /dev/null
+++ b/tests/accessories.py
@@ -0,0 +1,268 @@
+# -*- coding: utf-8 -*-
+"""Accessories for automated py.test runner."""
+# standard imports
+from __future__ import print_function, with_statement
+
+# std imports
+import os
+import sys
+import codecs
+import platform
+import functools
+import traceback
+import contextlib
+import subprocess
+
+# 3rd party
+import six
+import pytest
+
+# local
+from blessed import Terminal
+
+if platform.system() != "Windows":
+ import curses
+ import pty
+ import termios
+else:
+ import jinxed as curses
+
+
+test_kind = 'xterm-256color'
+if platform.system() == 'Windows':
+ test_kind = 'vtwin10'
+TestTerminal = functools.partial(Terminal, kind=test_kind)
+SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n'
+RECV_SEMAPHORE = b'SEMAPHORE\r\n'
+many_lines_params = [40, 80]
+# we must test a '1' column for conditional in _handle_long_word
+many_columns_params = [1, 10]
+
+if os.environ.get('TEST_QUICK'):
+ many_lines_params = [80, ]
+ many_columns_params = [25, ]
+
+all_terms_params = 'xterm screen ansi vt220 rxvt cons25 linux'.split()
+
+if os.environ.get('TEST_FULL'):
+ try:
+ all_terms_params = [
+ # use all values of the first column of data in output of 'toe -a'
+ _term.split(None, 1)[0] for _term in
+ subprocess.Popen(('toe', '-a'),
+ stdout=subprocess.PIPE,
+ close_fds=True)
+ .communicate()[0].splitlines()]
+ except OSError:
+ pass
+elif platform.system() == 'Windows':
+ all_terms_params = ['vtwin10', ]
+elif os.environ.get('TEST_QUICK'):
+ all_terms_params = 'xterm screen ansi linux'.split()
+
+
+def init_subproc_coverage(run_note):
+ try:
+ import coverage
+ except ImportError:
+ return None
+ _coveragerc = os.path.join(
+ os.path.dirname(__file__),
+ os.pardir, 'tox.ini')
+ cov = coverage.Coverage(config_file=_coveragerc)
+ cov.start()
+ return cov
+
+
+class as_subprocess(object):
+ """This helper executes test cases in a child process, avoiding a python-internal bug of
+ _curses: setupterm() may not be called more than once per process."""
+ _CHILD_PID = 0
+ encoding = 'utf8'
+
+ def __init__(self, func):
+ self.func = func
+
+ def __call__(self, *args, **kwargs):
+ if platform.system() == 'Windows':
+ self.func(*args, **kwargs)
+ return
+
+ pid_testrunner = os.getpid()
+ pid, master_fd = pty.fork()
+ if pid == self._CHILD_PID:
+ # child process executes function, raises exception
+ # if failed, causing a non-zero exit code, using the
+ # protected _exit() function of ``os``; to prevent the
+ # 'SystemExit' exception from being thrown.
+ cov = init_subproc_coverage(
+ "@as_subprocess-{pid};{func_name}(*{args}, **{kwargs})"
+ .format(pid=os.getpid(), func_name=self.func,
+ args=args, kwargs=kwargs))
+ try:
+ self.func(*args, **kwargs)
+ except Exception:
+ e_type, e_value, e_tb = sys.exc_info()
+ o_err = list()
+ for line in traceback.format_tb(e_tb):
+ o_err.append(line.rstrip().encode('utf-8'))
+ o_err.append(('-=' * 20).encode('ascii'))
+ o_err.extend([_exc.rstrip().encode('utf-8') for _exc in
+ traceback.format_exception_only(
+ e_type, e_value)])
+ os.write(sys.__stdout__.fileno(), b'\n'.join(o_err))
+ os.close(sys.__stdout__.fileno())
+ os.close(sys.__stderr__.fileno())
+ os.close(sys.__stdin__.fileno())
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(1)
+ else:
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ # detect rare fork in test runner, when bad bugs happen
+ if pid_testrunner != os.getpid():
+ print('TEST RUNNER HAS FORKED, {0}=>{1}: EXIT'
+ .format(pid_testrunner, os.getpid()), file=sys.stderr)
+ os._exit(1)
+
+ exc_output = six.text_type()
+ decoder = codecs.getincrementaldecoder(self.encoding)()
+ while True:
+ try:
+ _exc = os.read(master_fd, 65534)
+ except OSError:
+ # linux EOF
+ break
+ if not _exc:
+ # bsd EOF
+ break
+ exc_output += decoder.decode(_exc)
+
+ # parent process asserts exit code is 0, causing test
+ # to fail if child process raised an exception/assertion
+ pid, status = os.waitpid(pid, 0)
+ os.close(master_fd)
+
+ # Display any output written by child process
+ # (esp. any AssertionError exceptions written to stderr).
+ exc_output_msg = 'Output in child process:\n%s\n%s\n%s' % (
+ u'=' * 40, exc_output, u'=' * 40,)
+ assert exc_output == '', exc_output_msg
+
+ # Also test exit status is non-zero
+ assert os.WEXITSTATUS(status) == 0
+
+
+def read_until_semaphore(fd, semaphore=RECV_SEMAPHORE,
+ encoding='utf8', timeout=10):
+ """
+ Read file descriptor ``fd`` until ``semaphore`` is found.
+
+ Used to ensure the child process is awake and ready. For timing tests; without a semaphore, the
+ time to fork() would be (incorrectly) included in the duration of the test, which can be very
+ length on continuous integration servers (such as Travis-CI).
+ """
+ # note that when a child process writes xyz\\n, the parent
+ # process will read xyz\\r\\n -- this is how pseudo terminals
+ # behave; a virtual terminal requires both carriage return and
+ # line feed, it is only for convenience that \\n does both.
+ outp = six.text_type()
+ decoder = codecs.getincrementaldecoder(encoding)()
+ semaphore = semaphore.decode('ascii')
+ while not outp.startswith(semaphore):
+ try:
+ _exc = os.read(fd, 1)
+ except OSError: # Linux EOF
+ break
+ if not _exc: # BSD EOF
+ break
+ outp += decoder.decode(_exc, final=False)
+ assert outp.startswith(semaphore), (
+ 'Semaphore not recv before EOF '
+ '(expected: %r, got: %r)' % (semaphore, outp,))
+ return outp[len(semaphore):]
+
+
+def read_until_eof(fd, encoding='utf8'):
+ """
+ Read file descriptor ``fd`` until EOF.
+
+ Return decoded string.
+ """
+ decoder = codecs.getincrementaldecoder(encoding)()
+ outp = six.text_type()
+ while True:
+ try:
+ _exc = os.read(fd, 100)
+ except OSError: # linux EOF
+ break
+ if not _exc: # bsd EOF
+ break
+ outp += decoder.decode(_exc, final=False)
+ return outp
+
+
+@contextlib.contextmanager
+def echo_off(fd):
+ """Ensure any bytes written to pty fd are not duplicated as output."""
+ if platform.system() != 'Windows':
+ try:
+ attrs = termios.tcgetattr(fd)
+ attrs[3] = attrs[3] & ~termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attrs)
+ yield
+ finally:
+ attrs[3] = attrs[3] | termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attrs)
+ else:
+ yield
+
+
+def unicode_cap(cap):
+ """Return the result of ``tigetstr`` except as Unicode."""
+ try:
+ val = curses.tigetstr(cap)
+ except curses.error:
+ val = None
+ if val:
+ return val.decode('latin1')
+ return u''
+
+
+def unicode_parm(cap, *parms):
+ """Return the result of ``tparm(tigetstr())`` except as Unicode."""
+ try:
+ cap = curses.tigetstr(cap)
+ except curses.error:
+ cap = None
+ if cap:
+ try:
+ val = curses.tparm(cap, *parms)
+ except curses.error:
+ val = None
+ if val:
+ return val.decode('latin1')
+ return u''
+
+
+@pytest.fixture(params=all_terms_params)
+def all_terms(request):
+ """Common kind values for all kinds of terminals."""
+ return request.param
+
+
+@pytest.fixture(params=many_lines_params)
+def many_lines(request):
+ """Various number of lines for screen height."""
+ return request.param
+
+
+@pytest.fixture(params=many_columns_params)
+def many_columns(request):
+ """Various number of columns for screen width."""
+ return request.param
diff --git a/tests/test_core.py b/tests/test_core.py
new file mode 100644
index 00000000..a939aea7
--- /dev/null
+++ b/tests/test_core.py
@@ -0,0 +1,498 @@
+# -*- coding: utf-8 -*-
+"""Core blessed Terminal() tests."""
+
+# std imports
+import io
+import os
+import sys
+import math
+import time
+import platform
+import warnings
+import collections
+
+# 3rd party
+import six
+import mock
+import pytest
+from six.moves import reload_module
+
+# local
+from .accessories import TestTerminal, all_terms, unicode_cap, as_subprocess
+
+
+def test_export_only_Terminal():
+ "Ensure only Terminal instance is exported for import * statements."
+ import blessed
+ assert blessed.__all__ == ('Terminal',)
+
+
+def test_null_location(all_terms):
+ """Make sure ``location()`` with no args just does position restoration."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
+ with t.location():
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc'), unicode_cap('rc')))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+def test_location_to_move_xy(all_terms):
+ """``location()`` and ``move_xy()`` receive complimentary arguments."""
+ @as_subprocess
+ def child(kind):
+ buf = six.StringIO()
+ t = TestTerminal(stream=buf, force_styling=True)
+ x, y = 12, 34
+ with t.location(y, x):
+ xy_val_from_move_xy = t.move_xy(y, x)
+ xy_val_from_location = buf.getvalue()[len(t.sc):]
+ assert xy_val_from_move_xy == xy_val_from_location
+
+ child(all_terms)
+
+
+def test_yield_keypad():
+ """Ensure ``keypad()`` writes keyboard_xmit and keyboard_local."""
+ @as_subprocess
+ def child(kind):
+ # given,
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
+ expected_output = u''.join((t.smkx, t.rmkx))
+
+ # exercise,
+ with t.keypad():
+ pass
+
+ # verify.
+ assert (t.stream.getvalue() == expected_output)
+
+ child(kind='xterm')
+
+
+def test_null_fileno():
+ """Make sure ``Terminal`` works when ``fileno`` is ``None``."""
+ @as_subprocess
+ def child():
+ # This simulates piping output to another program.
+ out = six.StringIO()
+ out.fileno = None
+ t = TestTerminal(stream=out)
+ assert (t.save == u'')
+
+ child()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires more than 1 tty")
+def test_number_of_colors_without_tty():
+ """``number_of_colors`` should return 0 when there's no tty."""
+ @as_subprocess
+ def child_256_nostyle():
+ t = TestTerminal(stream=six.StringIO())
+ assert (t.number_of_colors == 0)
+
+ @as_subprocess
+ def child_256_forcestyle():
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
+ assert (t.number_of_colors == 256)
+
+ @as_subprocess
+ def child_8_forcestyle():
+ kind = 'ansi'
+ if platform.system().lower() == 'freebsd':
+ # 'ansi' on freebsd returns 0 colors, we use the 'cons25' driver,
+ # compatible with its kernel tty.c
+ kind = 'cons25'
+ t = TestTerminal(kind=kind, stream=six.StringIO(),
+ force_styling=True)
+ assert (t.number_of_colors == 8)
+
+ @as_subprocess
+ def child_0_forcestyle():
+ t = TestTerminal(kind='vt220', stream=six.StringIO(),
+ force_styling=True)
+ assert (t.number_of_colors == 0)
+
+ child_0_forcestyle()
+ child_8_forcestyle()
+ child_256_forcestyle()
+ child_256_nostyle()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires more than 1 tty")
+def test_number_of_colors_with_tty():
+ """test ``number_of_colors`` 0, 8, and 256."""
+ @as_subprocess
+ def child_256():
+ t = TestTerminal()
+ assert (t.number_of_colors == 256)
+
+ @as_subprocess
+ def child_8():
+ kind = 'ansi'
+ if platform.system().lower() == 'freebsd':
+ # 'ansi' on freebsd returns 0 colors, we use the 'cons25' driver,
+ # compatible with its kernel tty.c
+ kind = 'cons25'
+ t = TestTerminal(kind=kind)
+ assert (t.number_of_colors == 8)
+
+ @as_subprocess
+ def child_0():
+ t = TestTerminal(kind='vt220')
+ assert (t.number_of_colors == 0)
+
+ child_0()
+ child_8()
+ child_256()
+
+
+def test_init_descriptor_always_initted(all_terms):
+ """Test height and width with non-tty Terminals."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO())
+ assert t._init_descriptor == sys.__stdout__.fileno()
+ assert (isinstance(t.height, int))
+ assert (isinstance(t.width, int))
+ assert t.height == t._height_and_width()[0]
+ assert t.width == t._height_and_width()[1]
+
+ child(all_terms)
+
+
+def test_force_styling_none(all_terms):
+ """If ``force_styling=None`` is used, don't ever do styling."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, force_styling=None)
+ assert (t.save == '')
+ assert (t.color(9) == '')
+ assert (t.bold('oi') == 'oi')
+
+ child(all_terms)
+
+
+def test_setupterm_singleton_issue_33():
+ """A warning is emitted if a new terminal ``kind`` is used per process."""
+ @as_subprocess
+ def child():
+ warnings.filterwarnings("error", category=UserWarning)
+
+ # instantiate first terminal, of type xterm-256color
+ term = TestTerminal(force_styling=True)
+ first_kind = term.kind
+ next_kind = 'xterm'
+
+ try:
+ # a second instantiation raises UserWarning
+ term = TestTerminal(kind=next_kind, force_styling=True)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert (err.args[0].startswith(
+ 'A terminal of kind "' + next_kind + '" has been requested')
+ ), err.args[0]
+ assert ('a terminal of kind "' + first_kind + '" will '
+ 'continue to be returned' in err.args[0]), err.args[0]
+ else:
+ # unless term is not a tty and setupterm() is not called
+ assert not term.is_a_tty or False, 'Should have thrown exception'
+ warnings.resetwarnings()
+
+ child()
+
+
+def test_setupterm_invalid_issue39():
+ """A warning is emitted if TERM is invalid."""
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=878089
+ #
+ # if TERM is unset, defaults to 'unknown', which should
+ # fail to lookup and emit a warning on *some* systems.
+ # freebsd actually has a termcap entry for 'unknown'
+ @as_subprocess
+ def child():
+ warnings.filterwarnings("error", category=UserWarning)
+
+ try:
+ term = TestTerminal(kind='unknown', force_styling=True)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert err.args[0] in (
+ "Failed to setupterm(kind='unknown'): "
+ "setupterm: could not find terminal",
+ "Failed to setupterm(kind='unknown'): "
+ "Could not find terminal unknown",
+ )
+ else:
+ if platform.system().lower() != 'freebsd':
+ assert not term.is_a_tty and not term.does_styling, (
+ 'Should have thrown exception')
+ warnings.resetwarnings()
+
+ child()
+
+
+def test_setupterm_invalid_has_no_styling():
+ """An unknown TERM type does not perform styling."""
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=878089
+
+ # if TERM is unset, defaults to 'unknown', which should
+ # fail to lookup and emit a warning, only.
+ @as_subprocess
+ def child():
+ warnings.filterwarnings("ignore", category=UserWarning)
+
+ term = TestTerminal(kind='xxXunknownXxx', force_styling=True)
+ assert term.kind is None
+ assert not term.does_styling
+ assert term.number_of_colors == 0
+ warnings.resetwarnings()
+
+ child()
+
+
+def test_missing_ordereddict_uses_module(monkeypatch):
+ """ordereddict module is imported when without collections.OrderedDict."""
+ import blessed.keyboard
+
+ if hasattr(collections, 'OrderedDict'):
+ monkeypatch.delattr('collections.OrderedDict')
+
+ try:
+ reload_module(blessed.keyboard)
+ except ImportError as err:
+ assert err.args[0] in ("No module named ordereddict", # py2
+ "No module named 'ordereddict'") # py3
+ sys.modules['ordereddict'] = mock.Mock()
+ sys.modules['ordereddict'].OrderedDict = -1
+ reload_module(blessed.keyboard)
+ assert blessed.keyboard.OrderedDict == -1
+ del sys.modules['ordereddict']
+ monkeypatch.undo()
+ reload_module(blessed.keyboard)
+ else:
+ assert platform.python_version_tuple() < ('2', '7') # reached by py2.6
+
+
+def test_python3_2_raises_exception(monkeypatch):
+ """Test python version 3.0 through 3.2 raises an exception."""
+ import blessed
+
+ monkeypatch.setattr('platform.python_version_tuple',
+ lambda: ('3', '2', '2'))
+
+ try:
+ reload_module(blessed)
+ except ImportError as err:
+ assert err.args[0] == (
+ 'Blessed needs Python 3.2.3 or greater for Python 3 '
+ 'support due to http://bugs.python.org/issue10570.')
+ monkeypatch.undo()
+ reload_module(blessed)
+ else:
+ assert False, 'Exception should have been raised'
+
+
+def test_without_dunder():
+ """Ensure dunder does not remain in module (py2x InterruptedError test."""
+ import blessed.terminal
+ assert '_' not in dir(blessed.terminal)
+
+
+def test_IOUnsupportedOperation():
+ """Ensure stream that throws IOUnsupportedOperation results in non-tty."""
+ @as_subprocess
+ def child():
+ import blessed.terminal
+
+ def side_effect():
+ raise io.UnsupportedOperation
+
+ mock_stream = mock.Mock()
+ mock_stream.fileno = side_effect
+
+ term = TestTerminal(stream=mock_stream)
+ assert term.stream == mock_stream
+ assert not term.does_styling
+ assert not term.is_a_tty
+ assert term.number_of_colors == 0
+
+ child()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="has process-wide side-effects")
+def test_winsize_IOError_returns_environ():
+ """When _winsize raises IOError, defaults from os.environ given."""
+ @as_subprocess
+ def child():
+ def side_effect(fd):
+ raise IOError
+
+ term = TestTerminal()
+ term._winsize = side_effect
+ os.environ['COLUMNS'] = '1984'
+ os.environ['LINES'] = '1888'
+ assert term._height_and_width() == (1888, 1984, None, None)
+
+ child()
+
+
+def test_yield_fullscreen(all_terms):
+ """Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
+ t.enter_fullscreen = u'BEGIN'
+ t.exit_fullscreen = u'END'
+ with t.fullscreen():
+ pass
+ expected_output = u''.join((t.enter_fullscreen, t.exit_fullscreen))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+def test_yield_hidden_cursor(all_terms):
+ """Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
+ t.hide_cursor = u'BEGIN'
+ t.normal_cursor = u'END'
+ with t.hidden_cursor():
+ pass
+ expected_output = u''.join((t.hide_cursor, t.normal_cursor))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="windows doesn't work like this")
+def test_no_preferredencoding_fallback_ascii():
+ """Ensure empty preferredencoding value defaults to ascii."""
+ @as_subprocess
+ def child():
+ with mock.patch('locale.getpreferredencoding') as get_enc:
+ get_enc.return_value = u''
+ t = TestTerminal()
+ assert t._encoding == 'ascii'
+
+ child()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl")
+def test_unknown_preferredencoding_warned_and_fallback_ascii():
+ """Ensure a locale without a codec emits a warning."""
+ @as_subprocess
+ def child():
+ with mock.patch('locale.getpreferredencoding') as get_enc:
+ get_enc.return_value = '---unknown--encoding---'
+ with pytest.warns(UserWarning, match=(
+ 'LookupError: unknown encoding: ---unknown--encoding---, '
+ 'fallback to ASCII for keyboard.')):
+ t = TestTerminal()
+ assert t._encoding == 'ascii'
+
+ child()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl")
+def test_win32_missing_tty_modules(monkeypatch):
+ """Ensure dummy exception is used when io is without UnsupportedOperation."""
+ @as_subprocess
+ def child():
+ OLD_STYLE = False
+ try:
+ original_import = getattr(__builtins__, '__import__')
+ OLD_STYLE = True
+ except AttributeError:
+ original_import = __builtins__['__import__']
+
+ tty_modules = ('termios', 'fcntl', 'tty')
+
+ def __import__(name, *args, **kwargs):
+ if name in tty_modules:
+ raise ImportError
+ return original_import(name, *args, **kwargs)
+
+ for module in tty_modules:
+ sys.modules.pop(module, None)
+
+ warnings.filterwarnings("error", category=UserWarning)
+ try:
+ if OLD_STYLE:
+ __builtins__.__import__ = __import__
+ else:
+ __builtins__['__import__'] = __import__
+ try:
+ import blessed.terminal
+ reload_module(blessed.terminal)
+ except UserWarning:
+ err = sys.exc_info()[1]
+ assert err.args[0] == blessed.terminal._MSG_NOSUPPORT
+
+ warnings.filterwarnings("ignore", category=UserWarning)
+ import blessed.terminal
+ reload_module(blessed.terminal)
+ assert not blessed.terminal.HAS_TTY
+ term = blessed.terminal.Terminal('ansi')
+ # https://en.wikipedia.org/wiki/VGA-compatible_text_mode
+ # see section '#PC_common_text_modes'
+ assert term.height == 25
+ assert term.width == 80
+
+ finally:
+ if OLD_STYLE:
+ setattr(__builtins__, '__import__', original_import)
+ else:
+ __builtins__['__import__'] = original_import
+ warnings.resetwarnings()
+ import blessed.terminal
+ reload_module(blessed.terminal)
+
+ child()
+
+
+def test_time_left():
+ """test '_time_left' routine returns correct positive delta difference."""
+ from blessed.keyboard import _time_left
+
+ # given stime =~ "10 seconds ago"
+ stime = (time.time() - 10)
+
+ # timeleft(now, 15s) = 5s remaining
+ timeout = 15
+ result = _time_left(stime=stime, timeout=timeout)
+
+ # we expect roughly 4.999s remain
+ assert math.ceil(result) == 5.0
+
+
+def test_time_left_infinite_None():
+ """keyboard '_time_left' routine returns None when given None."""
+ from blessed.keyboard import _time_left
+ assert _time_left(stime=time.time(), timeout=None) is None
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="cant multiprocess")
+def test_termcap_repr():
+ """Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor."""
+
+ given_ttype = 'vt220'
+ given_capname = 'cursor_up'
+ expected = [r"",
+ r"",
+ r""]
+
+ @as_subprocess
+ def child():
+ import blessed
+ term = blessed.Terminal(given_ttype)
+ given = repr(term.caps[given_capname])
+ assert given in expected
+
+ child()
diff --git a/tests/test_formatters.py b/tests/test_formatters.py
new file mode 100644
index 00000000..cfc01475
--- /dev/null
+++ b/tests/test_formatters.py
@@ -0,0 +1,442 @@
+# -*- coding: utf-8 -*-
+"""Tests string formatting functions."""
+# std imports
+import platform
+
+# 3rd party
+import mock
+import pytest
+
+if platform.system() != 'Windows':
+ import curses
+else:
+ import jinxed as curses
+
+
+def fn_tparm(*args):
+ return u'~'.join(
+ arg.decode('latin1') if not num else '%s' % (arg,)
+ for num, arg in enumerate(args)
+ ).encode('latin1')
+
+
+def test_parameterizing_string_args_unspecified(monkeypatch):
+ """Test default args of formatters.ParameterizingString."""
+ from blessed.formatters import ParameterizingString, FormattingString
+ # first argument to tparm() is the sequence name, returned as-is;
+ # subsequent arguments are usually Integers.
+ monkeypatch.setattr(curses, 'tparm', fn_tparm)
+
+ # given,
+ pstr = ParameterizingString(u'')
+
+ # exercise __new__
+ assert str(pstr) == u''
+ assert pstr._normal == u''
+ assert pstr._name == u''
+
+ # exercise __call__
+ zero = pstr(0)
+ assert isinstance(zero, FormattingString)
+ assert zero == u'~0'
+ assert zero('text') == u'~0text'
+
+ # exercise __call__ with multiple args
+ onetwo = pstr(1, 2)
+ assert isinstance(onetwo, FormattingString)
+ assert onetwo == u'~1~2'
+ assert onetwo('text') == u'~1~2text'
+
+
+def test_parameterizing_string_args(monkeypatch):
+ """Test basic formatters.ParameterizingString."""
+ from blessed.formatters import ParameterizingString, FormattingString
+
+ # first argument to tparm() is the sequence name, returned as-is;
+ # subsequent arguments are usually Integers.
+ monkeypatch.setattr(curses, 'tparm', fn_tparm)
+
+ # given,
+ pstr = ParameterizingString(u'cap', u'norm', u'seq-name')
+
+ # exercise __new__
+ assert str(pstr) == u'cap'
+ assert pstr._normal == u'norm'
+ assert pstr._name == u'seq-name'
+
+ # exercise __call__
+ zero = pstr(0)
+ assert isinstance(zero, FormattingString)
+ assert zero == u'cap~0'
+ assert zero('text') == u'cap~0textnorm'
+
+ # exercise __call__ with multiple args
+ onetwo = pstr(1, 2)
+ assert isinstance(onetwo, FormattingString)
+ assert onetwo == u'cap~1~2'
+ assert onetwo('text') == u'cap~1~2textnorm'
+
+
+def test_parameterizing_string_type_error(monkeypatch):
+ """Test formatters.ParameterizingString raising TypeError."""
+ from blessed.formatters import ParameterizingString
+
+ def tparm_raises_TypeError(*args):
+ raise TypeError('custom_err')
+
+ monkeypatch.setattr(curses, 'tparm', tparm_raises_TypeError)
+
+ # given,
+ pstr = ParameterizingString(u'cap', u'norm', u'cap-name')
+
+ # ensure TypeError when given a string raises custom exception
+ try:
+ pstr('XYZ')
+ assert False, "previous call should have raised TypeError"
+ except TypeError as err:
+ assert (err.args[0] == ( # py3x
+ "A native or nonexistent capability template, "
+ "'cap-name' received invalid argument ('XYZ',): "
+ "custom_err. You probably misspelled a "
+ "formatting call like `bright_red'") or
+ err.args[0] == (
+ "A native or nonexistent capability template, "
+ "u'cap-name' received invalid argument ('XYZ',): "
+ "custom_err. You probably misspelled a "
+ "formatting call like `bright_red'"))
+
+ # ensure TypeError when given an integer raises its natural exception
+ try:
+ pstr(0)
+ assert False, "previous call should have raised TypeError"
+ except TypeError as err:
+ assert err.args[0] == "custom_err"
+
+
+def test_formattingstring(monkeypatch):
+ """Test simple __call__ behavior of formatters.FormattingString."""
+ from blessed.formatters import FormattingString
+
+ # given, with arg
+ pstr = FormattingString(u'attr', u'norm')
+
+ # exercise __call__,
+ assert pstr._normal == u'norm'
+ assert str(pstr) == u'attr'
+ assert pstr('text') == u'attrtextnorm'
+
+ # given, with empty attribute
+ pstr = FormattingString(u'', u'norm')
+ assert pstr('text') == u'text'
+
+
+def test_nested_formattingstring(monkeypatch):
+ """Test nested __call__ behavior of formatters.FormattingString."""
+ from blessed.formatters import FormattingString
+
+ # given, with arg
+ pstr = FormattingString(u'a1-', u'n-')
+ zstr = FormattingString(u'a2-', u'n-')
+
+ # exercise __call__
+ assert pstr('x-', zstr('f-'), 'q-') == 'a1-x-a2-f-n-a1-q-n-'
+
+
+def test_nested_formattingstring_type_error(monkeypatch):
+ """Test formatters.FormattingString raising TypeError."""
+ from blessed.formatters import FormattingString
+
+ # given,
+ pstr = FormattingString(u'a-', u'n-')
+ expected_msg = (
+ "Positional argument #1 is {0} expected any of "
+ .format(type(1)))
+
+ # exercise,
+ with pytest.raises(TypeError) as err:
+ pstr('text', 1, '...')
+
+ # verify,
+ assert expected_msg in str(err.value)
+
+
+def test_nullcallablestring(monkeypatch):
+ """Test formatters.NullCallableString."""
+ from blessed.formatters import (NullCallableString)
+
+ # given, with arg
+ pstr = NullCallableString()
+
+ # exercise __call__,
+ assert str(pstr) == u''
+ assert pstr('text') == u'text'
+ assert pstr('text', 'moretext') == u'textmoretext'
+ assert pstr(99, 1) == u''
+ assert pstr() == u''
+ assert pstr(0) == u''
+
+
+def test_split_compound():
+ """Test formatters.split_compound."""
+ from blessed.formatters import split_compound
+
+ assert split_compound(u'') == [u'']
+ assert split_compound(u'a_b_c') == [u'a', u'b', u'c']
+ assert split_compound(u'a_on_b_c') == [u'a', u'on_b', u'c']
+ assert split_compound(u'a_bright_b_c') == [u'a', u'bright_b', u'c']
+ assert split_compound(u'a_on_bright_b_c') == [u'a', u'on_bright_b', u'c']
+
+
+def test_resolve_capability(monkeypatch):
+ """Test formatters.resolve_capability and term sugaring."""
+ from blessed.formatters import resolve_capability
+
+ # given, always returns a b'seq'
+ def tigetstr(attr):
+ return ('seq-%s' % (attr,)).encode('latin1')
+ monkeypatch.setattr(curses, 'tigetstr', tigetstr)
+ term = mock.Mock()
+ term._sugar = dict(mnemonic='xyz')
+
+ # exercise
+ assert resolve_capability(term, 'mnemonic') == u'seq-xyz'
+ assert resolve_capability(term, 'natural') == u'seq-natural'
+
+ # given, where tigetstr returns None
+ def tigetstr_none(attr):
+ return None
+ monkeypatch.setattr(curses, 'tigetstr', tigetstr_none)
+
+ # exercise,
+ assert resolve_capability(term, 'natural') == u''
+
+ # given, where does_styling is False
+ def raises_exception(*args):
+ assert False, "Should not be called"
+ term.does_styling = False
+ monkeypatch.setattr(curses, 'tigetstr', raises_exception)
+
+ # exercise,
+ assert resolve_capability(term, 'natural') == u''
+
+
+def test_resolve_color(monkeypatch):
+ """Test formatters.resolve_color."""
+ from blessed.formatters import (resolve_color,
+ FormattingString,
+ NullCallableString)
+
+ def color_cap(digit):
+ return 'seq-%s' % (digit,)
+ monkeypatch.setattr(curses, 'COLOR_RED', 1984)
+
+ # given, terminal with color capabilities
+ term = mock.Mock()
+ term._background_color = color_cap
+ term._foreground_color = color_cap
+ term.number_of_colors = -1
+ term.normal = 'seq-normal'
+
+ # exercise,
+ red = resolve_color(term, 'red')
+ assert isinstance(red, FormattingString)
+ assert red == u'seq-1984'
+ assert red('text') == u'seq-1984textseq-normal'
+
+ # exercise bold, +8
+ bright_red = resolve_color(term, 'bright_red')
+ assert isinstance(bright_red, FormattingString)
+ assert bright_red == u'seq-1992'
+ assert bright_red('text') == u'seq-1992textseq-normal'
+
+ # given, terminal without color
+ term.number_of_colors = 0
+
+ # exercise,
+ red = resolve_color(term, 'red')
+ assert isinstance(red, NullCallableString)
+ assert red == u''
+ assert red('text') == u'text'
+
+ # exercise bold,
+ bright_red = resolve_color(term, 'bright_red')
+ assert isinstance(bright_red, NullCallableString)
+ assert bright_red == u''
+ assert bright_red('text') == u'text'
+
+
+def test_resolve_attribute_as_color(monkeypatch):
+ """Test simple resolve_attribte() given color name."""
+ import blessed
+ from blessed.formatters import resolve_attribute
+
+ def resolve_color(term, digit):
+ return 'seq-%s' % (digit,)
+ COLORS = set(['COLORX', 'COLORY'])
+ COMPOUNDABLES = set(['JOINT', 'COMPOUND'])
+ monkeypatch.setattr(blessed.formatters, 'resolve_color', resolve_color)
+ monkeypatch.setattr(blessed.formatters, 'COLORS', COLORS)
+ monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES)
+ term = mock.Mock()
+ assert resolve_attribute(term, 'COLORX') == u'seq-COLORX'
+
+
+def test_resolve_attribute_as_compoundable(monkeypatch):
+ """Test simple resolve_attribte() given a compoundable."""
+ import blessed
+ from blessed.formatters import resolve_attribute, FormattingString
+
+ def resolve_cap(term, digit):
+ return 'seq-%s' % (digit,)
+ COMPOUNDABLES = set(['JOINT', 'COMPOUND'])
+ monkeypatch.setattr(blessed.formatters,
+ 'resolve_capability',
+ resolve_cap)
+ monkeypatch.setattr(blessed.formatters, 'COMPOUNDABLES', COMPOUNDABLES)
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ compound = resolve_attribute(term, 'JOINT')
+ assert isinstance(compound, FormattingString)
+ assert str(compound) == u'seq-JOINT'
+ assert compound('text') == u'seq-JOINTtextseq-normal'
+
+
+def test_resolve_attribute_non_compoundables(monkeypatch):
+ """Test recursive compounding of resolve_attribute()."""
+ import blessed
+ from blessed.formatters import resolve_attribute, ParameterizingString
+
+ def uncompoundables(attr):
+ return ['split', 'compound']
+
+ def resolve_cap(term, digit):
+ return 'seq-%s' % (digit,)
+
+ monkeypatch.setattr(blessed.formatters,
+ 'split_compound',
+ uncompoundables)
+ monkeypatch.setattr(blessed.formatters,
+ 'resolve_capability',
+ resolve_cap)
+ monkeypatch.setattr(curses, 'tparm', fn_tparm)
+
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ # given
+ pstr = resolve_attribute(term, 'not-a-compoundable')
+ assert isinstance(pstr, ParameterizingString)
+ assert str(pstr) == u'seq-not-a-compoundable'
+ # this is like calling term.move_x(3)
+ assert pstr(3) == u'seq-not-a-compoundable~3'
+ # this is like calling term.move_x(3)('text')
+ assert pstr(3)('text') == u'seq-not-a-compoundable~3textseq-normal'
+
+
+def test_resolve_attribute_recursive_compoundables(monkeypatch):
+ """Test recursive compounding of resolve_attribute()."""
+ import blessed
+ from blessed.formatters import resolve_attribute, FormattingString
+
+ # patch,
+ def resolve_cap(term, digit):
+ return 'seq-%s' % (digit,)
+ monkeypatch.setattr(blessed.formatters,
+ 'resolve_capability',
+ resolve_cap)
+ monkeypatch.setattr(curses, 'tparm', fn_tparm)
+ monkeypatch.setattr(curses, 'COLOR_RED', 6502)
+ monkeypatch.setattr(curses, 'COLOR_BLUE', 6800)
+
+ def color_cap(digit):
+ return 'seq-%s' % (digit,)
+ term = mock.Mock()
+ term._background_color = color_cap
+ term._foreground_color = color_cap
+ term.normal = 'seq-normal'
+
+ # given,
+ pstr = resolve_attribute(term, 'bright_blue_on_red')
+
+ # exercise,
+ assert isinstance(pstr, FormattingString)
+ assert str(pstr) == 'seq-6808seq-6502'
+ assert pstr('text') == 'seq-6808seq-6502textseq-normal'
+
+
+def test_pickled_parameterizing_string(monkeypatch):
+ """Test pickle-ability of a formatters.ParameterizingString."""
+ from blessed.formatters import ParameterizingString
+
+ # simply send()/recv() over multiprocessing Pipe, a simple
+ # pickle.loads(dumps(...)) did not reproduce this issue,
+ from multiprocessing import Pipe
+ import pickle
+
+ # first argument to tparm() is the sequence name, returned as-is;
+ # subsequent arguments are usually Integers.
+ monkeypatch.setattr(curses, 'tparm', fn_tparm)
+
+ # given,
+ pstr = ParameterizingString(u'seqname', u'norm', u'cap-name')
+
+ # multiprocessing Pipe implicitly pickles.
+ r, w = Pipe()
+
+ # exercise picklability of ParameterizingString
+ for proto_num in range(pickle.HIGHEST_PROTOCOL):
+ assert pstr == pickle.loads(pickle.dumps(pstr, protocol=proto_num))
+ w.send(pstr)
+ assert r.recv() == pstr
+
+ # exercise picklability of FormattingString
+ # -- the return value of calling ParameterizingString
+ zero = pstr(0)
+ for proto_num in range(pickle.HIGHEST_PROTOCOL):
+ assert zero == pickle.loads(pickle.dumps(zero, protocol=proto_num))
+ w.send(zero)
+ assert r.recv() == zero
+
+
+def test_tparm_returns_null(monkeypatch):
+ """Test 'tparm() returned NULL' is caught (win32 PDCurses systems)."""
+ # on win32, any calls to tparm raises curses.error with message,
+ # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c
+ from blessed.formatters import ParameterizingString, NullCallableString
+
+ def tparm(*args):
+ raise curses.error("tparm() returned NULL")
+
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ pstr = ParameterizingString(u'cap', u'norm', u'seq-name')
+
+ value = pstr(u'x')
+ assert isinstance(value, NullCallableString)
+
+
+def test_tparm_other_exception(monkeypatch):
+ """Test 'tparm() returned NULL' is caught (win32 PDCurses systems)."""
+ # on win32, any calls to tparm raises curses.error with message,
+ # "tparm() returned NULL", function PyCurses_tparm of _cursesmodule.c
+ from blessed.formatters import ParameterizingString
+
+ def tparm(*args):
+ raise curses.error("unexpected error in tparm()")
+
+ monkeypatch.setattr(curses, 'tparm', tparm)
+
+ term = mock.Mock()
+ term.normal = 'seq-normal'
+
+ pstr = ParameterizingString(u'cap', u'norm', u'seq-name')
+
+ try:
+ pstr(u'x')
+ assert False, "previous call should have raised curses.error"
+ except curses.error:
+ pass
diff --git a/tests/test_full_keyboard.py b/tests/test_full_keyboard.py
new file mode 100644
index 00000000..c805fe66
--- /dev/null
+++ b/tests/test_full_keyboard.py
@@ -0,0 +1,672 @@
+# -*- coding: utf-8 -*-
+# std imports
+import os
+import sys
+import math
+import time
+import signal
+import platform
+
+# 3rd party
+import six
+import pytest
+
+# local
+from .accessories import (SEMAPHORE,
+ RECV_SEMAPHORE,
+ SEND_SEMAPHORE,
+ TestTerminal,
+ echo_off,
+ as_subprocess,
+ read_until_eof,
+ read_until_semaphore,
+ init_subproc_coverage)
+
+pytestmark = pytest.mark.skipif(
+ os.environ.get('TEST_KEYBOARD', None) != 'yes' or platform.system() == 'Windows',
+ reason="Timing-sensitive tests please do not run on build farms.")
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_kbhit_interrupted():
+ """kbhit() should not be interrupted with a signal handler."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0:
+ cov = init_subproc_coverage('test_kbhit_interrupted')
+
+ # child pauses, writes semaphore and begins awaiting input
+ global got_sigwinch
+ got_sigwinch = False
+
+ def on_resize(sig, action):
+ global got_sigwinch
+ got_sigwinch = True
+
+ term = TestTerminal()
+ signal.signal(signal.SIGWINCH, on_resize)
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.raw():
+ assert term.inkey(timeout=1.05) == u''
+ os.write(sys.__stdout__.fileno(), b'complete')
+ assert got_sigwinch
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.kill(pid, signal.SIGWINCH)
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'complete'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_kbhit_interrupted_nonetype():
+ """kbhit() should also allow interruption with timeout of None."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0:
+ cov = init_subproc_coverage('test_kbhit_interrupted_nonetype')
+
+ # child pauses, writes semaphore and begins awaiting input
+ global got_sigwinch
+ got_sigwinch = False
+
+ def on_resize(sig, action):
+ global got_sigwinch
+ got_sigwinch = True
+
+ term = TestTerminal()
+ signal.signal(signal.SIGWINCH, on_resize)
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.raw():
+ term.inkey(timeout=1)
+ os.write(sys.__stdout__.fileno(), b'complete')
+ assert got_sigwinch
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ time.sleep(0.05)
+ os.kill(pid, signal.SIGWINCH)
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'complete'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+
+
+def test_kbhit_no_kb():
+ """kbhit() always immediately returns False without a keyboard."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ stime = time.time()
+ assert term._keyboard_fd is None
+ assert not term.kbhit(timeout=1.1)
+ assert math.floor(time.time() - stime) == 1.0
+ child()
+
+
+def test_keystroke_0s_cbreak_noinput():
+ """0-second keystroke without input; '' should be returned."""
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=0)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 0.0)
+ child()
+
+
+def test_keystroke_0s_cbreak_noinput_nokb():
+ """0-second keystroke without data in input stream and no keyboard/tty."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=0)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 0.0)
+ child()
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_keystroke_1s_cbreak_noinput():
+ """1-second keystroke without input; '' should be returned after ~1 second."""
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=1)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 1.0)
+ child()
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_keystroke_1s_cbreak_noinput_nokb():
+ """1-second keystroke without input or keyboard."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=1)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 1.0)
+ child()
+
+
+def test_keystroke_0s_cbreak_with_input():
+ """0-second keystroke with input; Keypress should be immediately returned."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0:
+ cov = init_subproc_coverage('test_keystroke_0s_cbreak_with_input')
+ # child pauses, writes semaphore and begins awaiting input
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ inp = term.inkey(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.encode('utf-8'))
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ os.write(master_fd, u'x'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'x'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_cbreak_with_input_slowly():
+ """0-second keystroke with input; Keypress should be immediately returned."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0:
+ cov = init_subproc_coverage('test_keystroke_cbreak_with_input_slowly')
+ # child pauses, writes semaphore and begins awaiting input
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ while True:
+ inp = term.inkey(timeout=0.5)
+ os.write(sys.__stdout__.fileno(), inp.encode('utf-8'))
+ if inp == 'X':
+ break
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ os.write(master_fd, u'a'.encode('ascii'))
+ time.sleep(0.1)
+ os.write(master_fd, u'b'.encode('ascii'))
+ time.sleep(0.1)
+ os.write(master_fd, u'cdefgh'.encode('ascii'))
+ time.sleep(0.1)
+ os.write(master_fd, u'X'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'abcdefghX'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_0s_cbreak_multibyte_utf8():
+ """0-second keystroke with multibyte utf-8 input; should decode immediately."""
+ # utf-8 bytes represent "latin capital letter upsilon".
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_keystroke_0s_cbreak_multibyte_utf8')
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ inp = term.inkey(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.encode('utf-8'))
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ os.write(master_fd, u'\u01b1'.encode('utf-8'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'Ʊ'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+@pytest.mark.skipif(os.environ.get('TRAVIS', None) is not None,
+ reason="travis-ci does not handle ^C very well.")
+def test_keystroke_0s_raw_input_ctrl_c():
+ """0-second keystroke with raw allows receiving ^C."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_keystroke_0s_raw_input_ctrl_c')
+ term = TestTerminal()
+ read_until_semaphore(sys.__stdin__.fileno(), semaphore=SEMAPHORE)
+ with term.raw():
+ os.write(sys.__stdout__.fileno(), RECV_SEMAPHORE)
+ inp = term.inkey(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.encode('latin1'))
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, SEND_SEMAPHORE)
+ # ensure child is in raw mode before sending ^C,
+ read_until_semaphore(master_fd)
+ os.write(master_fd, u'\x03'.encode('latin1'))
+ stime = time.time()
+ output = read_until_eof(master_fd)
+ pid, status = os.waitpid(pid, 0)
+ assert (output == u'\x03' or
+ output == u'' and not os.isatty(0))
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+def test_keystroke_0s_cbreak_sequence():
+ """0-second keystroke with multibyte sequence; should decode immediately."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_keystroke_0s_cbreak_sequence')
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ inp = term.inkey(timeout=0)
+ os.write(sys.__stdout__.fileno(), inp.name.encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, u'\x1b[D'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ output = read_until_eof(master_fd)
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'KEY_LEFT'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_keystroke_1s_cbreak_with_input():
+ """1-second keystroke w/multibyte sequence; should return after ~1 second."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_keystroke_1s_cbreak_with_input')
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ inp = term.inkey(timeout=3)
+ os.write(sys.__stdout__.fileno(), inp.name.encode('utf-8'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ time.sleep(1)
+ os.write(master_fd, u'\x1b[C'.encode('ascii'))
+ output = read_until_eof(master_fd)
+
+ pid, status = os.waitpid(pid, 0)
+ assert output == u'KEY_RIGHT'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_esc_delay_cbreak_035():
+ """esc_delay will cause a single ESC (\\x1b) to delay for 0.35."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_esc_delay_cbreak_035')
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=5)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %i' % (inp.name, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.write(master_fd, u'\x1b'.encode('ascii'))
+ key_name, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key_name == u'KEY_ESCAPE'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+ assert 34 <= int(duration_ms) <= 45, duration_ms
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_esc_delay_cbreak_135():
+ """esc_delay=1.35 will cause a single ESC (\\x1b) to delay for 1.35."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_esc_delay_cbreak_135')
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=5, esc_delay=1.35)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %i' % (inp.name, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.write(master_fd, u'\x1b'.encode('ascii'))
+ key_name, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key_name == u'KEY_ESCAPE'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 1.0
+ assert 134 <= int(duration_ms) <= 145, int(duration_ms)
+
+
+def test_esc_delay_cbreak_timout_0():
+ """esc_delay still in effect with timeout of 0 ("nonblocking")."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_esc_delay_cbreak_timout_0')
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=0)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %i' % (inp.name, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ os.write(master_fd, u'\x1b'.encode('ascii'))
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ key_name, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key_name == u'KEY_ESCAPE'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+ assert 34 <= int(duration_ms) <= 45, int(duration_ms)
+
+
+def test_esc_delay_cbreak_nonprefix_sequence():
+ """ESC a (\\x1ba) will return an ESC immediately."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_esc_delay_cbreak_nonprefix_sequence')
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ stime = time.time()
+ esc = term.inkey(timeout=5)
+ inp = term.inkey(timeout=5)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.write(master_fd, u'\x1ba'.encode('ascii'))
+ key1_name, key2, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key1_name == u'KEY_ESCAPE'
+ assert key2 == u'a'
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+ assert -1 <= int(duration_ms) <= 15, duration_ms
+
+
+def test_esc_delay_cbreak_prefix_sequence():
+ """An unfinished multibyte sequence (\\x1b[) will delay an ESC by .35."""
+ import pty
+ pid, master_fd = pty.fork()
+ if pid == 0: # child
+ cov = init_subproc_coverage('test_esc_delay_cbreak_prefix_sequence')
+ term = TestTerminal()
+ os.write(sys.__stdout__.fileno(), SEMAPHORE)
+ with term.cbreak():
+ stime = time.time()
+ esc = term.inkey(timeout=5)
+ inp = term.inkey(timeout=5)
+ measured_time = (time.time() - stime) * 100
+ os.write(sys.__stdout__.fileno(), (
+ '%s %s %i' % (esc.name, inp, measured_time,)).encode('ascii'))
+ sys.stdout.flush()
+ if cov is not None:
+ cov.stop()
+ cov.save()
+ os._exit(0)
+
+ with echo_off(master_fd):
+ read_until_semaphore(master_fd)
+ stime = time.time()
+ os.write(master_fd, u'\x1b['.encode('ascii'))
+ key1_name, key2, duration_ms = read_until_eof(master_fd).split()
+
+ pid, status = os.waitpid(pid, 0)
+ assert key1_name == u'KEY_ESCAPE'
+ assert key2 == u'['
+ assert os.WEXITSTATUS(status) == 0
+ assert math.floor(time.time() - stime) == 0.0
+ assert 34 <= int(duration_ms) <= 45, duration_ms
+
+
+def test_get_location_0s():
+ """0-second get_location call without response."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ stime = time.time()
+ y, x = term.get_location(timeout=0)
+ assert (math.floor(time.time() - stime) == 0.0)
+ assert (y, x) == (-1, -1)
+ child()
+
+
+def test_get_location_0s_under_raw():
+ """0-second get_location call without response under raw mode."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ with term.raw():
+ stime = time.time()
+ y, x = term.get_location(timeout=0)
+ assert (math.floor(time.time() - stime) == 0.0)
+ assert (y, x) == (-1, -1)
+ child()
+
+
+def test_get_location_0s_reply_via_ungetch():
+ """0-second get_location call with response."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ stime = time.time()
+ # monkey patch in an invalid response !
+ term.ungetch(u'\x1b[10;10R')
+
+ y, x = term.get_location(timeout=0.01)
+ assert (math.floor(time.time() - stime) == 0.0)
+ assert (y, x) == (10, 10)
+ child()
+
+
+def test_get_location_0s_reply_via_ungetch_under_raw():
+ """0-second get_location call with response under raw mode."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ with term.raw():
+ stime = time.time()
+ # monkey patch in an invalid response !
+ term.ungetch(u'\x1b[10;10R')
+
+ y, x = term.get_location(timeout=0.01)
+ assert (math.floor(time.time() - stime) == 0.0)
+ assert (y, x) == (10, 10)
+ child()
+
+
+def test_kbhit_no_kb():
+ """kbhit() always immediately returns False without a keyboard."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ stime = time.time()
+ assert term._keyboard_fd is None
+ assert not term.kbhit(timeout=1.1)
+ assert math.floor(time.time() - stime) == 1.0
+ child()
+
+
+def test_keystroke_0s_cbreak_noinput():
+ """0-second keystroke without input; '' should be returned."""
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=0)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 0.0)
+ child()
+
+
+def test_keystroke_0s_cbreak_noinput_nokb():
+ """0-second keystroke without data in input stream and no keyboard/tty."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=0)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 0.0)
+ child()
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_keystroke_1s_cbreak_noinput():
+ """1-second keystroke without input; '' should be returned after ~1 second."""
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=1)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 1.0)
+ child()
+
+
+@pytest.mark.skipif(os.environ.get('TEST_QUICK', None) is not None,
+ reason="TEST_QUICK specified")
+def test_keystroke_1s_cbreak_noinput_nokb():
+ """1-second keystroke without input or keyboard."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(stream=six.StringIO())
+ with term.cbreak():
+ stime = time.time()
+ inp = term.inkey(timeout=1)
+ assert (inp == u'')
+ assert (math.floor(time.time() - stime) == 1.0)
+ child()
diff --git a/tests/test_keyboard.py b/tests/test_keyboard.py
new file mode 100644
index 00000000..b604293a
--- /dev/null
+++ b/tests/test_keyboard.py
@@ -0,0 +1,362 @@
+# -*- coding: utf-8 -*-
+"""Tests for keyboard support."""
+# std imports
+import sys
+import platform
+import tempfile
+import functools
+
+# 3rd party
+import mock
+import pytest
+
+# local
+from .accessories import TestTerminal, all_terms, as_subprocess
+
+if platform.system() != 'Windows':
+ import curses
+ import tty # NOQA
+else:
+ import jinxed as curses
+
+if sys.version_info[0] == 3:
+ unichr = chr
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="?")
+def test_break_input_no_kb():
+ """cbreak() should not call tty.setcbreak() without keyboard."""
+ @as_subprocess
+ def child():
+ with tempfile.NamedTemporaryFile() as stream:
+ term = TestTerminal(stream=stream)
+ with mock.patch("tty.setcbreak") as mock_setcbreak:
+ with term.cbreak():
+ assert not mock_setcbreak.called
+ assert term._keyboard_fd is None
+ child()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="?")
+def test_raw_input_no_kb():
+ """raw should not call tty.setraw() without keyboard."""
+ @as_subprocess
+ def child():
+ with tempfile.NamedTemporaryFile() as stream:
+ term = TestTerminal(stream=stream)
+ with mock.patch("tty.setraw") as mock_setraw:
+ with term.raw():
+ assert not mock_setraw.called
+ assert term._keyboard_fd is None
+ child()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="?")
+def test_raw_input_with_kb():
+ """raw should call tty.setraw() when with keyboard."""
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ assert term._keyboard_fd is not None
+ with mock.patch("tty.setraw") as mock_setraw:
+ with term.raw():
+ assert mock_setraw.called
+ child()
+
+
+def test_notty_kb_is_None():
+ """term._keyboard_fd should be None when os.isatty returns False."""
+ # in this scenerio, stream is sys.__stdout__,
+ # but os.isatty(0) is False,
+ # such as when piping output to less(1)
+ @as_subprocess
+ def child():
+ with mock.patch("os.isatty") as mock_isatty:
+ mock_isatty.return_value = False
+ term = TestTerminal()
+ assert term._keyboard_fd is None
+ child()
+
+
+def test_keystroke_default_args():
+ """Test keyboard.Keystroke constructor with default arguments."""
+ from blessed.keyboard import Keystroke
+ ks = Keystroke()
+ assert ks._name is None
+ assert ks.name == ks._name
+ assert ks._code is None
+ assert ks.code == ks._code
+ assert u'x' == u'x' + ks
+ assert not ks.is_sequence
+ assert repr(ks) in ("u''", # py26, 27
+ "''",) # py33
+
+
+def test_a_keystroke():
+ """Test keyboard.Keystroke constructor with set arguments."""
+ from blessed.keyboard import Keystroke
+ ks = Keystroke(ucs=u'x', code=1, name=u'the X')
+ assert ks._name == u'the X'
+ assert ks.name == ks._name
+ assert ks._code == 1
+ assert ks.code == ks._code
+ assert u'xx' == u'x' + ks
+ assert ks.is_sequence
+ assert repr(ks) == "the X"
+
+
+def test_get_keyboard_codes():
+ """Test all values returned by get_keyboard_codes are from curses."""
+ import blessed.keyboard
+ exemptions = dict(blessed.keyboard.CURSES_KEYCODE_OVERRIDE_MIXIN)
+ for value, keycode in blessed.keyboard.get_keyboard_codes().items():
+ if keycode in exemptions:
+ assert value == exemptions[keycode]
+ continue
+ if keycode[4:] in blessed.keyboard._CURSES_KEYCODE_ADDINS:
+ assert not hasattr(curses, keycode)
+ assert hasattr(blessed.keyboard, keycode)
+ assert getattr(blessed.keyboard, keycode) == value
+ else:
+ assert hasattr(curses, keycode)
+ assert getattr(curses, keycode) == value
+
+
+def test_alternative_left_right():
+ """Test _alternative_left_right behavior for space/backspace."""
+ from blessed.keyboard import _alternative_left_right
+ term = mock.Mock()
+ term._cuf1 = u''
+ term._cub1 = u''
+ assert not bool(_alternative_left_right(term))
+ term._cuf1 = u' '
+ term._cub1 = u'\b'
+ assert not bool(_alternative_left_right(term))
+ term._cuf1 = u'seq-right'
+ term._cub1 = u'seq-left'
+ assert (_alternative_left_right(term) == {
+ u'seq-right': curses.KEY_RIGHT,
+ u'seq-left': curses.KEY_LEFT})
+
+
+def test_cuf1_and_cub1_as_RIGHT_LEFT(all_terms):
+ """Test that cuf1 and cub1 are assigned KEY_RIGHT and KEY_LEFT."""
+ from blessed.keyboard import get_keyboard_sequences
+
+ @as_subprocess
+ def child(kind):
+ term = TestTerminal(kind=kind, force_styling=True)
+ keymap = get_keyboard_sequences(term)
+ if term._cuf1:
+ assert term._cuf1 in keymap
+ assert keymap[term._cuf1] == term.KEY_RIGHT
+ if term._cub1:
+ assert term._cub1 in keymap
+ if term._cub1 == '\b':
+ assert keymap[term._cub1] == term.KEY_BACKSPACE
+ else:
+ assert keymap[term._cub1] == term.KEY_LEFT
+
+ child(all_terms)
+
+
+def test_get_keyboard_sequences_sort_order():
+ """ordereddict ensures sequences are ordered longest-first."""
+ @as_subprocess
+ def child(kind):
+ term = TestTerminal(kind=kind, force_styling=True)
+ maxlen = None
+ for sequence, code in term._keymap.items():
+ if maxlen is not None:
+ assert len(sequence) <= maxlen
+ assert sequence
+ maxlen = len(sequence)
+ kind = 'xterm-256color'
+ if platform.system() == 'Windows':
+ kind = 'vtwin10'
+ child(kind)
+
+
+def test_get_keyboard_sequence(monkeypatch):
+ """Test keyboard.get_keyboard_sequence."""
+ import blessed.keyboard
+
+ (KEY_SMALL, KEY_LARGE, KEY_MIXIN) = range(3)
+ (CAP_SMALL, CAP_LARGE) = 'cap-small cap-large'.split()
+ (SEQ_SMALL, SEQ_LARGE, SEQ_MIXIN, SEQ_ALT_CUF1, SEQ_ALT_CUB1) = (
+ b'seq-small-a',
+ b'seq-large-abcdefg',
+ b'seq-mixin',
+ b'seq-alt-cuf1',
+ b'seq-alt-cub1_')
+
+ # patch curses functions
+ monkeypatch.setattr(curses, 'tigetstr',
+ lambda cap: {CAP_SMALL: SEQ_SMALL,
+ CAP_LARGE: SEQ_LARGE}[cap])
+
+ monkeypatch.setattr(blessed.keyboard, 'capability_names',
+ dict(((KEY_SMALL, CAP_SMALL,),
+ (KEY_LARGE, CAP_LARGE,))))
+
+ # patch global sequence mix-in
+ monkeypatch.setattr(blessed.keyboard,
+ 'DEFAULT_SEQUENCE_MIXIN', (
+ (SEQ_MIXIN.decode('latin1'), KEY_MIXIN),))
+
+ # patch for _alternative_left_right
+ term = mock.Mock()
+ term._cuf1 = SEQ_ALT_CUF1.decode('latin1')
+ term._cub1 = SEQ_ALT_CUB1.decode('latin1')
+ keymap = blessed.keyboard.get_keyboard_sequences(term)
+
+ assert list(keymap.items()) == [
+ (SEQ_LARGE.decode('latin1'), KEY_LARGE),
+ (SEQ_ALT_CUB1.decode('latin1'), curses.KEY_LEFT),
+ (SEQ_ALT_CUF1.decode('latin1'), curses.KEY_RIGHT),
+ (SEQ_SMALL.decode('latin1'), KEY_SMALL),
+ (SEQ_MIXIN.decode('latin1'), KEY_MIXIN)]
+
+
+def test_resolve_sequence():
+ """Test resolve_sequence for order-dependent mapping."""
+ from blessed.keyboard import resolve_sequence, OrderedDict
+ mapper = OrderedDict(((u'SEQ1', 1),
+ (u'SEQ2', 2),
+ # takes precedence over LONGSEQ, first-match
+ (u'KEY_LONGSEQ_longest', 3),
+ (u'LONGSEQ', 4),
+ # wont match, LONGSEQ is first-match in this order
+ (u'LONGSEQ_longer', 5),
+ # falls through for L{anything_else}
+ (u'L', 6)))
+ codes = {1: u'KEY_SEQ1',
+ 2: u'KEY_SEQ2',
+ 3: u'KEY_LONGSEQ_longest',
+ 4: u'KEY_LONGSEQ',
+ 5: u'KEY_LONGSEQ_longer',
+ 6: u'KEY_L'}
+ ks = resolve_sequence(u'', mapper, codes)
+ assert ks == u''
+ assert ks.name is None
+ assert ks.code is None
+ assert not ks.is_sequence
+ assert repr(ks) in ("u''", # py26, 27
+ "''",) # py33
+
+ ks = resolve_sequence(u'notfound', mapper=mapper, codes=codes)
+ assert ks == u'n'
+ assert ks.name is None
+ assert ks.code is None
+ assert not ks.is_sequence
+ assert repr(ks) in (u"u'n'", "'n'",)
+
+ ks = resolve_sequence(u'SEQ1', mapper, codes)
+ assert ks == u'SEQ1'
+ assert ks.name == u'KEY_SEQ1'
+ assert ks.code == 1
+ assert ks.is_sequence
+ assert repr(ks) in (u"KEY_SEQ1", "KEY_SEQ1")
+
+ ks = resolve_sequence(u'LONGSEQ_longer', mapper, codes)
+ assert ks == u'LONGSEQ'
+ assert ks.name == u'KEY_LONGSEQ'
+ assert ks.code == 4
+ assert ks.is_sequence
+ assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ")
+
+ ks = resolve_sequence(u'LONGSEQ', mapper, codes)
+ assert ks == u'LONGSEQ'
+ assert ks.name == u'KEY_LONGSEQ'
+ assert ks.code == 4
+ assert ks.is_sequence
+ assert repr(ks) in (u"KEY_LONGSEQ", "KEY_LONGSEQ")
+
+ ks = resolve_sequence(u'Lxxxxx', mapper, codes)
+ assert ks == u'L'
+ assert ks.name == u'KEY_L'
+ assert ks.code == 6
+ assert ks.is_sequence
+ assert repr(ks) in (u"KEY_L", "KEY_L")
+
+
+def test_keyboard_prefixes():
+ """Test keyboard.prefixes."""
+ from blessed.keyboard import get_leading_prefixes
+ keys = ['abc', 'abdf', 'e', 'jkl']
+ pfs = get_leading_prefixes(keys)
+ assert pfs == set([u'a', u'ab', u'abd', u'j', u'jk'])
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="no multiprocess")
+def test_keypad_mixins_and_aliases():
+ """Test PC-Style function key translations when in ``keypad`` mode."""
+ # Key plain app modified
+ # Up ^[[A ^[OA ^[[1;mA
+ # Down ^[[B ^[OB ^[[1;mB
+ # Right ^[[C ^[OC ^[[1;mC
+ # Left ^[[D ^[OD ^[[1;mD
+ # End ^[[F ^[OF ^[[1;mF
+ # Home ^[[H ^[OH ^[[1;mH
+ @as_subprocess
+ def child(kind):
+ term = TestTerminal(kind=kind, force_styling=True)
+ from blessed.keyboard import resolve_sequence
+
+ resolve = functools.partial(resolve_sequence,
+ mapper=term._keymap,
+ codes=term._keycodes)
+
+ assert resolve(unichr(10)).name == "KEY_ENTER"
+ assert resolve(unichr(13)).name == "KEY_ENTER"
+ assert resolve(unichr(8)).name == "KEY_BACKSPACE"
+ assert resolve(unichr(9)).name == "KEY_TAB"
+ assert resolve(unichr(27)).name == "KEY_ESCAPE"
+ assert resolve(unichr(127)).name == "KEY_BACKSPACE"
+ assert resolve(u"\x1b[A").name == "KEY_UP"
+ assert resolve(u"\x1b[B").name == "KEY_DOWN"
+ assert resolve(u"\x1b[C").name == "KEY_RIGHT"
+ assert resolve(u"\x1b[D").name == "KEY_LEFT"
+ assert resolve(u"\x1b[U").name == "KEY_PGDOWN"
+ assert resolve(u"\x1b[V").name == "KEY_PGUP"
+ assert resolve(u"\x1b[H").name == "KEY_HOME"
+ assert resolve(u"\x1b[F").name == "KEY_END"
+ assert resolve(u"\x1b[K").name == "KEY_END"
+ assert resolve(u"\x1bOM").name == "KEY_ENTER"
+ assert resolve(u"\x1bOj").name == "KEY_KP_MULTIPLY"
+ assert resolve(u"\x1bOk").name == "KEY_KP_ADD"
+ assert resolve(u"\x1bOl").name == "KEY_KP_SEPARATOR"
+ assert resolve(u"\x1bOm").name == "KEY_KP_SUBTRACT"
+ assert resolve(u"\x1bOn").name == "KEY_KP_DECIMAL"
+ assert resolve(u"\x1bOo").name == "KEY_KP_DIVIDE"
+ assert resolve(u"\x1bOX").name == "KEY_KP_EQUAL"
+ assert resolve(u"\x1bOp").name == "KEY_KP_0"
+ assert resolve(u"\x1bOq").name == "KEY_KP_1"
+ assert resolve(u"\x1bOr").name == "KEY_KP_2"
+ assert resolve(u"\x1bOs").name == "KEY_KP_3"
+ assert resolve(u"\x1bOt").name == "KEY_KP_4"
+ assert resolve(u"\x1bOu").name == "KEY_KP_5"
+ assert resolve(u"\x1bOv").name == "KEY_KP_6"
+ assert resolve(u"\x1bOw").name == "KEY_KP_7"
+ assert resolve(u"\x1bOx").name == "KEY_KP_8"
+ assert resolve(u"\x1bOy").name == "KEY_KP_9"
+ assert resolve(u"\x1b[1~").name == "KEY_FIND"
+ assert resolve(u"\x1b[2~").name == "KEY_INSERT"
+ assert resolve(u"\x1b[3~").name == "KEY_DELETE"
+ assert resolve(u"\x1b[4~").name == "KEY_SELECT"
+ assert resolve(u"\x1b[5~").name == "KEY_PGUP"
+ assert resolve(u"\x1b[6~").name == "KEY_PGDOWN"
+ assert resolve(u"\x1b[7~").name == "KEY_HOME"
+ assert resolve(u"\x1b[8~").name == "KEY_END"
+ assert resolve(u"\x1b[OA").name == "KEY_UP"
+ assert resolve(u"\x1b[OB").name == "KEY_DOWN"
+ assert resolve(u"\x1b[OC").name == "KEY_RIGHT"
+ assert resolve(u"\x1b[OD").name == "KEY_LEFT"
+ assert resolve(u"\x1b[OF").name == "KEY_END"
+ assert resolve(u"\x1b[OH").name == "KEY_HOME"
+ assert resolve(u"\x1bOP").name == "KEY_F1"
+ assert resolve(u"\x1bOQ").name == "KEY_F2"
+ assert resolve(u"\x1bOR").name == "KEY_F3"
+ assert resolve(u"\x1bOS").name == "KEY_F4"
+
+ child('xterm')
diff --git a/tests/test_length_sequence.py b/tests/test_length_sequence.py
new file mode 100644
index 00000000..33bea28c
--- /dev/null
+++ b/tests/test_length_sequence.py
@@ -0,0 +1,437 @@
+# encoding: utf-8
+# std imports
+import os
+import sys
+import struct
+import platform
+import itertools
+
+# 3rd party
+import six
+import pytest
+
+from .accessories import ( # isort:skip
+ TestTerminal, as_subprocess, all_terms, many_lines, many_columns)
+
+if platform.system() != 'Windows':
+ import fcntl
+ import termios
+
+
+def test_length_cjk():
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+
+ # given,
+ given = term.bold_red(u'コンニチハ, セカイ!')
+ expected = sum((2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1,))
+
+ # exercise,
+ assert term.length(given) == expected
+
+ child()
+
+
+def test_length_ansiart():
+ @as_subprocess
+ def child(kind):
+ import codecs
+ term = TestTerminal(kind=kind)
+ # this 'ansi' art contributed by xzip!impure for another project,
+ # unlike most CP-437 DOS ansi art, this is actually utf-8 encoded.
+ fname = os.path.join(os.path.dirname(__file__), 'wall.ans')
+ lines = codecs.open(fname, 'r', 'utf-8').readlines()
+ assert term.length(lines[0]) == 67 # ^[[64C^[[34m▄▓▄
+ assert term.length(lines[1]) == 75
+ assert term.length(lines[2]) == 78
+ assert term.length(lines[3]) == 78
+ assert term.length(lines[4]) == 78
+ assert term.length(lines[5]) == 78
+ assert term.length(lines[6]) == 77
+ kind = 'xterm-256color'
+ if platform.system() == 'Windows':
+ kind = 'vtwin10'
+ child(kind)
+
+
+def test_sequence_length(all_terms):
+ """Ensure T.length(string containing sequence) is correcterm."""
+ @as_subprocess
+ def child(kind):
+ term = TestTerminal(kind=kind, force_styling=True)
+ # Create a list of ascii characters, to be separated
+ # by word, to be zipped up with a cycling list of
+ # terminal sequences. Then, compare the length of
+ # each, the basic plain_texterm.__len__ vs. the Terminal
+ # method length. They should be equal.
+ plain_text = (u'The softest things of the world '
+ u'Override the hardest things of the world '
+ u'That which has no substance '
+ u'Enters into that which has no openings')
+ if term.bold:
+ assert (term.length(term.bold) == 0)
+ assert (term.length(term.bold(u'x')) == 1)
+ assert (term.length(term.bold_red) == 0)
+ assert (term.length(term.bold_red(u'x')) == 1)
+ assert (term.strip(term.bold) == u'')
+ assert (term.rstrip(term.bold) == u'')
+ assert (term.lstrip(term.bold) == u'')
+ assert (term.strip(term.bold(u' x ')) == u'x')
+ assert (term.strip(term.bold(u'z x q'), 'zq') == u' x ')
+ assert (term.rstrip(term.bold(u' x ')) == u' x')
+ assert (term.lstrip(term.bold(u' x ')) == u'x ')
+ assert (term.strip(term.bold_red) == u'')
+ assert (term.rstrip(term.bold_red) == u'')
+ assert (term.lstrip(term.bold_red) == u'')
+ assert (term.strip(term.bold_red(u' x ')) == u'x')
+ assert (term.rstrip(term.bold_red(u' x ')) == u' x')
+ assert (term.lstrip(term.bold_red(u' x ')) == u'x ')
+ assert (term.strip_seqs(term.bold) == u'')
+ assert (term.strip_seqs(term.bold(u' x ')) == u' x ')
+ assert (term.strip_seqs(term.bold_red) == u'')
+ assert (term.strip_seqs(term.bold_red(u' x ')) == u' x ')
+
+ if term.underline:
+ assert (term.length(term.underline) == 0)
+ assert (term.length(term.underline(u'x')) == 1)
+ assert (term.length(term.underline_red) == 0)
+ assert (term.length(term.underline_red(u'x')) == 1)
+ assert (term.strip(term.underline) == u'')
+ assert (term.strip(term.underline(u' x ')) == u'x')
+ assert (term.strip(term.underline_red) == u'')
+ assert (term.strip(term.underline_red(u' x ')) == u'x')
+ assert (term.rstrip(term.underline_red(u' x ')) == u' x')
+ assert (term.lstrip(term.underline_red(u' x ')) == u'x ')
+ assert (term.strip_seqs(term.underline) == u'')
+ assert (term.strip_seqs(term.underline(u' x ')) == u' x ')
+ assert (term.strip_seqs(term.underline_red) == u'')
+ assert (term.strip_seqs(term.underline_red(u' x ')) == u' x ')
+
+ if term.reverse:
+ assert (term.length(term.reverse) == 0)
+ assert (term.length(term.reverse(u'x')) == 1)
+ assert (term.length(term.reverse_red) == 0)
+ assert (term.length(term.reverse_red(u'x')) == 1)
+ assert (term.strip(term.reverse) == u'')
+ assert (term.strip(term.reverse(u' x ')) == u'x')
+ assert (term.strip(term.reverse_red) == u'')
+ assert (term.strip(term.reverse_red(u' x ')) == u'x')
+ assert (term.rstrip(term.reverse_red(u' x ')) == u' x')
+ assert (term.lstrip(term.reverse_red(u' x ')) == u'x ')
+ assert (term.strip_seqs(term.reverse) == u'')
+ assert (term.strip_seqs(term.reverse(u' x ')) == u' x ')
+ assert (term.strip_seqs(term.reverse_red) == u'')
+ assert (term.strip_seqs(term.reverse_red(u' x ')) == u' x ')
+
+ if term.blink:
+ assert (term.length(term.blink) == 0)
+ assert (term.length(term.blink(u'x')) == 1)
+ assert (term.length(term.blink_red) == 0)
+ assert (term.length(term.blink_red(u'x')) == 1)
+ assert (term.strip(term.blink) == u'')
+ assert (term.strip(term.blink(u' x ')) == u'x')
+ assert (term.strip(term.blink(u'z x q'), u'zq') == u' x ')
+ assert (term.strip(term.blink_red) == u'')
+ assert (term.strip(term.blink_red(u' x ')) == u'x')
+ assert (term.strip_seqs(term.blink) == u'')
+ assert (term.strip_seqs(term.blink(u' x ')) == u' x ')
+ assert (term.strip_seqs(term.blink_red) == u'')
+ assert (term.strip_seqs(term.blink_red(u' x ')) == u' x ')
+
+ if term.home:
+ assert (term.length(term.home) == 0)
+ assert (term.strip(term.home) == u'')
+ if term.clear_eol:
+ assert (term.length(term.clear_eol) == 0)
+ assert (term.strip(term.clear_eol) == u'')
+ if term.enter_fullscreen:
+ assert (term.length(term.enter_fullscreen) == 0)
+ assert (term.strip(term.enter_fullscreen) == u'')
+ if term.exit_fullscreen:
+ assert (term.length(term.exit_fullscreen) == 0)
+ assert (term.strip(term.exit_fullscreen) == u'')
+
+ # horizontally, we decide move_down and move_up are 0,
+ assert (term.length(term.move_down) == 0)
+ assert (term.length(term.move_down(2)) == 0)
+ assert (term.length(term.move_up) == 0)
+ assert (term.length(term.move_up(2)) == 0)
+
+ # other things aren't so simple, somewhat edge cases,
+ # moving backwards and forwards horizontally must be
+ # accounted for as a "length", as
+ # will result in a printed column length of 12 (even
+ # though columns 2-11 are non-destructive space
+ assert (term.length(u'x\b') == 0)
+ assert (term.strip(u'x\b') == u'')
+
+ # XXX why are some terminals width of 9 here ??
+ assert (term.length(u'\t') in (8, 9))
+ assert (term.strip(u'\t') == u'')
+
+ assert (term.length(u'_' + term.move_left) == 0)
+ assert (term.length(term.move_right) == 1)
+
+ if term.cub:
+ assert (term.length((u'_' * 10) + term.cub(10)) == 0)
+
+ if term.cuf:
+ assert (term.length(term.cuf(10)) == 10)
+
+ # vertical spacing is unaccounted as a 'length'
+ assert (term.length(term.move_up) == 0)
+ assert (term.length(term.cuu(10)) == 0)
+ assert (term.length(term.move_down) == 0)
+ assert (term.length(term.cud(10)) == 0)
+
+ # this is how manpages perform underlining, this is done
+ # with the 'overstrike' capability of teletypes, and aparently
+ # less(1), '123' -> '1\b_2\b_3\b_'
+ text_wseqs = u''.join(itertools.chain(
+ *zip(plain_text, itertools.cycle(['\b_']))))
+ assert (term.length(text_wseqs) == len(plain_text))
+
+ child(all_terms)
+
+
+def test_env_winsize():
+ """Test height and width is appropriately queried in a pty."""
+ @as_subprocess
+ def child():
+ # set the pty's virtual window size
+ os.environ['COLUMNS'] = '99'
+ os.environ['LINES'] = '11'
+ term = TestTerminal(stream=six.StringIO())
+ save_init = term._init_descriptor
+ save_stdout = sys.__stdout__
+ try:
+ term._init_descriptor = None
+ sys.__stdout__ = None
+ winsize = term._height_and_width()
+ width = term.width
+ height = term.height
+ finally:
+ term._init_descriptor = save_init
+ sys.__stdout__ = save_stdout
+ assert winsize.ws_col == width == 99
+ assert winsize.ws_row == height == 11
+
+ child()
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl")
+def test_winsize(many_lines, many_columns):
+ """Test height and width is appropriately queried in a pty."""
+ @as_subprocess
+ def child(lines=25, cols=80):
+ # set the pty's virtual window size
+ val = struct.pack('HHHH', lines, cols, 0, 0)
+ fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val)
+ term = TestTerminal()
+ winsize = term._height_and_width()
+ assert term.width == cols
+ assert term.height == lines
+ assert winsize.ws_col == cols
+ assert winsize.ws_row == lines
+
+ child(lines=many_lines, cols=many_columns)
+
+
+def test_Sequence_alignment_fixed_width():
+ @as_subprocess
+ def child(kind):
+ term = TestTerminal(kind=kind)
+ pony_msg = 'pony express, all aboard, choo, choo!'
+ pony_len = len(pony_msg)
+ pony_colored = u''.join(
+ ['%s%s' % (term.color(n % 7), ch,)
+ for n, ch in enumerate(pony_msg)])
+ pony_colored += term.normal
+ ladjusted = term.ljust(pony_colored, 88)
+ radjusted = term.rjust(pony_colored, 88)
+ centered = term.center(pony_colored, 88)
+ assert (term.length(pony_colored) == pony_len)
+ assert (term.length(centered.strip()) == pony_len)
+ assert (term.length(centered) == len(pony_msg.center(88)))
+ assert (term.length(ladjusted.strip()) == pony_len)
+ assert (term.length(ladjusted) == len(pony_msg.ljust(88)))
+ assert (term.length(radjusted.strip()) == pony_len)
+ assert (term.length(radjusted) == len(pony_msg.rjust(88)))
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires fcntl")
+def test_Sequence_alignment(all_terms):
+ """Tests methods related to Sequence class, namely ljust, rjust, center."""
+ @as_subprocess
+ def child(kind, lines=25, cols=80):
+ # set the pty's virtual window size
+ val = struct.pack('HHHH', lines, cols, 0, 0)
+ fcntl.ioctl(sys.__stdout__.fileno(), termios.TIOCSWINSZ, val)
+ term = TestTerminal(kind=kind)
+
+ pony_msg = 'pony express, all aboard, choo, choo!'
+ pony_len = len(pony_msg)
+ pony_colored = u''.join(
+ ['%s%s' % (term.color(n % 7), ch,)
+ for n, ch in enumerate(pony_msg)])
+ pony_colored += term.normal
+ ladjusted = term.ljust(pony_colored)
+ radjusted = term.rjust(pony_colored)
+ centered = term.center(pony_colored)
+ assert (term.length(pony_colored) == pony_len)
+ assert (term.length(centered.strip()) == pony_len)
+ assert (term.length(centered) == len(pony_msg.center(term.width)))
+ assert (term.length(ladjusted.strip()) == pony_len)
+ assert (term.length(ladjusted) == len(pony_msg.ljust(term.width)))
+ assert (term.length(radjusted.strip()) == pony_len)
+ assert (term.length(radjusted) == len(pony_msg.rjust(term.width)))
+
+ child(kind=all_terms)
+
+
+def test_sequence_is_movement_false(all_terms):
+ """Test parser about sequences that do not move the cursor."""
+ @as_subprocess
+ def child(kind):
+ from blessed.sequences import measure_length
+ term = TestTerminal(kind=kind)
+ assert (0 == measure_length(u'', term))
+ # not even a mbs
+ assert (0 == measure_length(u'xyzzy', term))
+ # negative numbers, though printable as %d, do not result
+ # in movement; just garbage. Also not a valid sequence.
+ assert (0 == measure_length(term.cuf(-333), term))
+ assert (len(term.clear_eol) == measure_length(term.clear_eol, term))
+ # various erases don't *move*
+ assert (len(term.clear_bol) == measure_length(term.clear_bol, term))
+ assert (len(term.clear_eos) == measure_length(term.clear_eos, term))
+ assert (len(term.bold) == measure_length(term.bold, term))
+ # various paints don't move
+ assert (len(term.red) == measure_length(term.red, term))
+ assert (len(term.civis) == measure_length(term.civis, term))
+ if term.cvvis:
+ assert (len(term.cvvis) == measure_length(term.cvvis, term))
+ assert (len(term.underline) == measure_length(term.underline, term))
+ assert (len(term.reverse) == measure_length(term.reverse, term))
+ for _num in (0, term.number_of_colors):
+ expected = len(term.color(_num))
+ given = measure_length(term.color(_num), term)
+ assert (expected == given)
+ assert (len(term.normal_cursor) == measure_length(term.normal_cursor, term))
+ assert (len(term.hide_cursor) == measure_length(term.hide_cursor, term))
+ assert (len(term.save) == measure_length(term.save, term))
+ assert (len(term.italic) == measure_length(term.italic, term))
+ assert (len(term.standout) == measure_length(term.standout, term)
+ ), (term.standout, term._wont_move)
+
+ child(all_terms)
+
+
+def test_termcap_will_move_false(all_terms):
+ """Test parser about sequences that do not move the cursor."""
+ @as_subprocess
+ def child(kind):
+ from blessed.sequences import iter_parse
+ term = TestTerminal(kind=kind)
+ if term.clear_eol:
+ assert not next(iter_parse(term, term.clear_eol))[1].will_move
+ if term.clear_bol:
+ assert not next(iter_parse(term, term.clear_bol))[1].will_move
+ if term.clear_eos:
+ assert not next(iter_parse(term, term.clear_eos))[1].will_move
+ if term.bold:
+ assert not next(iter_parse(term, term.bold))[1].will_move
+ if term.red:
+ assert not next(iter_parse(term, term.red))[1].will_move
+ if term.civis:
+ assert not next(iter_parse(term, term.civis))[1].will_move
+ if term.cvvis:
+ assert not next(iter_parse(term, term.cvvis))[1].will_move
+ if term.underline:
+ assert not next(iter_parse(term, term.underline))[1].will_move
+ if term.reverse:
+ assert not next(iter_parse(term, term.reverse))[1].will_move
+ if term.color(0):
+ assert not next(iter_parse(term, term.color(0)))[1].will_move
+ if term.normal_cursor:
+ assert not next(iter_parse(term, term.normal_cursor))[1].will_move
+ if term.save:
+ assert not next(iter_parse(term, term.save))[1].will_move
+ if term.italic:
+ assert not next(iter_parse(term, term.italic))[1].will_move
+ if term.standout:
+ assert not next(iter_parse(term, term.standout))[1].will_move
+
+ child(all_terms)
+
+
+def test_sequence_is_movement_true(all_terms):
+ """Test parsers about sequences that move the cursor."""
+ @as_subprocess
+ def child(kind):
+ from blessed.sequences import measure_length
+ term = TestTerminal(kind=kind)
+ # movements
+ assert (len(term.move(98, 76)) ==
+ measure_length(term.move(98, 76), term))
+ assert (len(term.move(54)) ==
+ measure_length(term.move(54), term))
+ assert (len(term.move_xy(1, 2)) ==
+ measure_length(term.move(1, 2), term))
+ assert (len(term.move_yx(3, 4)) ==
+ measure_length(term.move(3, 4), term))
+ assert not term.cud1 or (len(term.cud1) ==
+ measure_length(term.cud1, term))
+ assert not term.cub1 or (len(term.cub1) ==
+ measure_length(term.cub1, term))
+ assert not term.cuf1 or (len(term.cuf1) ==
+ measure_length(term.cuf1, term))
+ assert not term.cuu1 or (len(term.cuu1) ==
+ measure_length(term.cuu1, term))
+ assert not term.cub or (len(term.cub(333)) ==
+ measure_length(term.cub(333), term))
+ assert not term.cuf or (len(term.cuf(333)) ==
+ measure_length(term.cuf(333), term))
+ assert not term.home or (len(term.home) ==
+ measure_length(term.home, term))
+ assert not term.restore or (len(term.restore) ==
+ measure_length(term.restore, term))
+ assert not term.clear or (len(term.clear) ==
+ measure_length(term.clear, term))
+
+ child(all_terms)
+
+
+def test_termcap_will_move_true(all_terms):
+ """Test parser about sequences that move the cursor."""
+ @as_subprocess
+ def child(kind):
+ from blessed.sequences import iter_parse
+ term = TestTerminal(kind=kind, force_styling=True)
+ assert next(iter_parse(term, term.move(98, 76)))[1].will_move
+ assert next(iter_parse(term, term.move_yx(8, 76)))[1].will_move
+ assert next(iter_parse(term, term.move_xy(98, 7)))[1].will_move
+ assert next(iter_parse(term, term.move(54)))[1].will_move
+ assert next(iter_parse(term, term.cud1))[1].will_move
+ assert next(iter_parse(term, term.cub1))[1].will_move
+ assert next(iter_parse(term, term.cuf1))[1].will_move
+ assert next(iter_parse(term, term.cuu1))[1].will_move
+ if term.cub(333):
+ assert next(iter_parse(term, term.cub(333)))[1].will_move
+ if term.cuf(333):
+ assert next(iter_parse(term, term.cuf(333)))[1].will_move
+ assert next(iter_parse(term, term.home))[1].will_move
+ assert next(iter_parse(term, term.restore))[1].will_move
+ assert next(iter_parse(term, term.clear))[1].will_move
+ child(all_terms)
+
+
+def test_foreign_sequences():
+ """Test parsers about sequences received from foreign sources."""
+ @as_subprocess
+ def child(kind):
+ from blessed.sequences import measure_length
+ term = TestTerminal(kind=kind)
+ assert measure_length(u'\x1b[m', term) == len('\x1b[m')
+ child(kind='ansi')
diff --git a/tests/test_sequences.py b/tests/test_sequences.py
new file mode 100644
index 00000000..b4af8bc5
--- /dev/null
+++ b/tests/test_sequences.py
@@ -0,0 +1,514 @@
+# -*- coding: utf-8 -*-
+"""Tests for Terminal() sequences and sequence-awareness."""
+# std imports
+import sys
+import platform
+
+# 3rd party
+import six
+import pytest
+
+# local
+from .accessories import TestTerminal, all_terms, unicode_cap, unicode_parm, as_subprocess
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires real tty")
+def test_capability():
+ """Check that capability lookup works."""
+ @as_subprocess
+ def child():
+ # Also test that Terminal grabs a reasonable default stream.
+ t = TestTerminal()
+ sc = unicode_cap('sc')
+ assert t.save == sc
+ assert t.save == sc # Make sure caching doesn't screw it up.
+
+ child()
+
+
+def test_capability_without_tty():
+ """Assert capability templates are '' when stream is not a tty."""
+ @as_subprocess
+ def child():
+ t = TestTerminal(stream=six.StringIO())
+ assert t.save == u''
+ assert t.red == u''
+
+ child()
+
+
+def test_capability_with_forced_tty():
+ """force styling should return sequences even for non-ttys."""
+ @as_subprocess
+ def child():
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
+ assert t.save == unicode_cap('sc')
+
+ child()
+
+
+def test_parametrization():
+ """Test parameterizing a capability."""
+ @as_subprocess
+ def child():
+ term = TestTerminal(force_styling=True)
+ assert term.cup(3, 4) == unicode_parm('cup', 3, 4)
+
+ child()
+
+
+def test_height_and_width():
+ """Assert that ``height_and_width()`` returns full integers."""
+ @as_subprocess
+ def child():
+ t = TestTerminal() # kind shouldn't matter.
+ assert isinstance(t.height, int)
+ assert isinstance(t.width, int)
+
+ child()
+
+
+def test_stream_attr():
+ """Make sure Terminal ``stream`` is stdout by default."""
+ @as_subprocess
+ def child():
+ assert TestTerminal().stream == sys.__stdout__
+
+ child()
+
+
+def test_location_with_styling(all_terms):
+ """Make sure ``location()`` works on all terminals."""
+ @as_subprocess
+ def child_with_styling(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ with t.location(3, 4):
+ t.stream.write(u'hi')
+ expected_output = u''.join(
+ (unicode_cap('sc') or u'\x1b[s',
+ unicode_parm('cup', 4, 3),
+ u'hi',
+ unicode_cap('rc') or u'\x1b[u'))
+ assert (t.stream.getvalue() == expected_output)
+
+ child_with_styling(all_terms)
+
+
+def test_location_without_styling():
+ """Make sure ``location()`` silently passes without styling."""
+ @as_subprocess
+ def child_without_styling():
+ """No side effect for location as a context manager without styling."""
+ t = TestTerminal(stream=six.StringIO(), force_styling=None)
+
+ with t.location(3, 4):
+ t.stream.write(u'hi')
+
+ assert t.stream.getvalue() == u'hi'
+
+ child_without_styling()
+
+
+def test_horizontal_location(all_terms):
+ """Make sure we can move the cursor horizontally without changing rows."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ with t.location(x=5):
+ pass
+ _hpa = unicode_parm('hpa', 5)
+ if not _hpa and (kind.startswith('screen') or
+ kind.startswith('ansi')):
+ _hpa = u'\x1b[6G'
+ expected_output = u''.join(
+ (unicode_cap('sc') or u'\x1b[s',
+ _hpa,
+ unicode_cap('rc') or u'\x1b[u'))
+ assert (t.stream.getvalue() == expected_output), (
+ repr(t.stream.getvalue()), repr(expected_output))
+
+ child(all_terms)
+
+
+def test_vertical_location(all_terms):
+ """Make sure we can move the cursor horizontally without changing rows."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ with t.location(y=5):
+ pass
+ _vpa = unicode_parm('vpa', 5)
+ if not _vpa and (kind.startswith('screen') or
+ kind.startswith('ansi')):
+ _vpa = u'\x1b[6d'
+
+ expected_output = u''.join(
+ (unicode_cap('sc') or u'\x1b[s',
+ _vpa,
+ unicode_cap('rc') or u'\x1b[u'))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess")
+def test_inject_move_x():
+ """Test injection of hpa attribute for screen/ansi (issue #55)."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ COL = 5
+ with t.location(x=COL):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc') or u'\x1b[s',
+ u'\x1b[{0}G'.format(COL + 1),
+ unicode_cap('rc') or u'\x1b[u'))
+ assert (t.stream.getvalue() == expected_output)
+ assert (t.move_x(COL) == u'\x1b[{0}G'.format(COL + 1))
+
+ child('screen')
+ child('screen-256color')
+ child('ansi')
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess")
+def test_inject_move_y():
+ """Test injection of vpa attribute for screen/ansi (issue #55)."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ ROW = 5
+ with t.location(y=ROW):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc') or u'\x1b[s',
+ u'\x1b[{0}d'.format(ROW + 1),
+ unicode_cap('rc') or u'\x1b[u'))
+ assert (t.stream.getvalue() == expected_output)
+ assert (t.move_y(ROW) == u'\x1b[{0}d'.format(ROW + 1))
+
+ child('screen')
+ child('screen-256color')
+ child('ansi')
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess")
+def test_inject_civis_and_cnorm_for_ansi():
+ """Test injection of cvis attribute for ansi."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ with t.hidden_cursor():
+ pass
+ expected_output = u'\x1b[?25l\x1b[?25h'
+ assert (t.stream.getvalue() == expected_output)
+
+ child('ansi')
+
+
+@pytest.mark.skipif(platform.system() == 'Windows', reason="requires multiprocess")
+def test_inject_sc_and_rc_for_ansi():
+ """Test injection of sc and rc (save and restore cursor) for ansi."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ with t.location():
+ pass
+ expected_output = u'\x1b[s\x1b[u'
+ assert (t.stream.getvalue() == expected_output)
+
+ child('ansi')
+
+
+def test_zero_location(all_terms):
+ """Make sure ``location()`` pays attention to 0-valued args."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
+ with t.location(0, 0):
+ pass
+ expected_output = u''.join(
+ (unicode_cap('sc') or u'\x1b[s',
+ unicode_parm('cup', 0, 0),
+ unicode_cap('rc') or u'\x1b[u'))
+ assert (t.stream.getvalue() == expected_output)
+
+ child(all_terms)
+
+
+def test_mnemonic_colors(all_terms):
+ """Make sure color shortcuts work."""
+ @as_subprocess
+ def child(kind):
+ def color(t, num):
+ return t.number_of_colors and unicode_parm('setaf', num) or ''
+
+ def on_color(t, num):
+ return t.number_of_colors and unicode_parm('setab', num) or ''
+
+ # Avoid testing red, blue, yellow, and cyan, since they might someday
+ # change depending on terminal type.
+ t = TestTerminal(kind=kind)
+ assert (t.white == color(t, 7))
+ assert (t.green == color(t, 2)) # Make sure it's different than white.
+ assert (t.on_black == on_color(t, 0))
+ assert (t.on_green == on_color(t, 2))
+ assert (t.bright_black == color(t, 8))
+ assert (t.bright_green == color(t, 10))
+ assert (t.on_bright_black == on_color(t, 8))
+ assert (t.on_bright_green == on_color(t, 10))
+
+ child(all_terms)
+
+
+def test_callable_numeric_colors(all_terms):
+ """``color(n)`` should return a formatting wrapper."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ if t.magenta:
+ assert t.color(5)('smoo') == t.magenta + 'smoo' + t.normal
+ else:
+ assert t.color(5)('smoo') == 'smoo'
+
+ if t.on_magenta:
+ assert t.on_color(5)('smoo') == t.on_magenta + 'smoo' + t.normal
+ else:
+ assert t.color(5)(u'smoo') == 'smoo'
+
+ if t.color(4):
+ assert t.color(4)(u'smoo') == t.color(4) + u'smoo' + t.normal
+ else:
+ assert t.color(4)(u'smoo') == 'smoo'
+
+ if t.on_green:
+ assert t.on_color(2)('smoo') == t.on_green + u'smoo' + t.normal
+ else:
+ assert t.on_color(2)('smoo') == 'smoo'
+
+ if t.on_color(6):
+ assert t.on_color(6)('smoo') == t.on_color(6) + u'smoo' + t.normal
+ else:
+ assert t.on_color(6)('smoo') == 'smoo'
+
+ child(all_terms)
+
+
+def test_null_callable_numeric_colors(all_terms):
+ """``color(n)`` should be a no-op on null terminals."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=six.StringIO(), kind=kind)
+ assert (t.color(5)('smoo') == 'smoo')
+ assert (t.on_color(6)('smoo') == 'smoo')
+
+ child(all_terms)
+
+
+def test_naked_color_cap(all_terms):
+ """``term.color`` should return a stringlike capability."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ assert (t.color + '' == t.setaf + '')
+
+ child(all_terms)
+
+
+def test_formatting_functions(all_terms):
+ """Test simple and compound formatting wrappers."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ # test simple sugar,
+ if t.bold:
+ expected_output = u''.join((t.bold, u'hi', t.normal))
+ else:
+ expected_output = u'hi'
+ assert t.bold(u'hi') == expected_output
+ # Plain strs for Python 2.x
+ if t.green:
+ expected_output = u''.join((t.green, 'hi', t.normal))
+ else:
+ expected_output = u'hi'
+ assert t.green('hi') == expected_output
+ # Test unicode
+ if t.underline:
+ expected_output = u''.join((t.underline, u'boö', t.normal))
+ else:
+ expected_output = u'boö'
+ assert (t.underline(u'boö') == expected_output)
+
+ child(all_terms)
+
+
+def test_compound_formatting(all_terms):
+ """Test simple and compound formatting wrappers."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ if any((t.bold, t.green)):
+ expected_output = u''.join((t.bold, t.green, u'boö', t.normal))
+ else:
+ expected_output = u'boö'
+ assert t.bold_green(u'boö') == expected_output
+
+ if any((t.on_bright_red, t.bold, t.bright_green, t.underline)):
+ expected_output = u''.join(
+ (t.on_bright_red, t.bold, t.bright_green, t.underline, u'meh',
+ t.normal))
+ else:
+ expected_output = u'meh'
+ very_long_cap = t.on_bright_red_bold_bright_green_underline
+ assert (very_long_cap('meh') == expected_output)
+
+ child(all_terms)
+
+
+def test_nested_formatting(all_terms):
+ """Test complex nested compound formatting, wow!"""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+
+ # Test deeply nested styles
+ given = t.green('-a-', t.bold('-b-', t.underline('-c-'),
+ '-d-'),
+ '-e-')
+ expected = u''.join((
+ t.green, '-a-', t.bold, '-b-', t.underline, '-c-', t.normal,
+ t.green, t.bold, '-d-',
+ t.normal, t.green, '-e-', t.normal))
+ assert given == expected
+
+ # Test off-and-on nested styles
+ given = t.green('off ', t.underline('ON'),
+ ' off ', t.underline('ON'),
+ ' off')
+ expected = u''.join((
+ t.green, 'off ', t.underline, 'ON',
+ t.normal, t.green, ' off ', t.underline, 'ON',
+ t.normal, t.green, ' off', t.normal))
+ assert given == expected
+
+
+def test_formatting_functions_without_tty(all_terms):
+ """Test crazy-ass formatting wrappers when there's no tty."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=False)
+ assert (t.bold(u'hi') == u'hi')
+ assert (t.green('hi') == u'hi')
+ # Test non-ASCII chars, no longer really necessary:
+ assert (t.bold_green(u'boö') == u'boö')
+ assert (t.bold_underline_green_on_red('loo') == u'loo')
+
+ # Test deeply nested styles
+ given = t.green('-a-', t.bold('-b-', t.underline('-c-'),
+ '-d-'),
+ '-e-')
+ expected = u'-a--b--c--d--e-'
+ assert given == expected
+
+ # Test off-and-on nested styles
+ given = t.green('off ', t.underline('ON'),
+ ' off ', t.underline('ON'),
+ ' off')
+ expected = u'off ON off ON off'
+ assert given == expected
+ assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh')
+
+ child(all_terms)
+
+
+def test_nice_formatting_errors(all_terms):
+ """Make sure you get nice hints if you misspell a formatting wrapper."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(kind=kind)
+ try:
+ t.bold_misspelled('hey')
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' in e.args[0]
+ try:
+ t.bold_misspelled(u'hey') # unicode
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' in e.args[0]
+
+ try:
+ t.bold_misspelled(None) # an arbitrary non-string
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' not in e.args[0]
+
+ if platform.python_implementation() != 'PyPy':
+ # PyPy fails to toss an exception, Why?!
+ try:
+ t.bold_misspelled('a', 'b') # >1 string arg
+ assert not t.is_a_tty or False, 'Should have thrown exception'
+ except TypeError:
+ e = sys.exc_info()[1]
+ assert 'probably misspelled' in e.args[0], e.args
+
+ child(all_terms)
+
+
+def test_null_callable_string(all_terms):
+ """Make sure NullCallableString tolerates all kinds of args."""
+ @as_subprocess
+ def child(kind):
+ t = TestTerminal(stream=six.StringIO(), kind=kind)
+ assert (t.clear == '')
+ assert (t.move(1 == 2) == '')
+ assert (t.move_x(1) == '')
+ assert (t.bold() == '')
+ assert (t.bold('', 'x', 'huh?') == 'xhuh?')
+ assert (t.clear('x') == 'x')
+
+ child(all_terms)
+
+
+def test_padd():
+ """Test Terminal.padd(seq)."""
+ @as_subprocess
+ def child(kind):
+ from blessed.sequences import Sequence
+ from blessed import Terminal
+ term = Terminal(kind)
+ assert Sequence('xyz\b', term).padd() == u'xy'
+ assert Sequence('xyz\b-', term).padd() == u'xy-'
+ assert Sequence('xxxx\x1b[3Dzz', term).padd() == u'xzz'
+ assert Sequence('\x1b[3D', term).padd() == u'' # "Trim left"
+
+ kind = 'xterm-256color'
+ if platform.system() == 'Windows':
+ kind = 'vtwin10'
+ child(kind)
+
+
+def test_split_seqs(all_terms):
+ """Test Terminal.split_seqs."""
+ @as_subprocess
+ def child(kind):
+ from blessed import Terminal
+ term = Terminal(kind)
+
+ if term.sc and term.rc:
+ given_text = term.sc + 'AB' + term.rc + 'CD'
+ expected = [term.sc, 'A', 'B', term.rc, 'C', 'D']
+ result = list(term.split_seqs(given_text))
+ assert result == expected
+
+ if term.bold:
+ given_text = term.bold + 'bbq'
+ expected = [term.bold, 'b', 'b', 'q']
+ result = list(term.split_seqs(given_text))
+ assert result == expected
+
+ child(all_terms)
diff --git a/tests/test_wrap.py b/tests/test_wrap.py
new file mode 100644
index 00000000..29537596
--- /dev/null
+++ b/tests/test_wrap.py
@@ -0,0 +1,132 @@
+# std imports
+import os
+import textwrap
+
+# 3rd party
+import pytest
+
+# local
+from .accessories import TestTerminal, many_columns, as_subprocess
+
+TEXTWRAP_KEYWORD_COMBINATIONS = [
+ dict(break_long_words=False,
+ drop_whitespace=False,
+ subsequent_indent=''),
+ dict(break_long_words=False,
+ drop_whitespace=True,
+ subsequent_indent=''),
+ dict(break_long_words=False,
+ drop_whitespace=False,
+ subsequent_indent=' '),
+ dict(break_long_words=False,
+ drop_whitespace=True,
+ subsequent_indent=' '),
+ dict(break_long_words=True,
+ drop_whitespace=False,
+ subsequent_indent=''),
+ dict(break_long_words=True,
+ drop_whitespace=True,
+ subsequent_indent=''),
+ dict(break_long_words=True,
+ drop_whitespace=False,
+ subsequent_indent=' '),
+ dict(break_long_words=True,
+ drop_whitespace=True,
+ subsequent_indent=' '),
+]
+if os.environ.get('TEST_QUICK', None) is not None:
+ # test only one feature: everything on
+ TEXTWRAP_KEYWORD_COMBINATIONS = [
+ dict(break_long_words=True,
+ drop_whitespace=True,
+ subsequent_indent=' ')
+ ]
+
+
+def test_SequenceWrapper_invalid_width():
+ """Test exception thrown from invalid width."""
+ WIDTH = -3
+
+ @as_subprocess
+ def child():
+ term = TestTerminal()
+ try:
+ my_wrapped = term.wrap(u'------- -------------', WIDTH)
+ except ValueError as err:
+ assert err.args[0] == (
+ "invalid width %r(%s) (must be integer > 0)" % (
+ WIDTH, type(WIDTH)))
+ else:
+ assert False, 'Previous stmt should have raised exception.'
+ del my_wrapped # assigned but never used
+
+ child()
+
+
+@pytest.mark.parametrize("kwargs", TEXTWRAP_KEYWORD_COMBINATIONS)
+def test_SequenceWrapper(many_columns, kwargs):
+ """Test that text wrapping matches internal extra options."""
+ @as_subprocess
+ def child(width, pgraph, kwargs):
+ # build a test paragraph, along with a very colorful version
+ term = TestTerminal()
+ attributes = ('bright_red', 'on_bright_blue', 'underline', 'reverse',
+ 'red_reverse', 'red_on_white', 'on_bright_white')
+ term.bright_red('x')
+ term.on_bright_blue('x')
+ term.underline('x')
+ term.reverse('x')
+ term.red_reverse('x')
+ term.red_on_white('x')
+ term.on_bright_white('x')
+
+ pgraph_colored = u''.join([
+ getattr(term, (attributes[idx % len(attributes)]))(char)
+ if char != u' ' else u' '
+ for idx, char in enumerate(pgraph)])
+
+ internal_wrapped = textwrap.wrap(pgraph, width=width, **kwargs)
+ my_wrapped = term.wrap(pgraph, width=width, **kwargs)
+ my_wrapped_colored = term.wrap(pgraph_colored, width=width, **kwargs)
+
+ # ensure we textwrap ascii the same as python
+ assert internal_wrapped == my_wrapped
+
+ # ensure content matches for each line, when the sequences are
+ # stripped back off of each line
+ for line_no, (left, right) in enumerate(
+ zip(internal_wrapped, my_wrapped_colored)):
+ assert left == term.strip_seqs(right)
+
+ # ensure our colored textwrap is the same paragraph length
+ assert (len(internal_wrapped) == len(my_wrapped_colored))
+
+ child(width=many_columns, kwargs=kwargs,
+ pgraph=u' Z! a bc defghij klmnopqrstuvw<<>>xyz012345678900 ' * 2)
+ child(width=many_columns, kwargs=kwargs, pgraph=u'a bb ccc')
+
+
+def test_multiline():
+ """Test that text wrapping matches internal extra options."""
+
+ @as_subprocess
+ def child():
+ # build a test paragraph, along with a very colorful version
+ term = TestTerminal()
+ given_string = ('\n' + (32 * 'A') + '\n' +
+ (32 * 'B') + '\n' +
+ (32 * 'C') + '\n\n')
+ expected = [
+ '',
+ 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
+ 'AA',
+ 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
+ 'BB',
+ 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
+ 'CC',
+ '',
+ ]
+ result = term.wrap(given_string, width=30)
+ assert expected == result
+
+ child()
diff --git a/tests/wall.ans b/tests/wall.ans
new file mode 100644
index 00000000..081b4d28
--- /dev/null
+++ b/tests/wall.ans
@@ -0,0 +1,7 @@
+[64C[34m▄▓▄
+ [1;30m░░ [32;42m█▀▀[0;32m█[1;42m▀[0;32m█[10C[1m█[42m▀[0;32m████████[1;42m▀[0;32m█[1;42m▀▀█ █▀▀[0;32m█[1;42m▀[0;32m█[10C[1;42m█▀▀[0;32m█[1;42m▀[0;32m█[7C[1;30mxz[0m([1;30mimp[0m)
+ [34m▄▄▄ [1;32;42m▓[0;32m█████ [34m▄▄ [1;32;42m█▀▀[0;32m█[1;37;42m░ [0;32m▀▀▀▀▀ █████[1;42m▓ ▓[0;32m█████ [34m░[30;44m▓ [1;32;42m█▀▀[0;32m█[1;37;42m░ [32m▓[0;32m█████ [34m░[30;44m▓ [1;32;42m█▀▀[0;32m█[1;37;42m░ [34;44m░[0;34m▄ [1;30m░░
+ [0;34m▐▄[1;44m░▄[0;34m▀ [32m▀▀▀▀▀▀ [34m█[1;44m▓ [0;32m▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀ [34m██ [32m▀▀▀▀▀ ▀▀▀▀▀▀ [34m██ [32m▀▀▀▀▀ [34m█[1;44m▄[0;34m▌▌▄▄
+ █[1;44m▓[0;34m██ [30;47m░[0;37m▓████ [34m▐ [37m████[1;47m░ ░[0m████ [1;34;44m▄[0;34m█ [37m████▓[30;47m░ ░[0;37m▓████ [1;34;44m█[0;34m█ [37m████[1;47m░ [0;30;47m░[0;37m▓████ [1;34;44m█[0;34m█ [37m████[1;47m░ [0;34m█[1;44m▐▄▄[0;34m█[1;44m░[0;10m
+ [0;34m██[1;44m▓[0;34m▀ [30;47m░[0;37m▓████ █ ████[1;47m░ ░[0m████ [34m▐ [37m████▓[30;47m░ ░[0;37m▓████ [1;34;44m▄[0;34m█▌[37m████[1;47m░ [0;30;47m░[0;37m▓████ [1;34;44m▄[0;34m█▌[37m████[1;47m░ [0;34m▌█████
+ ▀ [30;47m░[0;37m▓████▄█▄▄███ [1;47m░ ░ [0m███▄▄▄▄████▓[30;47m░ ░[0;37m▓████▄▄▄▄███ [1;47m░ [0;30;47m░[0;37m▓████▄▄▄▄███ [1;47m░ [0;34m▀▀▀[30;44m▓[0;10m
diff --git a/tox.ini b/tox.ini
index 06bd4fd6..fb2b6282 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,8 +1,189 @@
[tox]
-envlist = py25, py26, py27, py32, py33
+envlist = about
+ autopep8
+ docformatter
+ isort
+ pylint
+ flake8
+ flake8_tests
+ pydocstyle
+ sphinx
+ py{26,27,34,35,36,37,38}
+skip_missing_interpreters = true
[testenv]
-commands = nosetests blessings
-deps = nose
-# So Python 3 runs don't pick up incompatible, un-2to3'd source from the cwd:
-changedir = .tox
\ No newline at end of file
+basepython = python3.8
+deps = pytest==5.3.2
+ pytest-cov==2.8.1
+ pytest-xdist==1.31.0
+ mock==3.0.5
+commands = {envbindir}/py.test --cov-config={toxinidir}/tox.ini {posargs:\
+ --strict --verbose \
+ --junit-xml=.tox/results.{envname}.xml \
+ --durations=3 \
+ } \
+ tests
+
+[pytest]
+looponfailroots = blessed
+norecursedirs = .git .tox build
+addopts = --disable-pytest-warnings
+ --cov-append --cov-report=html --color=yes --ignore=setup.py --ignore=.tox
+ --log-format='%(levelname)s %(relativeCreated)2.2f %(filename)s:%(lineno)d %(message)s'
+ --cov=blessed
+filterwarnings =
+ error
+junit_family = xunit1
+
+[coverage:run]
+branch = True
+source = blessed
+parallel = True
+
+[coverage:report]
+omit = tests/*
+exclude_lines = pragma: no cover
+precision = 1
+
+[coverage:paths]
+source = blessed/
+
+[isort]
+line_length = 100
+indent = ' '
+multi_line_output = 1
+length_sort = 1
+import_heading_stdlib = std imports
+import_heading_thirdparty = 3rd party
+import_heading_firstparty = local
+import_heading_localfolder = local
+sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
+no_lines_before=LOCALFOLDER
+known_third_party = jinxed
+atomic = true
+
+[flake8]
+max-line-length = 100
+exclude = .tox,build
+deps = flake8==3.7.9
+
+[doc8]
+max-line-length = 100
+
+[testenv:py26]
+setenv = TEST_QUICK=1
+basepython = python2.6
+
+[testenv:py27]
+basepython = python2.7
+deps = pytest==4.6.9
+ pytest-cov==2.8.1
+ pytest-xdist==1.31.0
+ mock==3.0.5
+
+[testenv:py34]
+setenv = TEST_QUICK=1
+basepython = python3.4
+deps = pytest==4.6.9
+ pytest-cov==2.8.1
+ pytest-xdist==1.31.0
+ mock==3.0.5
+
+[testenv:py35]
+setenv = TEST_QUICK=1
+basepython = python3.5
+
+[testenv:py36]
+setenv = TEST_QUICK=1
+basepython = python3.6
+
+[testenv:py37]
+setenv = TEST_QUICK=1
+basepython = python3.7
+
+[testenv:py38]
+basepython = python3.8
+deps = {[testenv]deps}
+ pytest-rerunfailures==8.0
+setenv = TEST_KEYBOARD = {env:TEST_KEYBOARD:yes}
+commands = {envbindir}/py.test --cov-config={toxinidir}/tox.ini \
+ {posargs:\
+ --reruns 5 \
+ --strict --verbose \
+ --junit-xml=.tox/results.{envname}.xml \
+ --durations=3 \
+ } \
+ tests
+
+[testenv:develop]
+commands = {posargs}
+
+[testenv:about]
+commands = python {toxinidir}/bin/display-sighandlers.py
+ python {toxinidir}/bin/display-terminalinfo.py
+ python {toxinidir}/bin/display-fpathconf.py
+
+[testenv:autopep8]
+deps = autopep8==1.4.4
+commands =
+ {envbindir}/autopep8 \
+ --in-place \
+ --recursive \
+ --aggressive \
+ --aggressive \
+ blessed/ bin/ setup.py
+
+[testenv:docformatter]
+deps =
+ docformatter==1.3.1
+ untokenize==0.1.1
+commands =
+ {envbindir}/docformatter \
+ --in-place \
+ --recursive \
+ --pre-summary-newline \
+ --wrap-summaries=100 \
+ --wrap-descriptions=100 \
+ {toxinidir}/blessed/ \
+ {toxinidir}/bin \
+ {toxinidir}/setup.py \
+ {toxinidir}/docs/conf.py
+
+[testenv:isort]
+deps = {[testenv]deps}
+ isort==4.3.21
+commands = {envbindir}/isort --quiet --apply --recursive
+
+[testenv:pylint]
+deps = pylint==2.4.4
+commands = {envbindir}/pylint --rcfile={toxinidir}/.pylintrc \
+ --ignore=tests,docs,setup.py,conf.py,build,distutils,.pyenv,.git,.tox \
+ {posargs:{toxinidir}}/blessed
+
+[testenv:flake8]
+deps = {[flake8]deps}
+commands = {envbindir}/flake8 --exclude=tests,docs/sphinxext/github.py setup.py docs/ blessed/ bin/
+
+[testenv:flake8_tests]
+deps = {[flake8]deps}
+commands = {envbindir}/flake8 --ignore=W504,F811,F401 tests/
+
+[testenv:pydocstyle]
+deps = pydocstyle==3.0.0
+ restructuredtext_lint
+ doc8
+commands = {envbindir}/pydocstyle --source --explain \
+ --ignore=D101,D212,D203,D204,D401 \
+ {toxinidir}/blessed
+ {envbindir}/rst-lint README.rst
+ {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs
+
+[testenv:sphinx]
+deps = -r docs/requirements.txt
+commands = {envbindir}/sphinx-build \
+ {posargs:-v -W -d {toxinidir}/docs/_build/doctrees -b html docs {toxinidir}/docs/_build/html}
+
+[testenv:codecov]
+passenv = TOXENV CI TRAVIS TRAVIS_* CODECOV_*
+deps = codecov>=1.4.0
+commands = codecov -e TOXENV
diff --git a/version.json b/version.json
new file mode 100644
index 00000000..8c3caff7
--- /dev/null
+++ b/version.json
@@ -0,0 +1 @@
+{"version": "1.17.0"}