summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Quast <contact@jeffquast.com>2015-04-14 19:52:19 -0700
committerJeff Quast <contact@jeffquast.com>2015-04-14 19:52:19 -0700
commit9aebb6b3521fadf1f618c879945da726327e2252 (patch)
tree21120bab65e3aebe6d704f93ceb0062c613f069e
parent22d74eb1bc0004c5f079eb43850c45bec3b58c7c (diff)
parentf9543adf65eca78bf8fd402a8ac2532c084894e8 (diff)
downloadblessings-9aebb6b3521fadf1f618c879945da726327e2252.tar.gz
Merge pull request #100 from erikrose/blessed-integration-pep257-docstrings
Blessed integration pep257 docstrings
-rw-r--r--.prospector.yaml37
-rw-r--r--.travis.yml2
-rw-r--r--blessings/__init__.py10
-rw-r--r--blessings/_binterms.py21
-rw-r--r--blessings/formatters.py261
-rw-r--r--blessings/keyboard.py259
-rw-r--r--blessings/sequences.py448
-rw-r--r--blessings/terminal.py796
-rw-r--r--blessings/tests/accessories.py13
-rw-r--r--blessings/tests/test_core.py46
-rw-r--r--blessings/tests/test_keyboard.py12
-rw-r--r--blessings/tests/test_length_sequence.py12
-rw-r--r--blessings/tests/test_sequences.py49
-rw-r--r--docs/api.rst32
-rw-r--r--docs/conf.py48
-rw-r--r--docs/further.rst21
-rw-r--r--docs/history.rst28
-rw-r--r--docs/index.rst2
-rw-r--r--docs/intro.rst24
-rw-r--r--docs/overview.rst379
-rw-r--r--fabfile.py39
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py131
-rwxr-xr-xtools/teamcity-runtests.sh2
-rw-r--r--tox.ini35
25 files changed, 1654 insertions, 1055 deletions
diff --git a/.prospector.yaml b/.prospector.yaml
index 5d1f7d2..c32f978 100644
--- a/.prospector.yaml
+++ b/.prospector.yaml
@@ -1,12 +1,16 @@
inherits:
- strictness_veryhigh
-ignore:
+ignore-patterns:
- (^|/)\..+
- ^docs/
- # ignore tests and bin/ for the moment, their quality does not so much matter.
+ - ^build/
+ # ignore these, their quality does not so much matter.
- ^bin/
- ^blessings/tests/
+ - ^tools/
+ # not maintained
+ - ^fabfile.py
test-warnings: true
@@ -24,6 +28,9 @@ frosted:
mccabe:
# complexity checking.
run: true
+ disable:
+ # Terminal.__init__ is too complex (14)
+ - MC0001
pep257:
# docstring checking
@@ -46,22 +53,26 @@ pylint:
# no 'mark' member
ignored-classes: pytest
disable:
- # Too many lines in module
- ##- C0302
- # Used * or ** magic
- ##- W0142
- # Used builtin function 'filter'.
- # (For maintainability, one should prefer list comprehension.)
- ##- W0141
- # Use % formatting in logging functions but pass the % parameters
- ##- W1202
+ # Access to a protected member _sugar of a client class
+ - protected-access
+ # blessings.Terminal: Too many instance attributes (12/7)
+ - too-many-instance-attributes
+ # blessings.Terminal: Too many public methods (25/20)
+ - too-many-public-methods
+ # blessings.Terminal: Too many branches (13/12)
+ - too-many-branches
+ # blessings.sequences.get_wontmove_sequence_patterns:
+ # Used builtin function 'map'
+ - bad-builtin
+
pyroma:
# checks setup.py
run: true
vulture:
- # this tool does a good job of finding unused code.
- run: true
+ # this tool does a good job of finding unused code, which isn't terribly
+ # useful from an API perspective, disable.
+ run: false
# vim: noai:ts=4:sw=4
diff --git a/.travis.yml b/.travis.yml
index e3c76c2..99e0fcc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,6 +26,8 @@ install:
fi
script:
+ - if [[ "${TOXENV}" == "py27" ]]; then tox -e sa; fi
+ - if [[ "${TOXENV}" == "py27" ]]; then tox -e docs; fi
- tox -e $TOXENV
after_success:
diff --git a/blessings/__init__.py b/blessings/__init__.py
index f5a65eb..813e2df 100644
--- a/blessings/__init__.py
+++ b/blessings/__init__.py
@@ -1,16 +1,18 @@
"""
-A thin, practical wrapper around terminal capabilities in Python
+A thin, practical wrapper around terminal capabilities in Python.
http://pypi.python.org/pypi/blessings
"""
+# std imports
import platform as _platform
+
+# local
+from blessings.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('Blessings needs Python 3.2.3 or greater for Python 3 '
'support due to http://bugs.python.org/issue10570.')
-
-from .terminal import Terminal
-
__all__ = ['Terminal']
diff --git a/blessings/_binterms.py b/blessings/_binterms.py
index 9621be5..1f6cc80 100644
--- a/blessings/_binterms.py
+++ b/blessings/_binterms.py
@@ -1,12 +1,13 @@
-""" Exports a list of binary terminals blessings is not able to cope with. """
-#: This list of terminals is manually managed, it describes all of the terminals
-#: that blessings cannot measure the sequence length for; they contain
-#: binary-packed capabilities instead of numerics, so it is not possible to
-#: build regular expressions in the way that sequences.py does.
+"""List of terminal definitions containing binary-packed sequences."""
+
+#: This list of terminals is manually managed, it describes all of the
+#: terminals that blessings cannot measure the sequence length for; they
+#: contain binary-packed capabilities instead of numerics, so it is not
+#: possible to build regular expressions in the way that sequences.py does.
#:
#: This may be generated by exporting TEST_BINTERMS, then analyzing the
#: jUnit result xml written to the project folder.
-binary_terminals = u"""
+BINARY_TERMINALS = u"""
9term
aaa+dec
aaa+rv
@@ -868,4 +869,10 @@ zen50
ztx
""".split()
-__all__ = ['binary_terminals']
+#: Message displayed when terminal containing binary-packed sequences
+#: is instantiated -- the 'warnings' module is used and may be filtered away.
+BINTERM_UNSUPPORTED_MSG = (
+ u"Terminal kind {0!r} contains binary-packed capabilities, blessings "
+ u"is likely to fail to measure the length of its sequences.")
+
+__all__ = ['BINARY_TERMINALS', 'BINTERM_UNSUPPORTED_MSG']
diff --git a/blessings/formatters.py b/blessings/formatters.py
index 283fb73..8a0f4e4 100644
--- a/blessings/formatters.py
+++ b/blessings/formatters.py
@@ -1,43 +1,64 @@
-"This sub-module provides formatting functions."
+"""This sub-module provides sequence-formatting functions."""
# standard imports
import curses
-import sys
# 3rd-party
import six
-_derivatives = ('on', 'bright', 'on_bright',)
-_colors = set('black red green yellow blue magenta cyan white'.split())
-_compoundables = set('bold underline reverse blink dim italic shadow '
- 'standout subscript superscript'.split())
+def _make_colors():
+ """
+ Return set of valid colors and their derivatives.
+
+ :rtype: set
+ """
+ derivatives = ('on', 'bright', 'on_bright',)
+ colors = set('black red green yellow blue magenta cyan white'.split())
+ return set(['_'.join((_derivitive, _color))
+ for _derivitive in derivatives
+ for _color in colors]) | colors
-#: Valid colors and their background (on), bright, and bright-bg derivatives.
-COLORS = set(['_'.join((derivitive, color))
- for derivitive in _derivatives
- for color in _colors]) | _colors
-#: All valid compoundable names.
-COMPOUNDABLES = (COLORS | _compoundables)
+def _make_compoundables(colors):
+ """
+ Return given set ``colors`` along with all "compoundable" attributes.
+
+ :param set colors: set of color names as string.
+ :rtype: set
+ """
+ _compoundables = set('bold underline reverse blink dim italic shadow '
+ 'standout subscript superscript'.split())
+ return colors | _compoundables
+
+
+#: Valid colors and their background (on), bright,
+#: and bright-background derivatives.
+COLORS = _make_colors()
+
+#: Attributes and colors which may be compounded by underscore.
+COMPOUNDABLES = _make_compoundables(COLORS)
class ParameterizingString(six.text_type):
- """A Unicode string which can be called as a parameterizing termcap.
+
+ 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')
+ >>> 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):
- """P.__new__(cls, cap, [normal, [name]])
+ """
+ Class constructor accepting 3 positional arguments.
:arg cap: parameterized string suitable for curses.tparm()
- :arg normal: terminating sequence for this capability.
- :arg name: name of this terminal capability.
+ :arg normal: terminating sequence for this capability (optional).
+ :arg name: name of this terminal capability (optional).
"""
assert len(args) and len(args) < 4, args
new = six.text_type.__new__(cls, args[0])
@@ -46,11 +67,14 @@ class ParameterizingString(six.text_type):
return new
def __call__(self, *args):
- """P(*args) -> FormattingString()
+ """
+ Returning :class:`FormattingString` instance for given parameters.
Return evaluated terminal capability (self), receiving arguments
``*args``, followed by the terminating sequence (self.normal) into
- a FormattingString capable of being called.
+ 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
@@ -80,32 +104,41 @@ class ParameterizingString(six.text_type):
class ParameterizingProxyString(six.text_type):
- """A Unicode string which can be called to proxy missing termcap entries.
-
- For example::
- >>> from blessings 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'
+ 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 blessings 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):
- """P.__new__(cls, (fmt, callable), [normal, [name]])
+ """
+ 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.
- :arg name: name of this terminal capability.
+ :arg normal: terminating sequence for this capability (optional).
+ :arg name: name of this terminal capability (optional).
"""
assert len(args) and len(args) < 4, args
- assert type(args[0]) is tuple, args[0]
+ 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]
@@ -114,25 +147,31 @@ class ParameterizingProxyString(six.text_type):
return new
def __call__(self, *args):
- """P(*args) -> FormattingString()
+ """
+ Returning :class:`FormattingString` instance for given parameters.
- Return evaluated terminal capability format, (self), using callable
- ``self._fmt_args`` receiving arguments ``*args``, followed by the
- terminating sequence (self.normal) into a FormattingString capable
- of being called.
+ 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 StringClass for proxied attributes.
-
- We know that some kinds of terminal kinds support sequences that the
- terminfo database always report -- such as the 'move_x' attribute for
- terminal type 'screen' and 'ansi', or 'hide_cursor' for 'ansi'.
-
- Returns instance of ParameterizingProxyString or NullCallableString.
+ """
+ Proxy and return callable string for proxied attributes.
+
+ :param Terminal term: :class:`~.Terminal` instance.
+ :param 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',)
@@ -160,18 +199,29 @@ def get_proxy_string(term, attr):
class FormattingString(six.text_type):
- """A Unicode string which can be called using ``text``,
- returning a new string, ``attr`` + ``text`` + ``normal``::
- >> style = FormattingString(term.bright_blue, term.normal)
- >> style('Big Blue')
- u'\x1b[94mBig Blue\x1b(B\x1b[m'
+ 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):
- """P.__new__(cls, sequence, [normal])
+ """
+ Class constructor accepting 2 positional arguments.
+
:arg sequence: terminal attribute sequence.
- :arg normal: terminating sequence for this attribute.
+ :arg normal: terminating sequence for this attribute (optional).
"""
assert 1 <= len(args) <= 2, args
new = six.text_type.__new__(cls, args[0])
@@ -179,35 +229,37 @@ class FormattingString(six.text_type):
return new
def __call__(self, text):
- """P(text) -> unicode
-
- Return string ``text``, joined by specified video attribute,
- (self), and followed by reset attribute sequence (term.normal).
- """
+ """Return ``text`` joined by ``sequence`` and ``normal``."""
if len(self):
return u''.join((self, text, self._normal))
return text
class NullCallableString(six.text_type):
- """A dummy callable Unicode to stand in for ``FormattingString`` and
- ``ParameterizingString`` for terminals that cannot perform styling.
+
"""
+ 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):
- """Return a Unicode or whatever you passed in as the first arg
- (hopefully a string of some kind).
+ """
+ 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 ``ParameterizingString``, as there are
- only about half a dozen string-returning capabilities listed in
+ 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 ``FormattingString`` without
+ the first arg, acting in place of :class:`FormattingString` without
any attributes.
"""
if len(args) != 1 or isinstance(args[0], int):
@@ -235,27 +287,37 @@ class NullCallableString(six.text_type):
def split_compound(compound):
- """Split a possibly compound format string into segments.
+ """
+ Split compound formating string into segments.
>>> split_compound('bold_underline_bright_blue_on_red')
['bold', 'underline', 'bright_blue', 'on_red']
+ :param 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 s in compound.split('_'):
+ for segment in compound.split('_'):
if merged_segs and merged_segs[-1] in mergeable_prefixes:
- merged_segs[-1] += '_' + s
+ merged_segs[-1] += '_' + segment
else:
- merged_segs.append(s)
+ merged_segs.append(segment)
return merged_segs
def resolve_capability(term, attr):
- """Return a Unicode string for the terminal capability ``attr``,
- or an empty string if not found, or if terminal is without styling
- capabilities.
+ """
+ Resolve a raw terminal capability using :func:`tigetstr`.
+
+ :param Terminal term: :class:`~.Terminal` instance.
+ :param 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'.
@@ -266,18 +328,29 @@ def resolve_capability(term, attr):
def resolve_color(term, color):
- """resolve_color(T, color) -> FormattingString()
-
- Resolve a ``color`` name to callable capability, ``FormattingString``
- unless ``term.number_of_colors`` is 0, then ``NullCallableString``.
-
- Valid ``color`` capabilities names are any of the simple color
- names, such as ``red``, or compounded, such as ``on_bright_green``.
"""
+ Resolve a simple color name to a callable capability.
+
+ This function supports :func:`resolve_attribute`.
+
+ :param Terminal term: :class:`~.Terminal` instance.
+ :param 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()
+
# NOTE(erikrose): 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.
+ # assumes it does: to terminfo(5) describes color(1) as COLOR_RED when
+ # using setaf, but COLOR_BLUE when using setf.
color_cap = (term._background_color if 'on_' in color else
term._foreground_color)
@@ -285,8 +358,6 @@ def resolve_color(term, color):
# bright colors at 8-15:
offset = 8 if 'bright_' in color else 0
base_color = color.rsplit('_', 1)[-1]
- if term.number_of_colors == 0:
- return NullCallableString()
attr = 'COLOR_%s' % (base_color.upper(),)
fmt_attr = color_cap(getattr(curses, attr) + offset)
@@ -294,11 +365,21 @@ def resolve_color(term, color):
def resolve_attribute(term, attr):
- """Resolve a sugary or plain capability name, color, or compound
- formatting name into a *callable* unicode string capability,
- ``ParameterizingString`` or ``FormattingString``.
"""
- # A simple color, such as `red' or `blue'.
+ Resolve a terminal attribute name into a capability class.
+
+ :param Terminal term: :class:`~.Terminal` instance.
+ :param 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)
diff --git a/blessings/keyboard.py b/blessings/keyboard.py
index bf6a4a5..8037163 100644
--- a/blessings/keyboard.py
+++ b/blessings/keyboard.py
@@ -1,67 +1,44 @@
-"This sub-module provides 'keyboard awareness'."
+"""This sub-module provides 'keyboard awareness'."""
-__all__ = ['Keystroke', 'get_keyboard_codes', 'get_keyboard_sequences']
-
-# standard imports
+# std imports
import curses.has_key
-import collections
import curses
-# 3rd-party
+# 3rd party
import six
-if hasattr(collections, 'OrderedDict'):
- OrderedDict = collections.OrderedDict
-else:
- # python 2.6 requires 3rd party library
- import ordereddict
- OrderedDict = ordereddict.OrderedDict
-
-get_curses_keycodes = lambda: dict(
- ((keyname, getattr(curses, keyname))
- for keyname in dir(curses)
- if keyname.startswith('KEY_'))
-)
+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
-# override a few curses constants with easier mnemonics,
-# there may only be a 1:1 mapping, so for those who desire
-# to use 'KEY_DC' from, perhaps, ported code, recommend
-# that they simply compare with curses.KEY_DC.
-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),
-)
-# Inject KEY_{names} that we think would be useful, there are no curses
-# definitions for the keypad keys. We need keys that generate multibyte
-# sequences, though it is useful to have some aliases for basic control
-# characters such as TAB.
-_lastval = max(get_curses_keycodes().values())
-for key in ('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 += 1
- setattr(curses, 'KEY_{0}'.format(key), _lastval)
+class Keystroke(six.text_type):
+ """
+ A unicode-derived class for describing a single keystroke.
-class Keystroke(six.text_type):
- """A unicode-derived class for describing keyboard input returned by
- the ``inkey()`` method of ``Terminal``, which may, at times, be a
- multibyte sequence, providing properties ``is_sequence`` as ``True``
- when the string is a known sequence, and ``code``, which returns an
- integer value that may be compared against the terminal class attributes
- such as ``KEY_LEFT``.
+ 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
@@ -69,31 +46,54 @@ class Keystroke(six.text_type):
@property
def is_sequence(self):
- "Whether the value represents a multibyte sequence (bool)."
+ """Whether the value represents a multibyte sequence (bool)."""
return self._code is not None
def __repr__(self):
- return self._name is None and six.text_type.__repr__(self) or self._name
+ """Docstring overwritten."""
+ return (self._name is None and
+ six.text_type.__repr__(self) or
+ self._name)
__repr__.__doc__ = six.text_type.__doc__
@property
def name(self):
- "String-name of key sequence, such as ``'KEY_LEFT'`` (str)."
+ """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)."
+ """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():
- """get_keyboard_codes() -> dict
+ """
+ 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, ``KEY_LEFT``. These are derived from the attributes by the
- same of the curses module, with the following exceptions:
+ 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``
@@ -102,6 +102,12 @@ def get_keyboard_codes():
* ``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)
@@ -112,14 +118,20 @@ def get_keyboard_codes():
def _alternative_left_right(term):
- """_alternative_left_right(T) -> dict
+ r"""
+ Determine and return mapping of left and right arrow keys sequences.
- Return dict of sequences ``term._cuf1``, and ``term._cub1``,
- valued as ``KEY_RIGHT``, ``KEY_LEFT`` when appropriate if available.
+ :param blessings.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.
- some terminals report a different value for *kcuf1* than *cuf1*, but
- actually send the value of *cuf1* for right arrow key (which is
- non-destructive space).
+ 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' ':
@@ -130,13 +142,23 @@ def _alternative_left_right(term):
def get_keyboard_sequences(term):
- """get_keyboard_sequences(T) -> (OrderedDict)
+ r"""
+ Return mapping of keyboard sequences paired by keycodes.
+
+ :param blessings.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, constant) from blessings Terminal instance ``term``,
- where ``sequence`` is a multibyte input sequence, such as u'\x1b[D',
- and ``constant`` is a constant, such as term.KEY_LEFT. The return
- value is an OrderedDict instance, with their keys sorted longest-first.
+ (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
@@ -170,35 +192,75 @@ def get_keyboard_sequences(term):
def resolve_sequence(text, mapper, codes):
- """resolve_sequence(text, mapper, codes) -> Keystroke()
-
- Returns first matching Keystroke() instance for sequences found in
- ``mapper`` beginning with input ``text``, where ``mapper`` is an
- OrderedDict of unicode multibyte sequences, such as u'\x1b[D' paired by
- their integer value (260), and ``codes`` is a dict of integer values (260)
- paired by their mnemonic name, 'KEY_LEFT'.
+ 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.
+
+ :param text: string of characters received from terminal input stream.
+ :param OrderedDict mapper: an OrderedDict of unicode multibyte sequences,
+ such as u'\x1b[D' paired by their integer value (260)
+ :param 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'')
-"""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.
+def _inject_curses_keynames():
+ r"""
+ Inject KEY_NAMES that we think would be useful into the curses module.
+
+ This function compliments the global constant
+ :obj:`DEFAULT_SEQUENCE_MIXIN`. It is important to note that this
+ function has the side-effect of **injecting** new attributes to the
+ curses module, and is called from the global namespace at time of
+ import.
-This goes for many: rxvt, putty, iTerm.
+ 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.
-These "mixins" are used for *all* terminals, regardless of their type.
+ 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.
-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.
-"""
+ It is not necessary to directly "monkeypatch" the curses module to
+ contain these constants, as they will also be accessible as attributes
+ of the Terminal class instance, they are provided only for convenience
+ when mixed in with other curses code.
+ """
+ _lastval = max(get_curses_keycodes().values())
+ for key in ('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 += 1
+ setattr(curses, 'KEY_{0}'.format(key), _lastval)
+
+_inject_curses_keynames()
+
+#: 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.
@@ -268,3 +330,22 @@ DEFAULT_SEQUENCE_MIXIN = (
(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/blessings/sequences.py b/blessings/sequences.py
index 8667693..00241de 100644
--- a/blessings/sequences.py
+++ b/blessings/sequences.py
@@ -1,9 +1,7 @@
# encoding: utf-8
-" This sub-module provides 'sequence awareness' for blessings."
+"""This module provides 'sequence awareness'."""
-__all__ = ['init_sequence_patterns', 'Sequence', 'SequenceTextWrapper']
-
-# built-ins
+# std imports
import functools
import textwrap
import warnings
@@ -11,29 +9,55 @@ import math
import re
# local
-from ._binterms import binary_terminals as _BINTERM_UNSUPPORTED
+from blessings._binterms import BINARY_TERMINALS, BINTERM_UNSUPPORTED_MSG
-# 3rd-party
+# 3rd party
import wcwidth
import six
-_BINTERM_UNSUPPORTED_MSG = (
- u"Terminal kind {0!r} contains binary-packed capabilities, blessings "
- u"is likely to fail to measure the length of its sequences.")
+__all__ = ['init_sequence_patterns', 'Sequence', 'SequenceTextWrapper']
+
+
+def _sort_sequences(regex_seqlist):
+ """
+ Sort, filter, and return ``regex_seqlist`` in ascending order of length.
+ :param list regex_seqlist: list of strings.
+ :rtype: list
+ :returns: given list filtered and sorted.
-def _merge_sequences(inp):
- """Merge a list of input sequence patterns for use in a regular expression.
+ Any items that are Falsey (such as ``None``, ``''``) are removed from
+ the return list. The longest expressions are returned first.
+ Merge a list of input sequence patterns for use in a regular expression.
Order by lengthyness (full sequence set precedent over subset),
and exclude any empty (u'') sequences.
"""
- return sorted(list(filter(None, inp)), key=len, reverse=True)
+ # The purpose of sorting longest-first, is that we should want to match
+ # a complete, longest-matching final sequence in preference of a
+ # shorted sequence that partially matches another. This does not
+ # typically occur for output sequences, though with so many
+ # programmatically generated regular expressions for so many terminal
+ # types, it is feasible.
+ return sorted(list(filter(None, regex_seqlist)), key=len, reverse=True)
def _build_numeric_capability(term, cap, optional=False,
base_num=99, nparams=1):
- """ Build regexp from capabilities having matching numeric
- parameter contained within termcap value: n->(\d+).
+ r"""
+ Return regular expression for capabilities containing specified digits.
+
+ This differs from function :func:`_build_any_numeric_capability`
+ in that, for the given ``base_num`` and ``nparams``, the value of
+ ``<base_num>-1``, through ``<base_num>+1`` inclusive is replaced
+ by regular expression pattern ``\d``. Any other digits found are
+ *not* replaced.
+
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :param str cap: terminal capability name.
+ :param int num: the numeric to use for parameterized capability.
+ :param int nparams: the number of parameters to use for capability.
+ :rtype: str
+ :returns: regular expression for the given capability.
"""
_cap = getattr(term, cap)
opt = '?' if optional else ''
@@ -51,13 +75,23 @@ def _build_numeric_capability(term, cap, optional=False,
def _build_any_numeric_capability(term, cap, num=99, nparams=1):
- """ Build regexp from capabilities having *any* digit parameters
- (substitute matching \d with pattern \d and return).
+ r"""
+ Return regular expression for capabilities containing any numerics.
+
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :param str cap: terminal capability name.
+ :param int num: the numeric to use for parameterized capability.
+ :param int nparams: the number of parameters to use for capability.
+ :rtype: str
+ :returns: regular expression for the given capability.
+
+ Build regular expression from capabilities having *any* digit parameters:
+ substitute any matching ``\d`` with literal ``\d`` and return.
"""
_cap = getattr(term, cap)
if _cap:
cap_re = re.escape(_cap(*((num,) * nparams)))
- cap_re = re.sub('(\d+)', r'(\d+)', cap_re)
+ cap_re = re.sub(r'(\d+)', r'(\d+)', cap_re)
if r'(\d+)' in cap_re:
return cap_re
warnings.warn('Missing numerics in %r, %r' % (cap, cap_re))
@@ -65,8 +99,11 @@ def _build_any_numeric_capability(term, cap, num=99, nparams=1):
def get_movement_sequence_patterns(term):
- """ Build and return set of regexp for capabilities of ``term`` known
- to cause movement.
+ """
+ Get list of regular expressions for sequences that cause movement.
+
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :rtype: list
"""
bnc = functools.partial(_build_numeric_capability, term)
@@ -106,8 +143,11 @@ def get_movement_sequence_patterns(term):
def get_wontmove_sequence_patterns(term):
- """ Build and return set of regexp for capabilities of ``term`` known
- not to cause any movement.
+ """
+ Get list of regular expressions for sequences not causing movement.
+
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :rtype: list
"""
bnc = functools.partial(_build_numeric_capability, term)
bna = functools.partial(_build_any_numeric_capability, term)
@@ -226,62 +266,77 @@ def get_wontmove_sequence_patterns(term):
def init_sequence_patterns(term):
- """Given a Terminal instance, ``term``, this function processes
+ """
+ Build database of regular expressions of terminal sequences.
+
+ Given a Terminal instance, ``term``, this function processes
and parses several known terminal capabilities, and builds and
- returns a dictionary database of regular expressions, which may
- be re-attached to the terminal by attributes of the same key-name:
+ returns a dictionary database of regular expressions, which is
+ re-attached to the terminal by attributes of the same key-name.
+
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :rtype: dict
+ :returns: dictionary containing mappings of sequence "groups",
+ containing a compiled regular expression which it matches:
+
+ - ``_re_will_move``
+
+ Any sequence matching this pattern will cause the terminal
+ cursor to move (such as *term.home*).
- ``_re_will_move``
- any sequence matching this pattern will cause the terminal
- cursor to move (such as *term.home*).
+ - ``_re_wont_move``
- ``_re_wont_move``
- any sequence matching this pattern will not cause the cursor
- to move (such as *term.bold*).
+ Any sequence matching this pattern will not cause the cursor
+ to move (such as *term.bold*).
- ``_re_cuf``
- regular expression that matches term.cuf(N) (move N characters forward),
- or None if temrinal is without cuf sequence.
+ - ``_re_cuf``
- ``_cuf1``
- *term.cuf1* sequence (cursor forward 1 character) as a static value.
+ Regular expression that matches term.cuf(N) (move N characters
+ forward), or None if temrinal is without cuf sequence.
- ``_re_cub``
- regular expression that matches term.cub(N) (move N characters backward),
- or None if terminal is without cub sequence.
+ - ``_cuf1``
- ``_cub1``
- *term.cuf1* sequence (cursor backward 1 character) as a static value.
+ *term.cuf1* sequence (cursor forward 1 character) as a static value.
+
+ - ``_re_cub``
+
+ Regular expression that matches term.cub(N) (move N characters
+ backward), or None if terminal is without cub sequence.
+
+ - ``_cub1``
+
+ *term.cuf1* sequence (cursor backward 1 character) as a static value.
These attributes make it possible to perform introspection on strings
containing sequences generated by this terminal, to determine the
printable length of a string.
"""
- if term.kind in _BINTERM_UNSUPPORTED:
- warnings.warn(_BINTERM_UNSUPPORTED_MSG.format(term.kind))
+ if term.kind in BINARY_TERMINALS:
+ warnings.warn(BINTERM_UNSUPPORTED_MSG.format(term.kind))
# Build will_move, a list of terminal capabilities that have
# indeterminate effects on the terminal cursor position.
_will_move = set()
if term.does_styling:
- _will_move = _merge_sequences(get_movement_sequence_patterns(term))
+ _will_move = _sort_sequences(get_movement_sequence_patterns(term))
# Build wont_move, a list of terminal capabilities that mainly affect
# video attributes, for use with measure_length().
_wont_move = set()
if term.does_styling:
- _wont_move = _merge_sequences(get_wontmove_sequence_patterns(term))
+ _wont_move = _sort_sequences(get_wontmove_sequence_patterns(term))
_wont_move += [
# some last-ditch match efforts; well, xterm and aixterm is going
# to throw \x1b(B and other oddities all around, so, when given
# input such as ansi art (see test using wall.ans), and well,
- # theres no reason a vt220 terminal shouldn't be able to recognize
- # blue_on_red, even if it didn't cause it to be generated. these
- # are final "ok, i will match this, anyway"
- re.escape(u'\x1b') + r'\[(\d+)m',
- re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m',
- re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m',
+ # there is no reason a vt220 terminal shouldn't be able to
+ # recognize blue_on_red, even if it didn't cause it to be
+ # generated. These are final "ok, i will match this, anyway" for
+ # basic SGR sequences.
re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)\;(\d+)m',
+ re.escape(u'\x1b') + r'\[(\d+)\;(\d+)\;(\d+)m',
+ re.escape(u'\x1b') + r'\[(\d+)\;(\d+)m',
+ re.escape(u'\x1b') + r'\[(\d+)m',
re.escape(u'\x1b(B'),
]
@@ -315,16 +370,27 @@ def init_sequence_patterns(term):
class SequenceTextWrapper(textwrap.TextWrapper):
+
+ """This 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):
"""
- escape-sequence aware variant of _wrap_chunks. Though
- movement sequences, such as term.left() are certainly not
- honored, sequences such as term.bold() are, and are not
- broken mid-sequence.
+ 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):
@@ -362,12 +428,13 @@ class SequenceTextWrapper(textwrap.TextWrapper):
return lines
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
- """_handle_long_word(chunks : [string],
- cur_line : [string],
- cur_len : int, width : int)
+ """Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`.
- Handle a chunk of text (most likely a word, not whitespace) that
- is too long to fit in any line.
+ 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
@@ -417,153 +484,187 @@ 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 .rjust(), .ljust(),
- .center(), and .len()
+ of printable length, allowing a properly implemented :meth:`rjust`,
+ :meth:`ljust`, :meth:`center`, and :meth:`length`.
"""
def __new__(cls, sequence_text, term):
- """Sequence(sequence_text, term) -> unicode object
+ """
+ Class constructor.
- :arg sequence_text: A string containing sequences.
- :arg term: Terminal instance this string was created with.
+ :param sequence_text: A string that may contain sequences.
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
"""
new = six.text_type.__new__(cls, sequence_text)
new._term = term
return new
def ljust(self, width, fillchar=u' '):
- """S.ljust(width, fillchar) -> unicode
+ """
+ Return string containing sequences, left-adjusted.
- Returns string derived from unicode string ``S``, left-adjusted
- by trailing whitespace padding ``fillchar``."""
- rightside = fillchar * int((max(0.0, float(width - self.length())))
- / float(len(fillchar)))
+ :param int width: Total width given to right-adjust ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :param 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' '):
- """S.rjust(width, fillchar=u'') -> unicode
+ """
+ Return string containing sequences, right-adjusted.
- Returns string derived from unicode string ``S``, right-adjusted
- by leading whitespace padding ``fillchar``."""
- leftside = fillchar * int((max(0.0, float(width - self.length())))
- / float(len(fillchar)))
+ :param int width: Total width given to right-adjust ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :param 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' '):
- """S.center(width, fillchar=u'') -> unicode
+ """
+ Return string containing sequences, centered.
- Returns string derived from unicode string ``S``, centered
- and surrounded with whitespace padding ``fillchar``."""
+ :param int width: Total width given to center ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :param 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)))
+ 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):
- """S.length() -> int
-
- Returns printable length of unicode string ``S`` that may contain
- terminal sequences.
-
- Although accounted for, strings containing sequences such as
- ``term.clear`` will not give accurate returns, it is not
- considered lengthy (a length of 0). Combining characters,
- are also not considered lengthy.
+ 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 (``+``), but ``\b`` is simply a length of 0.
+ length of 1 (displays as ``+``), but ``\b`` alone is simply a
+ length of 0.
Some characters may consume more than one cell, mainly those CJK
Unified Ideographs (Chinese, Japanese, Korean) defined by Unicode
as half or full-width characters.
-
- For example:
- >>> from blessings import Terminal
- >>> from blessings.sequences import Sequence
- >>> term = Terminal()
- >>> Sequence(term.clear + term.red(u'コンニチハ')).length()
- 5
"""
# because combining 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())
- def strip(self, chars=None):
- """S.strip([chars]) -> unicode
+ # we require ur"" for the docstring, but it is not supported by pep257
+ # tool: https://github.com/GreenSteam/pep257/issues/116
+ length.__doc__ += (
+ u"""For example:
- Return a copy of the string S with terminal sequences removed, and
- leading and trailing whitespace removed.
+ >>> from blessings import Terminal
+ >>> from blessings.sequences import Sequence
+ >>> term = Terminal()
+ >>> Sequence(term.clear + term.red(u'コンニチハ'), term).length()
+ 10
+
+ .. note:: Although accounted for, strings containing sequences such as
+ ``term.clear`` will not give accurate returns, it is not
+ considered lengthy (a length of 0).
+ """)
+
+ def strip(self, chars=None):
+ """
+ Return string of sequences, leading, and trailing whitespace removed.
- If chars is given and not None, remove characters in chars instead.
+ :param str chars: Remove characters in chars instead of whitespace.
+ :rtype: str
"""
return self.strip_seqs().strip(chars)
def lstrip(self, chars=None):
- """S.lstrip([chars]) -> unicode
-
- Return a copy of the string S with terminal sequences and leading
- whitespace removed.
+ """
+ Return string of all sequences and leading whitespace removed.
- If chars is given and not None, remove characters in chars instead.
+ :param str chars: Remove characters in chars instead of whitespace.
+ :rtype: str
"""
return self.strip_seqs().lstrip(chars)
def rstrip(self, chars=None):
- """S.rstrip([chars]) -> unicode
-
- Return a copy of the string S with terminal sequences and trailing
- whitespace removed.
+ """
+ Return string of all sequences and trailing whitespace removed.
- If chars is given and not None, remove characters in chars instead.
+ :param str chars: Remove characters in chars instead of whitespace.
+ :rtype: str
"""
return self.strip_seqs().rstrip(chars)
def strip_seqs(self):
- """S.strip_seqs() -> unicode
+ r"""
+ Return string of all sequences removed.
- Return a string without sequences for a string that contains
- sequences for the Terminal with which they were created.
-
- Where sequence ``move_right(n)`` is detected, it is replaced with
- ``n * u' '``, and where ``move_left()`` or ``\\b`` is detected,
- those last-most characters are destroyed.
-
- All other sequences are simply removed. An example,
>>> from blessings import Terminal
>>> from blessings.sequences import Sequence
>>> term = Terminal()
- >>> Sequence(term.clear + term.red(u'test')).strip_seqs()
- u'test'
+ >>> Sequence(term.cuf(5) + term.red(u'test'), term).strip_seqs()
+ u' test'
+
+ :rtype: str
+
+ This method is used to determine the printable width of a string,
+ and is the first pass of :meth:`length`.
+
+ .. note:: Non-destructive sequences that adjust horizontal distance
+ (such as ``\b`` or ``term.cuf(5)``) are replaced by destructive
+ space or erasing.
"""
# nxt: points to first character beyond current escape sequence.
# width: currently estimated display length.
- input = self.padd()
+ inp = self.padd()
outp = u''
nxt = 0
- for idx in range(0, len(input)):
+ for idx in range(0, len(inp)):
if idx == nxt:
# at sequence, point beyond it,
- nxt = idx + measure_length(input[idx:], self._term)
+ nxt = idx + measure_length(inp[idx:], self._term)
if nxt <= idx:
# append non-sequence to outp,
- outp += input[idx]
+ outp += inp[idx]
# point beyond next sequence, if any,
# otherwise point to next character
- nxt = idx + measure_length(input[idx:], self._term) + 1
+ nxt = idx + measure_length(inp[idx:], self._term) + 1
return outp
def padd(self):
- """S.padd() -> unicode
- Make non-destructive space or backspace into destructive ones.
+ r"""
+ Transform non-destructive space or backspace into destructive ones.
+
+ >>> from blessings import Terminal
+ >>> from blessings.sequences import Sequence
+ >>> term = Terminal()
+ >>> seq = term.cuf(10) + '-->' + '\b\b'
+ >>> padded = Sequence(seq, Terminal()).padd()
+ >>> print(seq, padded)
+ (u'\x1b[10C-->\x08\x08', u' -')
+
+ :rtype: str
- Where sequence ``move_right(n)`` is detected, it is replaced with
- ``n * u' '``. Where sequence ``move_left(n)`` or ``\\b`` is
+ This method is used to determine the printable width of a string,
+ and is the first pass of :meth:`strip_seqs`.
+
+ Where sequence ``term.cuf(n)`` is detected, it is replaced with
+ ``n * u' '``, and where sequence ``term.cub1(n)`` or ``\\b`` is
detected, those last-most characters are destroyed.
"""
outp = u''
@@ -583,21 +684,31 @@ class Sequence(six.text_type):
def measure_length(ucs, term):
- """measure_length(S, term) -> int
+ r"""
+ Return non-zero for string ``ucs`` that begins with a terminal sequence.
+
+ :param str ucs: String that may begin with a terminal sequence.
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :rtype: int
+ :returns: length of the sequence beginning at ``ucs``, if any.
+ Otherwise 0 if ``ucs`` does not begin with a terminal
+ sequence.
+
+ Returns non-zero for string ``ucs`` that begins with a terminal
+ sequence, of the length of characters in ``ucs`` until the *first*
+ matching sequence ends.
- Returns non-zero for string ``S`` that begins with a terminal sequence,
- that is: the width of the first unprintable sequence found in S. For use
- as a *next* pointer to skip past sequences. If string ``S`` is not a
- sequence, 0 is returned.
+ This is used as a *next* pointer to iterate over sequences. If the string
+ ``ucs`` does not begin with a sequence, ``0`` is returned.
A sequence may be a typical terminal sequence beginning with Escape
(``\x1b``), especially a Control Sequence Initiator (``CSI``, ``\x1b[``,
...), or those of ``\a``, ``\b``, ``\r``, ``\n``, ``\xe0`` (shift in),
- ``\x0f`` (shift out). They do not necessarily have to begin with CSI, they
- need only match the capabilities of attributes ``_re_will_move`` and
- ``_re_wont_move`` of terminal ``term``.
+ and ``\x0f`` (shift out). They do not necessarily have to begin with CSI,
+ they need only match the capabilities of attributes ``_re_will_move`` and
+ ``_re_wont_move`` of :class:`~.Terminal` which are constructed at time
+ of class initialization.
"""
-
# simple terminal control characters,
ctrl_seqs = u'\a\b\r\n\x0e\x0f'
@@ -613,7 +724,7 @@ def measure_length(ucs, term):
)
if matching_seq:
- start, end = matching_seq.span()
+ _, end = matching_seq.span()
return end
# none found, must be printable!
@@ -621,20 +732,34 @@ def measure_length(ucs, term):
def termcap_distance(ucs, cap, unit, term):
- """termcap_distance(S, cap, unit, term) -> int
+ r"""
+ Return distance of capabilities ``cub``, ``cub1``, ``cuf``, and ``cuf1``.
+
+ :param str ucs: Terminal sequence created using any of ``cub(n)``,
+ ``cub1``, ``cuf(n)``, or ``cuf1``.
+ :param str cap: ``cub`` or ``cuf`` only.
+ :param int unit: Unit multiplier, should always be ``1`` or ``-1``.
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :rtype: int
+ :returns: the printable distance determined by the given sequence. If
+ the given sequence does not match any of the ``cub`` or ``cuf``
- Match horizontal distance by simple ``cap`` capability name, ``cub1`` or
- ``cuf1``, with string matching the sequences identified by Terminal
- instance ``term`` and a distance of ``unit`` *1* or *-1*, for right and
- left, respectively.
+ This supports the higher level function :func:`horizontal_distance`.
+
+ Match horizontal distance by simple ``cap`` capability name, either
+ from termcap ``cub`` or ``cuf``, with string matching the sequences
+ identified by Terminal instance ``term`` and a distance of ``unit``
+ *1* or *-1*, for right and left, respectively.
Otherwise, by regular expression (using dynamic regular expressions built
- using ``cub(n)`` and ``cuf(n)``. Failing that, any of the standard SGR
- sequences (``\033[C``, ``\033[D``, ``\033[nC``, ``\033[nD``).
+ when :class:`~.Terminal` is first initialized) of ``cub(n)`` and
+ ``cuf(n)``. Failing that, any of the standard SGR sequences
+ (``\033[C``, ``\033[D``, ``\033[<n>C``, ``\033[<n>D``).
Returns 0 if unmatched.
"""
- assert cap in ('cuf', 'cub')
+ assert cap in ('cuf', 'cub'), cap
+ assert unit in (1, -1), unit
# match cub1(left), cuf1(right)
one = getattr(term, '_%s1' % (cap,))
if one and ucs.startswith(one):
@@ -650,16 +775,23 @@ def termcap_distance(ucs, cap, unit, term):
def horizontal_distance(ucs, term):
- """horizontal_distance(S, term) -> int
+ r"""
+ Determine the horizontal distance of single terminal sequence, ``ucs``.
- Returns Integer ``<n>`` in SGR sequence of form ``<ESC>[<n>C``
- (T.move_right(n)), or ``-(n)`` in sequence of form ``<ESC>[<n>D``
- (T.move_left(n)). Returns -1 for backspace (0x08), Otherwise 0.
+ :param ucs: terminal sequence, which may be any of the following:
- Tabstop (``\t``) cannot be correctly calculated, as the relative column
- position cannot be determined: 8 is always (and, incorrectly) returned.
- """
+ - move_right (fe. ``<ESC>[<n>C``): returns value ``(n)``.
+ - move left (fe. ``<ESC>[<n>D``): returns value ``-(n)``.
+ - backspace (``\b``) returns value -1.
+ - tab (``\t``) returns value 8.
+ :param blessings.Terminal term: :class:`~.Terminal` instance.
+ :rtype: int
+
+ .. note:: Tabstop (``\t``) cannot be correctly calculated, as the relative
+ column position cannot be determined: 8 is always (and, incorrectly)
+ returned.
+ """
if ucs.startswith('\b'):
return -1
diff --git a/blessings/terminal.py b/blessings/terminal.py
index 29f4835..94ecef9 100644
--- a/blessings/terminal.py
+++ b/blessings/terminal.py
@@ -1,5 +1,6 @@
-"This primary module provides the Terminal class."
-# standard imports
+# encoding: utf-8
+"""This module contains :class:`Terminal`, the primary API interface."""
+# std imports
import collections
import contextlib
import functools
@@ -13,31 +14,25 @@ import struct
import time
import sys
import os
+import io
try:
import termios
import fcntl
import tty
except ImportError:
- tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width')
- msg_nosupport = (
+ _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 '{0}'. The following methods "
"of Terminal are dummy/no-op unless a deriving class overrides "
- "them: {1}".format(sys.platform.lower(), ', '.join(tty_methods)))
- warnings.warn(msg_nosupport)
+ "them: {1}".format(sys.platform.lower(), ', '.join(_TTY_METHODS)))
+ warnings.warn(_MSG_NOSUPPORT)
HAS_TTY = False
else:
HAS_TTY = True
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.5"""
-
-try:
_ = InterruptedError
del _
except NameError:
@@ -66,16 +61,8 @@ from .keyboard import (
class Terminal(object):
- """A wrapper for curses and related terminfo(5) terminal capabilities.
- 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.
- """
+ """Wrapper for curses and related terminfo(5) terminal capabilities."""
#: Sugary names for commonly-used capabilities
_sugar = dict(
@@ -114,46 +101,48 @@ class Terminal(object):
no_underline='rmul')
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``.
-
"""
+ Class initializer.
+
+ :param str kind: A terminal string as taken by
+ :func:`curses.setupterm`. Defaults to the value of the ``TERM``
+ Environment variable.
+
+ .. note:: A terminal of only one ``kind`` may be initialized for
+ each process. See :obj:`_CUR_TERM`.
+
+ :param 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.
+ :param 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
+ # Using the global statement (col 8)
+
global _CUR_TERM
self._keyboard_fd = None
- # default stream is stdout, keyboard only valid as stdin when
- # output stream is stdout and output stream is a tty
+ # Default stream is stdout, keyboard only valid as stdin when
+ # output stream is stdout is a tty.
if stream is None or stream == sys.__stdout__:
stream = sys.__stdout__
self._keyboard_fd = sys.__stdin__.fileno()
try:
- stream_fd = (stream.fileno() if hasattr(stream, 'fileno')
- and callable(stream.fileno) else None)
- except IOUnsupportedOperation:
+ stream_fd = (stream.fileno() if hasattr(stream, 'fileno') and
+ callable(stream.fileno) else None)
+ except io.UnsupportedOperation:
stream_fd = None
self._is_a_tty = stream_fd is not None and os.isatty(stream_fd)
@@ -162,16 +151,15 @@ class Terminal(object):
# _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)
+ 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.
- # sys.__stdout__ seems to always have a descriptor of 1, even if output
- # is redirected.
- self._init_descriptor = (stream_fd is None and sys.__stdout__.fileno()
- or stream_fd)
+ self._init_descriptor = (stream_fd is None and
+ sys.__stdout__.fileno() or
+ stream_fd)
self._kind = kind or os.environ.get('TERM', 'unknown')
if self.does_styling:
@@ -185,7 +173,8 @@ class Terminal(object):
isinstance(self._kind, unicode)):
# pypy/2.4.0_2/libexec/lib_pypy/_curses.py, line 1131
# TypeError: initializer for ctype 'char *' must be a str
- curses.setupterm(self._kind.encode('ascii'), self._init_descriptor)
+ curses.setupterm(self._kind.encode('ascii'),
+ self._init_descriptor)
else:
curses.setupterm(self._kind, self._init_descriptor)
except curses.error as err:
@@ -207,14 +196,14 @@ class Terminal(object):
for re_name, re_val in init_sequence_patterns(self).items():
setattr(self, re_name, re_val)
- # build database of int code <=> KEY_NAME
+ # Build database of int code <=> KEY_NAME.
self._keycodes = get_keyboard_codes()
- # store attributes as: self.KEY_NAME = code
+ # 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
+ # Build database of sequence <=> KEY_NAME.
self._keymap = get_keyboard_sequences(self)
self._keyboard_buf = collections.deque()
@@ -225,29 +214,33 @@ class Terminal(object):
self._keyboard_decoder = codecs.getincrementaldecoder(
self._encoding)()
except LookupError as err:
- warnings.warn('%s, fallback to ASCII for keyboard.' % (err,))
+ warnings.warn('LookupError: %s, fallback to ASCII for '
+ 'keyboard.' % (err,))
self._encoding = 'ascii'
self._keyboard_decoder = codecs.getincrementaldecoder(
self._encoding)()
- self.stream = stream
+ self._stream = stream
def __getattr__(self, attr):
- """Return a terminal capability as Unicode string.
+ 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 ``term.normal``.
-
- This capability is also callable, so you can use ``term.bold("hi")``
- which results in the joining of (term.bold, "hi", term.normal).
+ 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 parametrized capability such as ``cup`` (cursor_address), pass
- the parameters as arguments ``some_term.cup(line, column)``. See
- manual page terminfo(5) for a complete list of capabilities.
+ >>> term.bold_blink_red_on_green("merry x-mas!").
+ u'\x1b[1m\x1b[5m\x1b[31m\x1b[42mmerry x-mas!\x1b[m'
+
+ For a parametrized capability such as ``move`` (cup), pass the
+ parameters as positional arguments ``term.move(line, column)``. See
+ manual page of terminfo(5) for a complete list of capabilities and
+ their arguments.
"""
if not self.does_styling:
return NullCallableString()
@@ -258,60 +251,82 @@ class Terminal(object):
@property
def kind(self):
- """Name of this terminal type as string."""
+ """Name of this terminal type."""
return self._kind
@property
def does_styling(self):
- """Whether this instance will emit terminal sequences (bool)."""
+ """Whether this instance will emit terminal sequences."""
return self._does_styling
@property
def is_a_tty(self):
- """Whether the ``stream`` associated with this instance is a terminal
- (bool)."""
+ """Whether :attr:`~.stream` is a terminal."""
return self._is_a_tty
@property
def height(self):
- """T.height -> int
-
- The height of the terminal in characters.
- """
+ """The height of the terminal (by number of character cells)."""
return self._height_and_width().ws_row
@property
def width(self):
- """T.width -> int
-
- The width of the terminal in characters.
- """
+ """The width of the terminal (by number of character cells)."""
return self._height_and_width().ws_col
@staticmethod
- def _winsize(fd):
- """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel)
+ def _winsize(fdesc):
+ """
+ Return named tuple describing size of the terminal by ``fdesc``.
+
+ If the given platform does not have modules :mod:`termios`,
+ :mod:`fcntl`, or :mod:`tty`, window size of 80 columns by 24
+ rows is always returned.
- The tty connected by file desriptor fd is queried for its window size,
- and returned as a collections.namedtuple instance WINSZ.
+ :param int fdesc: file descriptor queries for its window size.
+ :raises IOError: the file descriptor ``fdesc`` is not a terminal.
+ :rtype: WINSZ
- May raise exception IOError.
+ 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)
+ data = fcntl.ioctl(fdesc, termios.TIOCGWINSZ, WINSZ._BUF)
return WINSZ(*struct.unpack(WINSZ._FMT, data))
return WINSZ(ws_row=24, ws_col=80, ws_xpixel=0, ws_ypixel=0)
def _height_and_width(self):
- """Return a tuple of (terminal height, terminal width).
"""
- # TODO(jquast): hey kids, even if stdout is redirected to a file,
- # we can still query sys.__stdin__.fileno() for our terminal size.
- # -- of course, if both are redirected, we have no use for this fd.
- for fd in (self._init_descriptor, sys.__stdout__):
+ 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 24 rows is returned.
+
+ :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 fdesc in (self._init_descriptor, sys.__stdout__):
+ # pylint: disable=pointless-except
+ # Except doesn't do anything
try:
- if fd is not None:
- return self._winsize(fd)
+ if fdesc is not None:
+ return self._winsize(fdesc)
except IOError:
pass
@@ -322,24 +337,32 @@ class Terminal(object):
@contextlib.contextmanager
def location(self, x=None, y=None):
- """Return a context manager for temporarily moving the cursor.
+ """
+ Return a context manager for temporarily moving the cursor.
+
+ :param int x: Move to a specific column (optional).
+ :param int y: Move to a specific row (optional).
+ :rtype: None
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
+ print('Hello, world!')
+ print('previous 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.
+ This context manager yields no value, its side-effect is to write
+ the "save cursor position (sc)" sequence upon entering to
+ :attr:`stream` and "restore cursor position (rc)" upon entering.
+ .. note:: Store and restore cursor provides no stack: This means that
+ :meth:`location` calls cannot be chained: 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:
@@ -356,16 +379,23 @@ class Terminal(object):
@contextlib.contextmanager
def fullscreen(self):
- """Return a context manager that enters fullscreen mode while inside it
- and restores normal mode on leaving.
+ """
+ Context manager that switches to alternate screen.
+
+ :rtype: None
- Fullscreen mode is characterized by instructing the terminal emulator
- to store and save the current screen state (all screen output), switch
- to "alternate screen". Upon exiting, the previous screen state is
- returned.
+ This context manager yields no value, its side-effect is to save
+ the primary screen buffer on entering, and to restore it again
+ upon exit. The secondary screen buffer entered while using the
+ context manager also remains, and is faithfully restored again
+ on the next entrance::
- This call may not be tested; only one screen state may be saved at a
- time.
+ with term.fullscreen(), term.hidden_cursor():
+ main()
+
+ .. note:: There is only one primary and secondary screen: This means
+ that :meth:`fullscreen` calls cannot be chained: only one should
+ be entered at a time.
"""
self.stream.write(self.enter_fullscreen)
try:
@@ -375,8 +405,21 @@ class Terminal(object):
@contextlib.contextmanager
def hidden_cursor(self):
- """Return a context manager that hides the cursor upon entering,
- and makes it visible again upon exiting."""
+ """
+ Context manager that hides the cursor.
+
+ :rtype: None
+
+ This context manager yields no value, its side-effect is to emit
+ the ``hide_cursor`` sequence to :attr:`stream` on entering, and
+ to emit ``normal_cursor`` sequence upon exit::
+
+ with term.fullscreen(), term.hidden_cursor():
+ main()
+
+ .. note:: :meth:`hidden_cursor` calls cannot be chained: only one
+ should be entered at a time.
+ """
self.stream.write(self.hide_cursor)
try:
yield
@@ -385,15 +428,16 @@ class Terminal(object):
@property
def color(self):
- """Returns capability that sets the foreground color.
+ """
+ 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`.
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.
-
- :arg num: The number, 0-15, of the color
-
"""
if not self.does_styling:
return NullCallableString()
@@ -402,7 +446,12 @@ class Terminal(object):
@property
def on_color(self):
- "Returns capability that sets the background color."
+ """
+ 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,
@@ -410,119 +459,213 @@ class Terminal(object):
@property
def normal(self):
- "Returns sequence that resets video attribute."
+ """
+ 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 set to default.
+ """
if self._normal:
return self._normal
self._normal = resolve_capability(self, 'normal')
return self._normal
@property
+ def stream(self):
+ """
+ The output stream connected to the terminal.
+
+ This is a convenience attribute. It is used 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):
- """Return the number of colors the terminal supports.
+ """
+ The number of colors the terminal supports.
Common values are 0, 8, 16, 88, and 256. Most commonly
- this may be used to test color capabilities at all::
+ this may be used to test whether the terminal supports colors::
if term.number_of_colors:
- ..."""
+ ...
+ """
# trim value to 0, as tigetnum('colors') returns -1 if no support,
- # -2 if no such capability.
+ # and -2 if no such capability.
return max(0, self.does_styling and curses.tigetnum('colors') or -1)
@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' '):
- """T.ljust(text, [width], [fillchar]) -> unicode
-
- Return string ``text``, left-justified by printable length ``width``.
- Padding is done using the specified fill character (default is a
- space). Default ``width`` is the attached terminal's width. ``text``
- may contain terminal sequences."""
+ """
+ Return string ``text`` containing sequences, left-adjusted.
+
+ :param str text: String of text to be right-adjusted, may contain
+ terminal sequences.
+ :param int width: Total width given to right-adjust ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :param str fillchar: String for padding right-of ``text``.
+ :returns: String of ``text``, right-aligned by ``width``.
+ :rtype: str
+ """
if width is None:
width = self.width
return Sequence(text, self).ljust(width, fillchar)
def rjust(self, text, width=None, fillchar=u' '):
- """T.rjust(text, [width], [fillchar]) -> unicode
-
- Return string ``text``, right-justified by printable length ``width``.
- Padding is done using the specified fill character (default is a
- space). Default ``width`` is the attached terminal's width. ``text``
- may contain terminal sequences."""
+ """
+ Return string ``text`` containing sequences, right-adjusted.
+
+ :param str text: String of text to be right-adjusted, may contain
+ terminal sequences.
+ :param int width: Total width given to right-adjust ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :param str fillchar: String for padding left-of ``text``.
+ :returns: String of ``text``, right-aligned by ``width``.
+ :rtype: str
+ """
if width is None:
width = self.width
return Sequence(text, self).rjust(width, fillchar)
def center(self, text, width=None, fillchar=u' '):
- """T.center(text, [width], [fillchar]) -> unicode
-
- Return string ``text``, centered by printable length ``width``.
- Padding is done using the specified fill character (default is a
- space). Default ``width`` is the attached terminal's width. ``text``
- may contain terminal sequences."""
+ """
+ Return string ``text`` containing sequences, centered.
+
+ :param str text: String of text to be centered, may contain terminal
+ sequences.
+ :param int width: Total width given to center ``text``. If
+ unspecified, the width of the attached terminal is used (default).
+ :param str fillchar: String for padding left and right-of ``text``.
+ :returns: String of ``text``, centered by ``width``.
+ :rtype: str
+ """
if width is None:
width = self.width
return Sequence(text, self).center(width, fillchar)
def length(self, text):
- """T.length(text) -> int
+ u"""
+ Return printable length of string ``text`` containing sequences.
+
+ :param str text: String of text to determine printable length, may
+ contain terminal sequences.
+ :rtype: int
+ :returns: printable length of string as terminal character cells.
+
+ Strings containing text that consumes 2 character cells are supported.
- Return the printable length of string ``text``, which may contain
- terminal sequences. Strings containing sequences such as 'clear',
- which repositions the cursor, does not give accurate results, and
- their printable length is evaluated *0*..
+ >>> 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):
- """T.strip(text) -> unicode
-
- Return string ``text`` with terminal sequences removed, and leading
- and trailing whitespace removed.
+ r"""
+ Return ``text`` without sequences and leading or trailing whitespace.
+
+ :param str text: String of text that may contain terminal
+ sequences.
+ :returns: Text stripped of sequences and leading or trailing
+ whitespace.
+ :rtype: str
+
+ >>> term = blessings.Terminal()
+ >>> term.strip(u' \x1b[0;3m XXX ')
+ u'XXX'
"""
return Sequence(text, self).strip(chars)
def rstrip(self, text, chars=None):
- """T.rstrip(text) -> unicode
+ r"""
+ Return ``text`` stripped of terminal sequences and trailing whitespace.
- Return string ``text`` with terminal sequences and trailing whitespace
- removed.
+ :param str text: String of text that may contain terminal sequences.
+ :returns: Text stripped of sequences and trailing whitespace.
+ :rtype: str
+
+ >>> term = blessings.Terminal()
+ >>> term.rstrip(u' \x1b[0;3m XXX ')
+ u' XXX'
"""
return Sequence(text, self).rstrip(chars)
def lstrip(self, text, chars=None):
- """T.lstrip(text) -> unicode
+ r"""
+ Return ``text`` stripped of terminal sequences and leading whitespace.
+
+ :param str text: String of text that may contain terminal sequences.
+ :returns: Text stripped of sequences and leading whitespace.
+ :rtype: str
- Return string ``text`` with terminal sequences and leading whitespace
- removed.
+ >>> term = blessings.Terminal()
+ >>> term.lstrip(u' \x1b[0;3m XXX ')
+ u'XXX '
"""
return Sequence(text, self).lstrip(chars)
def strip_seqs(self, text):
- """T.strip_seqs(text) -> unicode
+ r"""
+ Return ``text`` stripped only of its terminal sequences.
- Return string ``text`` stripped only of its sequences.
+ :param str text: String of text that may contain terminal sequences.
+ :returns: Text stripped of sequences.
+ :rtype: str
+
+ >>> term = blessings.Terminal()
+ >>> term.strip_seqs(u'\x1b[0;3mXXX')
+ u'XXX'
"""
return Sequence(text, self).strip_seqs()
def wrap(self, text, width=None, **kwargs):
- """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode]
-
- Wrap paragraphs containing escape sequences ``text`` to the full
- ``width`` of Terminal instance *T*, unless ``width`` is specified.
- Wrapped by the virtual printable length, irregardless of the video
- attribute sequences it may contain, allowing text containing colors,
- bold, underline, etc. to be wrapped.
-
- Returns a list of strings that may contain escape sequences. See
- ``textwrap.TextWrapper`` for all available additional kwargs to
- customize wrapping behavior such as ``subsequent_indent``.
+ """
+ Wrap a string of ``text``, returning an array of wrapped lines.
+
+ :param 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`.
+ :param int width: Unlike :func:`textwrap.wrap`, ``width`` will
+ default to the width of the attached terminal.
+ :rtype: list
+ :returns: list of strings that may contain escape sequences.
+
+ See :class:`textwrap.TextWrapper` class for available keyword arguments
+ to customize wrapping behaviour.
"""
width = self.width if width is None else width
lines = []
@@ -535,29 +678,49 @@ class Terminal(object):
return lines
def _next_char(self):
- """T._next_char() -> unicode
+ """
+ Read and decode next byte from keyboard stream.
- Read and decode next byte from keyboard stream. May return u''
- if decoding is not yet complete, or completed unicode character.
- Should always return bytes when self._char_is_ready() returns True.
+ :rtype: unicode
+ :returns: a single unicode character, or ``u''`` if a multi-byte
+ sequence has not yet been fully received.
- Implementors of input streams other than os.read() on the stdin fd
- should derive and override this method.
+ This method supports :meth:`keystroke`, reading only one byte from
+ the keyboard string at a time. This method should always return
+ without blocking if called when :meth:`_char_is_ready` returns
+ 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 _char_is_ready(self, timeout=None, interruptable=True):
- """T._char_is_ready([timeout=None]) -> bool
-
- Returns True if a keypress has been detected on keyboard.
-
- When ``timeout`` is 0, this call is non-blocking, Otherwise blocking
- until keypress is detected (default). When ``timeout`` is a positive
- number, returns after ``timeout`` seconds have elapsed.
-
- If input is not a terminal, False is always returned.
+ """
+ Whether a keypress has been detected on the keyboard.
+
+ This method is used by method :meth:`keystroke` to determine if
+ a byte may be read using method :meth:`_next_char` without blocking.
+
+ :param 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).
+ :param bool interruptable: Normally, when this function is interrupted
+ by a signal, such as the installment of SIGWINCH, this function will
+ ignore this interruption and continue to poll for input up to the
+ ``timeout`` specified. If you'd rather this function return ``u''``
+ early, specify False for ``interruptable``.
+
+ This is an open issue for review to **remove** this parameter,
+ https://github.com/erikrose/blessings/issues/96
+ :rtype: bool
+ :returns: True if a keypress is awaiting to be read on the keyboard
+ attached to this terminal. If input is not a terminal, False is
+ always returned.
"""
# Special care is taken to handle a custom SIGWINCH handler, which
# causes select() to be interrupted with errno 4 (EAGAIN) --
@@ -566,13 +729,12 @@ class Terminal(object):
# beyond timeout, then False is returned. Otherwise, when timeout is 0,
# we continue to block indefinitely (default).
stime = time.time()
- check_w, check_x, ready_r = [], [], [None, ]
+ ready_r = [None, ]
check_r = [self._keyboard_fd] if self._keyboard_fd is not None else []
while HAS_TTY and True:
try:
- ready_r, ready_w, ready_x = select.select(
- check_r, check_w, check_x, timeout)
+ ready_r, _, _ = select.select(check_r, [], [], timeout)
except InterruptedError:
if not interruptable:
return u''
@@ -591,28 +753,36 @@ class Terminal(object):
@contextlib.contextmanager
def keystroke_input(self, raw=False):
- """Return a context manager that sets up the terminal to do
- key-at-a-time input.
-
- On entering the context manager, "cbreak" mode is activated, disabling
- line buffering of keyboard input and turning off automatic echoing of
- input. (You must explicitly print any input if you'd like it shown.)
- Also referred to as 'rare' mode, this is the opposite of 'cooked' mode,
- the default for most shells.
-
- If ``raw`` is True, enter "raw" mode instead. Raw mode differs in that
- the interrupt, quit, suspend, and flow control characters are all
- passed through as their raw character values instead of generating a
- signal.
-
- More information can be found in the manual page for curses.h,
- http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak
-
- The python manual for curses,
- http://docs.python.org/2/library/curses.html
-
- Note also that setcbreak sets VMIN = 1 and VTIME = 0,
- http://www.unixwiz.net/techtips/termios-vmin-vtime.html
+ """
+ Context manager that enables key-at-a-time input.
+
+ Normally, characters received from the keyboard cannot be read by
+ python until the return key is pressed: this is referred to as
+ "cooked" or "canonical input" mode, allowing the tty driver to perform
+ line editing before being read by your program and is usually the
+ default mode set by your unix shell before executing any programs.
+
+ Also referred to as 'rare' mode, entering this context is the opposite
+ of 'cooked' mode: On entering, :func:`tty.setcbreak` mode is activated,
+ disabling line buffering of keyboard input and turning off automatic
+ echoing of input. This allows each keystroke to be received
+ immediately after it is pressed.
+
+ :param bool raw: When True, enter :func:`tty.setraw` mode instead.
+ Raw mode differs in that the interrupt, quit, suspend, and flow
+ control characters are all passed through as their raw character
+ values instead of generating a signal.
+
+ This context manager yields no value, its side-effect is to
+ set the :mod:`termios` attributes of the terminal attached to
+ :obj:`sys.__stdin__`.
+
+ .. note:: you must explicitly print any input received if you'd like
+ it displayed. And, if providing any kind of editing, you must
+ also handle backspace and other line editing control characters.
+
+ .. 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:
@@ -631,17 +801,19 @@ class Terminal(object):
@contextlib.contextmanager
def keypad(self):
- """
- Context manager that enables keypad input (*keyboard_transmit* mode).
+ r"""
+ Context manager that enables keypad input ("keyboard_transmit" mode).
- This enables the effect of calling the curses function keypad(3x):
- display terminfo(5) capability `keypad_xmit` (smkx) upon entering,
- and terminfo(5) capability `keypad_local` (rmkx) upon exiting.
+ This context manager yields no value, its side-effect is to emit
+ capability keypad_xmit (smkx) upon entering, and keypad_local
+ (rmkx) upon exiting.
- On an IBM-PC keypad of ttype *xterm*, with numlock off, the
- lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``.
+ 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 keypad mode, ``\\x1b[OF`` is transmitted,
+ However, upon entering :meth:`keypad`, ``\\x1b[OF`` is transmitted,
translating to ``KEY_LL`` (lower-left key), allowing diagonal
direction keys to be determined.
"""
@@ -652,39 +824,37 @@ class Terminal(object):
self.stream.write(self.rmkx)
def keystroke(self, timeout=None, esc_delay=0.35, interruptable=True):
- """T.keystroke(timeout=None, [esc_delay, [interruptable]]) -> Keystroke
-
- Receive next keystroke from keyboard (stdin), blocking until a
- keypress is received or ``timeout`` elapsed, if specified.
-
- When used without the context manager ``cbreak``, stdin remains
- line-buffered, and this function will block until return is pressed,
- even though only one unicode character is returned at a time..
-
- The value returned is an instance of ``Keystroke``, with properties
- ``is_sequence``, and, when True, non-None values for attributes
- ``code`` and ``name``. The value of ``code`` may be compared against
- attributes of this terminal beginning with *KEY*, such as
- ``KEY_ESCAPE``.
-
- To distinguish between ``KEY_ESCAPE`` and sequences beginning with
- escape, the ``esc_delay`` specifies the amount of time after receiving
- the escape character (chr(27)) to seek for the completion
- of other application keys before returning ``KEY_ESCAPE``.
-
- Normally, when this function is interrupted by a signal, such as the
- installment of SIGWINCH, this function will ignore this interruption
- and continue to poll for input up to the ``timeout`` specified. If
- you'd rather this function return ``u''`` early, specify ``False`` for
- ``interruptable``.
- """
- # TODO(jquast): "meta sends escape", where alt+1 would send '\x1b1',
- # what do we do with that? Surely, something useful.
- # comparator to term.KEY_meta('x') ?
- # TODO(jquast): Ctrl characters, KEY_CTRL_[A-Z], and the rest;
- # KEY_CTRL_\, KEY_CTRL_{, etc. are not legitimate
- # attributes. comparator to term.KEY_ctrl('z') ?
-
+ """
+ Receive and return next keystroke from keyboard within given timeout.
+
+ :param float timeout: Number of seconds to allow to elapse without
+ keystroke before returning. When None (default), this
+ method blocks indefinitely.
+ :param float esc_delay: To distinguish between ``KEY_ESCAPE`` and
+ sequences beginning with escape, the parameter ``esc_delay``
+ specifies the amount of time after receiving the escape character
+ (``chr(27)``) to seek for the completion of an application key
+ before returning a :class:`~.Keystroke` for ``KEY_ESCAPE``.
+ :param bool interruptable: Normally, when this function is interrupted
+ by a signal, such as the installment of SIGWINCH, this function will
+ ignore this interruption and continue to poll for input up to the
+ ``timeout`` specified. If you'd rather this function return ``u''``
+ early, specify False for ``interruptable``.
+
+ This is an open issue for review to **remove** this parameter,
+ https://github.com/erikrose/blessings/issues/96
+ :rtype: :class:`~.Keystroke`.
+ :raises NoKeyboard: The :attr:`stream` is not a terminal with
+ timeout parameter as the default value of None, which would
+ cause the program to hang indefinitely.
+ :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:`keystroke_input`, :obj:`sys.__stdin__` remains
+ line-buffered, and this function will block until the return key
+ is pressed.
+ """
if timeout is None and self._keyboard_fd is None:
raise NoKeyboard(
'Waiting for a keystroke on a terminal with no keyboard '
@@ -692,10 +862,18 @@ class Terminal(object):
'timeout and revise your program logic.')
def time_left(stime, timeout):
- """time_left(stime, timeout) -> float
+ """
+ Return time remaining since ``stime`` before given ``timeout``.
- Returns time-relative time remaining before ``timeout``
- after time elapsed since ``stime``.
+ This function assists determining the value of ``timeout`` for
+ class method :meth:`_char_is_ready`.
+
+ :param float stime: starting time for measurement
+ :param float timeout: timeout period, may be set to None to
+ indicate no timeout (where 0 is always returned).
+ :rtype: float or int
+ :returns: time remaining as float. If no time is remaining,
+ then the integer ``0`` is returned.
"""
if timeout is not None:
if timeout == 0:
@@ -718,66 +896,82 @@ class Terminal(object):
ucs += self._next_char()
# decode keystroke, if any
- ks = resolve(text=ucs)
+ keystroke = 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._char_is_ready(time_left(stime, timeout),
- interruptable):
+ while (not keystroke and
+ self._char_is_ready(time_left(stime, timeout), interruptable)):
ucs += self._next_char()
- ks = resolve(text=ucs)
+ keystroke = resolve(text=ucs)
# handle escape key (KEY_ESCAPE) vs. escape sequence (which begins
# with KEY_ESCAPE, \x1b[, \x1bO, or \x1b?), up to esc_delay when
# received. This is not optimal, but causes least delay when
# (currently unhandled, and rare) "meta sends escape" is used,
# or when an unsupported sequence is sent.
- if ks.code == self.KEY_ESCAPE:
+ if keystroke.code == self.KEY_ESCAPE:
esctime = time.time()
- while (ks.code == self.KEY_ESCAPE and
+ while (keystroke.code == self.KEY_ESCAPE and
self._char_is_ready(time_left(esctime, esc_delay))):
ucs += self._next_char()
- ks = resolve(text=ucs)
+ keystroke = resolve(text=ucs)
# buffer any remaining text received
- self._keyboard_buf.extendleft(ucs[len(ks):])
- return ks
+ self._keyboard_buf.extendleft(ucs[len(keystroke):])
+ return keystroke
class NoKeyboard(Exception):
- """Exception raised when a Terminal that has no means of input connected is
- asked to retrieve a keystroke without an infinite timeout."""
-
-
-# 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 setupterm(), so the value of cur_term cannot be changed
-# once set: subsequent calls to setupterm() have no effect.
-#
-# Therefore, the ``kind`` of each Terminal() is, in essence, a singleton.
-# This global variable reflects that, and a warning is emitted if somebody
-# expects otherwise.
-_CUR_TERM = None
+ """Illegal operation requiring a keyboard without one attached."""
+
-WINSZ = collections.namedtuple('WINSZ', (
- 'ws_row', # /* rows, in characters */
- 'ws_col', # /* columns, in characters */
- 'ws_xpixel', # /* horizontal size, pixels */
- 'ws_ypixel', # /* vertical size, pixels */
-))
-#: format of termios structure
-WINSZ._FMT = 'hhhh'
-#: buffer of termios structure appropriate for ioctl argument
-WINSZ._BUF = '\x00' * struct.calcsize(WINSZ._FMT)
+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)
+
+#: 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:`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.
+_CUR_TERM = None
diff --git a/blessings/tests/accessories.py b/blessings/tests/accessories.py
index 1d3dd45..cb4c05c 100644
--- a/blessings/tests/accessories.py
+++ b/blessings/tests/accessories.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Accessories for automated py.test runner."""
# standard imports
-from __future__ import with_statement
+from __future__ import with_statement, print_function
import contextlib
import subprocess
import functools
@@ -27,7 +27,7 @@ RECV_SEMAPHORE = b'SEMAPHORE\r\n'
all_xterms_params = ['xterm', 'xterm-256color']
many_lines_params = [30, 100]
many_columns_params = [1, 10]
-from blessings._binterms import binary_terminals
+from blessings._binterms import BINARY_TERMINALS
default_all_terms = ['screen', 'vt220', 'rxvt', 'cons25', 'linux', 'ansi']
if os.environ.get('TEST_ALLTERMS'):
try:
@@ -42,7 +42,7 @@ if os.environ.get('TEST_ALLTERMS'):
else:
available_terms = default_all_terms
all_terms_params = list(set(available_terms) - (
- set(binary_terminals) if not os.environ.get('TEST_BINTERMS')
+ set(BINARY_TERMINALS) if not os.environ.get('TEST_BINTERMS')
else set())) or default_all_terms
@@ -58,6 +58,7 @@ class as_subprocess(object):
self.func = func
def __call__(self, *args, **kwargs):
+ pid_testrunner = os.getpid()
pid, master_fd = pty.fork()
if pid == self._CHILD_PID:
# child process executes function, raises exception
@@ -93,6 +94,10 @@ class as_subprocess(object):
cov.save()
os._exit(0)
+ 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:
@@ -206,7 +211,7 @@ def unicode_parm(cap, *parms):
return u''
-@pytest.fixture(params=binary_terminals)
+@pytest.fixture(params=BINARY_TERMINALS)
def unsupported_sequence_terminals(request):
"""Terminals that emit warnings for unsupported sequence-awareness."""
return request.param
diff --git a/blessings/tests/test_core.py b/blessings/tests/test_core.py
index bc7f7f1..e69884e 100644
--- a/blessings/tests/test_core.py
+++ b/blessings/tests/test_core.py
@@ -2,10 +2,6 @@
"Core blessings Terminal() tests."
# std
-try:
- from StringIO import StringIO
-except ImportError:
- from io import StringIO
import collections
import warnings
import platform
@@ -13,6 +9,7 @@ import locale
import sys
import imp
import os
+import io
# local
from .accessories import (
@@ -25,6 +22,7 @@ from .accessories import (
# 3rd party
import mock
import pytest
+import six
def test_export_only_Terminal():
@@ -37,7 +35,7 @@ def test_null_location(all_terms):
"Make sure ``location()`` with no args just does position restoration."
@as_subprocess
def child(kind):
- t = TestTerminal(stream=StringIO(), force_styling=True)
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
with t.location():
pass
expected_output = u''.join(
@@ -51,7 +49,7 @@ def test_flipped_location_move(all_terms):
"``location()`` and ``move()`` receive counter-example arguments."
@as_subprocess
def child(kind):
- buf = StringIO()
+ buf = six.StringIO()
t = TestTerminal(stream=buf, force_styling=True)
y, x = 10, 20
with t.location(y, x):
@@ -67,7 +65,7 @@ def test_yield_keypad():
@as_subprocess
def child(kind):
# given,
- t = TestTerminal(stream=StringIO(), force_styling=True)
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
expected_output = u''.join((t.smkx, t.rmkx))
# exercise,
@@ -85,7 +83,7 @@ def test_null_fileno():
@as_subprocess
def child():
# This simulates piping output to another program.
- out = StringIO()
+ out = six.StringIO()
out.fileno = None
t = TestTerminal(stream=out)
assert (t.save == u'')
@@ -97,12 +95,12 @@ 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=StringIO())
+ t = TestTerminal(stream=six.StringIO())
assert (t.number_of_colors == 0)
@as_subprocess
def child_256_forcestyle():
- t = TestTerminal(stream=StringIO(), force_styling=True)
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
assert (t.number_of_colors == 256)
@as_subprocess
@@ -112,13 +110,13 @@ def test_number_of_colors_without_tty():
# 'ansi' on freebsd returns 0 colors, we use the 'cons25' driver,
# compatible with its kernel tty.c
kind = 'cons25'
- t = TestTerminal(kind=kind, stream=StringIO(),
+ 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=StringIO(),
+ t = TestTerminal(kind='vt220', stream=six.StringIO(),
force_styling=True)
assert (t.number_of_colors == 0)
@@ -159,7 +157,7 @@ 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=StringIO())
+ t = TestTerminal(kind=kind, stream=six.StringIO())
assert t._init_descriptor == sys.__stdout__.fileno()
assert (isinstance(t.height, int))
assert (isinstance(t.width, int))
@@ -300,20 +298,6 @@ def test_python3_2_raises_exception(monkeypatch):
assert False, 'Exception should have been raised'
-def test_IOUnsupportedOperation_dummy(monkeypatch):
- "Ensure dummy exception is used when io is without UnsupportedOperation."
- import blessings.terminal
- import io
- if hasattr(io, 'UnsupportedOperation'):
- monkeypatch.delattr('io.UnsupportedOperation')
-
- imp.reload(blessings.terminal)
- assert blessings.terminal.IOUnsupportedOperation.__doc__.startswith(
- "A dummy exception to take the place of")
- monkeypatch.undo()
- imp.reload(blessings.terminal)
-
-
def test_without_dunder():
"Ensure dunder does not remain in module (py2x InterruptedError test."
import blessings.terminal
@@ -327,7 +311,7 @@ def test_IOUnsupportedOperation():
import blessings.terminal
def side_effect():
- raise blessings.terminal.IOUnsupportedOperation
+ raise io.UnsupportedOperation
mock_stream = mock.Mock()
mock_stream.fileno = side_effect
@@ -361,7 +345,7 @@ def test_yield_fullscreen(all_terms):
"Ensure ``fullscreen()`` writes enter_fullscreen and exit_fullscreen."
@as_subprocess
def child(kind):
- t = TestTerminal(stream=StringIO(), force_styling=True)
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
t.enter_fullscreen = u'BEGIN'
t.exit_fullscreen = u'END'
with t.fullscreen():
@@ -376,7 +360,7 @@ def test_yield_hidden_cursor(all_terms):
"Ensure ``hidden_cursor()`` writes hide_cursor and normal_cursor."
@as_subprocess
def child(kind):
- t = TestTerminal(stream=StringIO(), force_styling=True)
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
t.hide_cursor = u'BEGIN'
t.normal_cursor = u'END'
with t.hidden_cursor():
@@ -447,7 +431,7 @@ def test_win32_missing_tty_modules(monkeypatch):
imp.reload(blessings.terminal)
except UserWarning:
err = sys.exc_info()[1]
- assert err.args[0] == blessings.terminal.msg_nosupport
+ assert err.args[0] == blessings.terminal._MSG_NOSUPPORT
warnings.filterwarnings("ignore", category=UserWarning)
import blessings.terminal
diff --git a/blessings/tests/test_keyboard.py b/blessings/tests/test_keyboard.py
index 46e53c2..ff4d518 100644
--- a/blessings/tests/test_keyboard.py
+++ b/blessings/tests/test_keyboard.py
@@ -3,11 +3,6 @@
# std imports
import functools
import tempfile
-try:
- from StringIO import StringIO
-except ImportError:
- import io
- StringIO = io.StringIO
import platform
import signal
import curses
@@ -35,6 +30,7 @@ from .accessories import (
# 3rd-party
import pytest
import mock
+import six
if sys.version_info[0] == 3:
unichr = chr
@@ -261,7 +257,7 @@ def test_char_is_ready_no_kb():
"_char_is_ready() always immediately returns False without a keyboard."
@as_subprocess
def child():
- term = TestTerminal(stream=StringIO())
+ term = TestTerminal(stream=six.StringIO())
stime = time.time()
assert term._keyboard_fd is None
assert not term._char_is_ready(timeout=1.1)
@@ -286,7 +282,7 @@ def test_keystroke_0s_keystroke_input_noinput_nokb():
"0-second keystroke without data in input stream and no keyboard/tty."
@as_subprocess
def child():
- term = TestTerminal(stream=StringIO())
+ term = TestTerminal(stream=six.StringIO())
with term.keystroke_input():
stime = time.time()
inp = term.keystroke(timeout=0)
@@ -312,7 +308,7 @@ def test_keystroke_1s_keystroke_input_noinput_nokb():
"1-second keystroke without input or keyboard."
@as_subprocess
def child():
- term = TestTerminal(stream=StringIO())
+ term = TestTerminal(stream=six.StringIO())
with term.keystroke_input():
stime = time.time()
inp = term.keystroke(timeout=1)
diff --git a/blessings/tests/test_length_sequence.py b/blessings/tests/test_length_sequence.py
index 0eac165..4f19b3f 100644
--- a/blessings/tests/test_length_sequence.py
+++ b/blessings/tests/test_length_sequence.py
@@ -1,4 +1,5 @@
# encoding: utf-8
+# std imports
import itertools
import platform
import termios
@@ -6,12 +7,9 @@ import struct
import fcntl
import sys
import os
-try:
- from StringIO import StringIO
-except ImportError:
- from io import StringIO
-from .accessories import (
+# local
+from blessings.tests.accessories import (
all_terms,
as_subprocess,
TestTerminal,
@@ -19,7 +17,9 @@ from .accessories import (
many_lines,
)
+# 3rd party
import pytest
+import six
def test_length_cjk():
@@ -204,7 +204,7 @@ def test_env_winsize():
# set the pty's virtual window size
os.environ['COLUMNS'] = '99'
os.environ['LINES'] = '11'
- t = TestTerminal(stream=StringIO())
+ t = TestTerminal(stream=six.StringIO())
save_init = t._init_descriptor
save_stdout = sys.__stdout__
try:
diff --git a/blessings/tests/test_sequences.py b/blessings/tests/test_sequences.py
index 8d1597e..af3619f 100644
--- a/blessings/tests/test_sequences.py
+++ b/blessings/tests/test_sequences.py
@@ -1,10 +1,6 @@
# -*- coding: utf-8 -*-
"""Tests for Terminal() sequences and sequence-awareness."""
# std imports
-try:
- from StringIO import StringIO
-except ImportError:
- from io import StringIO
import platform
import random
import sys
@@ -21,9 +17,10 @@ from .accessories import (
unicode_cap,
)
-# 3rd-party
+# 3rd party
import pytest
import mock
+import six
def test_capability():
@@ -44,7 +41,7 @@ def test_capability_without_tty():
"""Assert capability templates are '' when stream is not a tty."""
@as_subprocess
def child():
- t = TestTerminal(stream=StringIO())
+ t = TestTerminal(stream=six.StringIO())
assert t.save == u''
assert t.red == u''
@@ -55,7 +52,7 @@ def test_capability_with_forced_tty():
"""force styling should return sequences even for non-ttys."""
@as_subprocess
def child():
- t = TestTerminal(stream=StringIO(), force_styling=True)
+ t = TestTerminal(stream=six.StringIO(), force_styling=True)
assert t.save == unicode_cap('sc')
child()
@@ -94,7 +91,7 @@ def test_stream_attr():
reason="travis-ci does not have binary-packed terminals.")
def test_emit_warnings_about_binpacked():
"""Test known binary-packed terminals (kermit, avatar) emit a warning."""
- from blessings.sequences import _BINTERM_UNSUPPORTED_MSG
+ from blessings._binterms import BINTERM_UNSUPPORTED_MSG
@as_subprocess
def child(kind):
@@ -106,7 +103,7 @@ def test_emit_warnings_about_binpacked():
TestTerminal(kind=kind, force_styling=True)
except UserWarning:
err = sys.exc_info()[1]
- assert (err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(kind) or
+ assert (err.args[0] == BINTERM_UNSUPPORTED_MSG.format(kind) or
err.args[0].startswith('Unknown parameter in ') or
err.args[0].startswith('Failed to setupterm(')
), err
@@ -128,8 +125,8 @@ def test_emit_warnings_about_binpacked():
def test_unit_binpacked_unittest():
"""Unit Test known binary-packed terminals emit a warning (travis-safe)."""
import warnings
- from blessings.sequences import (_BINTERM_UNSUPPORTED_MSG,
- init_sequence_patterns)
+ from blessings._binterms import BINTERM_UNSUPPORTED_MSG
+ from blessings.sequences import init_sequence_patterns
warnings.filterwarnings("error", category=UserWarning)
term = mock.Mock()
term.kind = 'tek4207-s'
@@ -138,25 +135,25 @@ def test_unit_binpacked_unittest():
init_sequence_patterns(term)
except UserWarning:
err = sys.exc_info()[1]
- assert err.args[0] == _BINTERM_UNSUPPORTED_MSG.format(term.kind)
+ assert err.args[0] == BINTERM_UNSUPPORTED_MSG.format(term.kind)
else:
assert False, 'Previous stmt should have raised exception.'
warnings.resetwarnings()
-def test_merge_sequences():
+def test_sort_sequences():
"""Test sequences are filtered and ordered longest-first."""
- from blessings.sequences import _merge_sequences
+ from blessings.sequences import _sort_sequences
input_list = [u'a', u'aa', u'aaa', u'']
output_expected = [u'aaa', u'aa', u'a']
- assert (_merge_sequences(input_list) == output_expected)
+ assert (_sort_sequences(input_list) == output_expected)
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=StringIO(), force_styling=True)
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
with t.location(3, 4):
t.stream.write(u'hi')
expected_output = u''.join(
@@ -173,7 +170,7 @@ def test_location_without_styling():
@as_subprocess
def child_without_styling():
"""No side effect for location as a context manager without styling."""
- t = TestTerminal(stream=StringIO(), force_styling=None)
+ t = TestTerminal(stream=six.StringIO(), force_styling=None)
with t.location(3, 4):
t.stream.write(u'hi')
@@ -187,7 +184,7 @@ 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=StringIO(), force_styling=True)
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
with t.location(x=5):
pass
expected_output = u''.join(
@@ -206,7 +203,7 @@ 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=StringIO(), force_styling=True)
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
with t.location(y=5):
pass
expected_output = u''.join(
@@ -224,7 +221,7 @@ 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=StringIO(), force_styling=True)
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
COL = 5
with t.location(x=COL):
pass
@@ -244,7 +241,7 @@ 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=StringIO(), force_styling=True)
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
ROW = 5
with t.location(y=ROW):
pass
@@ -264,7 +261,7 @@ 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=StringIO(), force_styling=True)
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
with t.hidden_cursor():
pass
expected_output = u''.join(
@@ -280,7 +277,7 @@ 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=StringIO(), force_styling=True)
+ t = TestTerminal(kind=kind, stream=six.StringIO(), force_styling=True)
with t.location(0, 0):
pass
expected_output = u''.join(
@@ -354,7 +351,7 @@ 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=StringIO(), kind=kind)
+ t = TestTerminal(stream=six.StringIO(), kind=kind)
assert (t.color(5)('smoo') == 'smoo')
assert (t.on_color(6)('smoo') == 'smoo')
@@ -432,7 +429,7 @@ 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=StringIO(), force_styling=False)
+ 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:
@@ -484,7 +481,7 @@ def test_null_callable_string(all_terms):
"""Make sure NullCallableString tolerates all kinds of args."""
@as_subprocess
def child(kind):
- t = TestTerminal(stream=StringIO(), kind=kind)
+ t = TestTerminal(stream=six.StringIO(), kind=kind)
assert (t.clear == '')
assert (t.move(1 == 2) == '')
assert (t.move_x(1) == '')
diff --git a/docs/api.rst b/docs/api.rst
index 0317767..9bca2fa 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -1,30 +1,46 @@
API Documentation
=================
-terminal
---------
+terminal.py
+-----------
.. automodule:: blessings.terminal
:members:
:undoc-members:
+ :special-members: __getattr__
+.. autodata:: _CUR_TERM
-formatters
-----------
+formatters.py
+-------------
.. automodule:: blessings.formatters
:members:
:undoc-members:
+ :private-members:
+ :special-members: __call__
+.. autodata:: COLORS
+.. autodata:: COMPOUNDABLES
-keyboard
---------
+keyboard.py
+-----------
.. automodule:: blessings.keyboard
:members:
:undoc-members:
+ :private-members:
+ :special-members: __new__
+.. autofunction:: _alternative_left_right
+.. autofunction:: _inject_curses_keynames
+.. autodata:: DEFAULT_SEQUENCE_MIXIN
+.. autodata:: CURSES_KEYCODE_OVERRIDE_MIXIN
-sequences
----------
+sequences.py
+------------
.. automodule:: blessings.sequences
:members:
:undoc-members:
+ :private-members:
+.. autofunction:: _sort_sequences
+.. autofunction:: _build_numeric_capability
+.. autofunction:: _build_any_numeric_capability
diff --git a/docs/conf.py b/docs/conf.py
index 98742f6..9e5ae92 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -2,9 +2,6 @@
import sys
import os
-# local imports
-from blessings import Terminal
-
# 3rd-party
import sphinx_rtd_theme
import sphinx.environment
@@ -32,12 +29,33 @@ github_project_url = "https://github.com/erikrose/blessings"
#
# https://github.com/SuperCowPowers/workbench/issues/172
# https://groups.google.com/forum/#!topic/sphinx-users/GNx7PVXoZIU
-# http://stackoverflow.com/questions/12772927/specifying-an-online-image-in-sphinx-restructuredtext-format
+# http://stackoverflow.com/a/28778969
+#
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
+# Monkey-patch functools.wraps and contextlib.wraps
+# https://github.com/sphinx-doc/sphinx/issues/1711#issuecomment-93126473
+import functools
+def no_op_wraps(func):
+ """
+ Replaces functools.wraps in order to undo wrapping when generating Sphinx documentation
+ """
+ import sys
+ if func.__module__ is None or 'blessings' not in func.__module__:
+ return functools.orig_wraps(func)
+ def wrapper(decorator):
+ sys.stderr.write('patched for function signature: {0!r}\n'.format(func))
+ return func
+ return wrapper
+functools.orig_wraps = functools.wraps
+functools.wraps = no_op_wraps
+import contextlib
+contextlib.wraps = no_op_wraps
+from blessings.terminal import *
+
# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@@ -45,8 +63,12 @@ sphinx.environment.BuildEnvironment.warn_node = _warn_node
# 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', ]
+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']
@@ -93,7 +115,7 @@ exclude_patterns = ['_build']
# 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::).
@@ -167,16 +189,16 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# 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 <link> tag referring to it. The value of this option must be the
@@ -245,3 +267,7 @@ autodoc_member_order = 'bysource'
# when linking to standard python library, use and prefer python 3
# documentation.
intersphinx_mapping = {'http://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/further.rst b/docs/further.rst
index 4847814..ed13d81 100644
--- a/docs/further.rst
+++ b/docs/further.rst
@@ -6,7 +6,7 @@ that dive deeper into Terminal I/O programming than :class:`~.Terminal` offers.
Here are some recommended readings to help you along:
- `terminfo(5)
- <http://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5>`_
+ <http://invisible-island.net/ncurses/man/terminfo.5.html>`_
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**.
@@ -16,8 +16,8 @@ Here are some recommended readings to help you along:
of your preferred posix-like operating system.
- `The TTY demystified
- <http://www.linusakesson.net/programming/tty/index.php>`_ by
- Linus Åkesson.
+ <http://www.linusakesson.net/programming/tty/index.php>`_
+ by Linus Åkesson.
- `A Brief Introduction to Termios
<https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios/>`_ by
@@ -53,18 +53,19 @@ Here are some recommended readings to help you along:
terminal types) emitted by most terminal capabilities to an action in a
series of ``case`` switch statements.
- - Many modern libraries are now based `https://github.com/GNOME/vte
- <libvte>`_ (or just 'vte'): Gnome Terminal, sakura, Terminator, Lilyterm,
- ROXTerm, evilvte, Termit, Termite, Tilda, tinyterm, lxterminal.
- - `Thomas E. Dickey <http://invisible-island.net/>` has been maintaining
+ - Many modern libraries are now based on `libvte
+ <https://github.com/GNOME/vte>`_ (or just 'vte'): Gnome Terminal,
+ sakura, Terminator, Lilyterm, ROXTerm, evilvte, Termit, Termite, Tilda,
+ tinyterm, lxterminal.
+ - `Thomas E. Dickey <http://invisible-island.net/>`_ has been maintaining
`xterm <http://invisible-island.net/xterm/xterm.html>`_, as well as a
- primary maintainer of many related packages such as `ncurses
+ primary maintainer of many related packages such as `ncurses
<http://invisible-island.net/ncurses/ncurses.html>`_ for quite a long
while. There is often speculation and misinformation among developers of
terminal emulators and programs that interact with them. Thomas Dickey's
analysis is always thorough and complete.
- xterm, urxvt, SyncTerm, and EtherTerm.
- - There are far too many to name, Chose one you like! Thomas Dickey
+ - 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
@@ -76,7 +77,7 @@ Here are some recommended readings to help you along:
- `FreeBSD <https://github.com/freebsd/freebsd/blob/master/sys/kern/tty.c>`_
- `OpenBSD <http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/sys/kern/tty.c?content-type=text/plain>`_
- - `Illumos (Solaris) <https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/io/tty_common.c>`
+ - `Illumos (Solaris) <https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/io/tty_common.c>`_
- `Minix <https://github.com/minix3/minix/blob/master/minix/drivers/tty/tty/tty.c>`_
- `Linux <https://github.com/torvalds/linux/blob/master/drivers/tty/n_tty.c>`_
diff --git a/docs/history.rst b/docs/history.rst
index c7f5417..ce5a321 100644
--- a/docs/history.rst
+++ b/docs/history.rst
@@ -1,8 +1,9 @@
Version History
===============
-2.0
- * This release is primarily a set of contributions from
+1.9.5
+ * This release is primarily a set of contributions from the
+ `blessed <https://github.com/jquast/blessed>` fork by
:ghuser:`jquast` unless otherwise indicated.
**new features**:
@@ -34,7 +35,7 @@ Version History
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.
- - New public attribute: :attr:`~.kind`: the very same as given
+ - New public attribute: :attr:`~.kind`: the very same as given
by the keyword argument of the same (or, determined by and
equivalent to the ``TERM`` Environment variable).
- Some attributes are now properties and raise exceptions when assigned,
@@ -52,9 +53,10 @@ Version History
- The '2to3' tool is no longer used for python 3 support
- Converted nose tests to pytest via tox. Added a TeamCity build farm to
include OSX and FreeBSD testing. ``tox`` is now the primary entry point
- with which to execute tests, run static analysis, and build documentation.
- - py.test fixtures and ``@as_subprocess`` decorator for testing of many more
- terminal types than just 'xterm-256-color' as previously tested.
+ with which to execute tests, run static analysis, and build
+ documentation.
+ - py.test fixtures and ``@as_subprocess`` decorator for testing of many
+ more terminal types than just 'xterm-256-color' as previously tested.
- ``setup.py develop`` ensures a virtualenv and installs tox.
- 100% (combined) coverage.
@@ -118,10 +120,10 @@ Version History
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.
+ * 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
@@ -133,9 +135,9 @@ Version History
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 :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`.
diff --git a/docs/index.rst b/docs/index.rst
index 986f483..37794b3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -10,9 +10,9 @@ Contents:
intro
overview
- api
further
pains
+ api
history
=======
diff --git a/docs/intro.rst b/docs/intro.rst
index c41ed0f..fefb747 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -20,10 +20,13 @@
.. image:: https://img.shields.io/pypi/dm/blessings.svg
:alt: Downloads
+ :target: https://pypi.python.org/pypi/blessings
Introduction
============
+Blessings is a thin, practical wrapper around terminal capabilities in Python.
+
Coding with *Blessings* looks like this... ::
from blessings import Terminal
@@ -36,8 +39,9 @@ Coding with *Blessings* looks like this... ::
with t.location(0, t.height - 1):
print(t.center(t.blink('press any key to continue.')))
- with t.cbreak():
- t.inkey()
+ with t.keystroke_input():
+ inp = t.keystroke()
+ print('You pressed ' + repr(inp))
The Pitch
@@ -104,18 +108,19 @@ 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(5)`_ database so it works with any terminal type
+* Uses the `terminfo(5)`_ 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
+ 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.)
+ 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 formatting.
* Provides convenient access to **all** terminal capabilities.
* Outputs to any file-like object (*StringIO*, file), 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
-* Safely decodes internationalization keyboard input to their unicode equivalents.
+ calls to curses or whatever other terminal libraries you like.
+* Safely decodes internationalization keyboard input to their unicod e
+ equivalents.
* Safely decodes multibyte sequences for application/arrow keys.
* Allows the printable length of strings containing sequences to be determined.
* Provides plenty of context managers to safely express various terminal modes,
@@ -126,7 +131,8 @@ Blessings does not provide...
* Native color support on the Windows command prompt. A PDCurses_ build
of python for windows provides only partial support at this time -- there
are plans to merge with the ansi_ module in concert with colorama_ to
- resolve this. Patches welcome!
+ resolve this. `Patches welcome
+ <https://github.com/erikrose/blessings/issues/21>`_!
Further Documentation
---------------------
@@ -152,4 +158,4 @@ Blessings is under the MIT License. See the LICENSE file.
.. _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://www.openbsd.org/cgi-bin/man.cgi?query=terminfo&apropos=0&sektion=5
+.. _`terminfo(5)`: http://invisible-island.net/ncurses/man/terminfo.5.html
diff --git a/docs/overview.rst b/docs/overview.rst
index 4b78be6..f718aa8 100644
--- a/docs/overview.rst
+++ b/docs/overview.rst
@@ -1,44 +1,63 @@
Overview
========
-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 setup. After that, you can proceed to ask it all sorts of things
-about the terminal, such as its size and color support, and use its styling
-to construct strings containing color and styling. Also, 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.
+Blessings 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()
-Simple Formatting
------------------
+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:
-Lots of handy formatting codes are available as attributes on a *Terminal* class
-instance. For example::
+ >>> term.green_reverse('ALL SYSTEMS GO')
+ u'\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 blessings 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*.
+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 *Terminal* instance as an argument for ``.format`` string
-method, so that capabilities can be displayed in-line for more complex strings::
+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
-------------
+~~~~~~~~~~~~
The basic capabilities supported by most terminals are:
@@ -68,7 +87,7 @@ The less commonly supported capabilities:
``no_shadow``
Exit shadow text mode.
``standout``
- Enable standout mode (often, an alias for ``reverse``.).
+ Enable standout mode (often, an alias for ``reverse``).
``no_standout``
Exit standout mode.
``subscript``
@@ -88,13 +107,13 @@ 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 to a *Terminal*
-instance. If it is not a supported capability, or a non-tty is used as an
-output stream, an empty string is returned.
+manual, under column **Cap-name**, may be used as an attribute of a
+:class:`~.Terminal` instance. If it is not a supported capability, or a non-tty
+is used as an output stream, an empty string is returned.
Colors
-------
+~~~~~~
Color terminals are capable of at least 8 basic colors.
@@ -110,8 +129,8 @@ Color terminals are capable of at least 8 basic colors.
The same colors, prefixed with *bright_* (synonymous with *bold_*),
such as *bright_blue*, provides 16 colors in total.
-The same colors, prefixed with *on_* sets the background color, some
-terminals also provide an additional 8 high-intensity versions using
+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 blessings import Terminal
@@ -119,50 +138,53 @@ terminals also provide an additional 8 high-intensity versions using
term = Terminal()
print(term.on_bright_blue('Blue skies!'))
+
print(term.bright_red_on_bright_yellow('Pepperoni Pizza!'))
-There is also a numerical interface to colors, which takes an integer from
-0-15.::
+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 blessings import Terminal
term = Terminal()
- for n in range(16):
- print(term.color(n)('Color {}'.format(n)))
+ 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.
-If the terminal defined by the **TERM** environment variable does not support
-colors, these simply return empty strings, or the string passed as an argument
-when used as a callable, without any video attributes. If the **TERM** defines
-a terminal that does support colors, but actually does not, they are usually
-harmless.
+Colorless Terminals
+~~~~~~~~~~~~~~~~~~~
-Colorless terminals, such as the amber or green monochrome *vt220*, do not
+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, such as a selection bar, to simply select
-a foreground color, followed by reverse video to achieve the desired
-background color effect::
+desirable in some applications to simply select a foreground color, followed
+by reverse video to achieve the desired background color effect::
from blessings import Terminal
term = Terminal()
- print('some terminals {standout} more than others'.format(
- standout=term.green_reverse('standout')))
-
-Which appears as *bright white on green* on color terminals, or *black text
-on amber or green* on monochrome terminals.
+ print(term.green_reverse('some terminals standout more than others'))
-You can check whether the terminal definition used supports colors, and how
-many, using the ``number_of_colors`` property, which returns any of *0*,
-*8* or *256* for terminal types such as *vt220*, *ansi*, and
-*xterm-256color*, respectively.
+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, unlink *black*, *bright_black* is not
-invisible -- it is actually a very dark shade of gray!
+.. note:: On most color terminals, *bright_black* is not invisible -- it is
+ actually a very dark shade of gray!
Compound Formatting
--------------------
+~~~~~~~~~~~~~~~~~~~
If you want to do lots of crazy formatting all at once, you can just mash it
all together::
@@ -174,28 +196,30 @@ all together::
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::
+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::
#!/usr/bin/env python
import argparse
+ from blessings import Terminal
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='+')
- from blessings import Terminal
-
term = Terminal()
+
args = parser.parse_args()
style = getattr(term, args.style)
print(style(' '.join(args.text)))
-Saved as **tprint.py**, this could be called simply::
+Saved as **tprint.py**, this could be used like::
$ ./tprint.py bright_blue_reverse Blue Skies
@@ -203,29 +227,34 @@ Saved as **tprint.py**, this could be called simply::
Moving The Cursor
-----------------
-When you want to move the cursor, you have a few choices, the
-``location(x=None, y=None)`` context manager, ``move(y, x)``, ``move_y(row)``,
-and ``move_x(col)`` attributes.
+When you want to move the cursor, you have a few choices:
+
+- ``location(x=None, y=None)`` context manager.
+- ``move(row, col)`` capability.
+- ``move_y(row)`` capability.
+- ``move_x(col)`` capability.
-**NOTE**: The ``location()`` method receives arguments in form of *(x, y)*,
-whereas the ``move()`` argument receives arguments in form of *(y, x)*. This
-is a flaw in the original `erikrose/blessings`_ implementation, but remains
-for compatibility.
+.. note:: The :meth:`~.Terminal.location` method receives arguments in form
+ of *(x, y)*, whereas the ``move()`` capability receives arguments in form
+ of *(y, x)*. This will be changed to match in the 2.0 release, :ghissue:`58`.
Moving Temporarily
~~~~~~~~~~~~~~~~~~
-A context manager, ``location()`` is provided to move the cursor to an
-*(x, y)* screen position and restore the previous position upon exit::
+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 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 **optional** *x* and/or *y*::
+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.')
@@ -236,8 +265,7 @@ When omitted, it saves the cursor position and restore it upon exit::
print(term.move(1, 1) + 'Hi')
print(term.move(9, 9) + 'Mom')
-**NOTE**: calls to ``location()`` may not be nested, as only one location
-may be saved.
+.. note:: calls to :meth:`~.Terminal.location` may not be nested.
Moving Permanently
@@ -269,14 +297,17 @@ cursor one character in various directions:
* ``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.
+.. 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 *height* and *width* properties of the *Terminal* class instance::
+Use the :attr:`~.Terminal.height` and :attr:`~.Terminal.width` properties to
+determine the size of the window::
from blessings import Terminal
@@ -285,7 +316,8 @@ Use the *height* and *width* properties of the *Terminal* class instance::
with term.location(x=term.width / 3, y=term.height / 3):
print('1/3 ways in!')
-These are always current, so they may be used with a callback from SIGWINCH_ signals.::
+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 blessings import Terminal
@@ -297,7 +329,8 @@ These are always current, so they may be used with a callback from SIGWINCH_ sig
signal.signal(signal.SIGWINCH, on_resize)
- term.inkey()
+ # wait for keypress
+ term.keystroke()
Clearing The Screen
@@ -336,7 +369,7 @@ There's also a context manager you can use as a shortcut::
with term.fullscreen():
print(term.move_y(term.height // 2) +
term.center('press any key').rstrip())
- term.inkey()
+ term.keystroke()
Pipe Savvy
@@ -344,13 +377,13 @@ 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
-*Terminal* will return empty strings. You'll get a nice-looking file without
-any formatting codes gumming up the works.
+: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 ``force_styling=True`` to the *Terminal* constructor.
+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 *does_styling* attribute on *Terminal* that lets
+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::
@@ -361,34 +394,38 @@ bars and other frippery and just stick to content::
if term.does_styling:
with term.location(x=0, y=term.height - 1):
print('Progress: [=======> ]')
- print(term.bold('Important stuff'))
+ print(term.bold("60%"))
Sequence Awareness
------------------
Blessings may measure the printable width of strings containing sequences,
-providing ``.center``, ``.ljust``, and ``.rjust`` methods, using the
-terminal screen's width as the default *width* value::
+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 blessings import Terminal
term = Terminal()
- with term.location(y=term.height / 2):
- print (term.center(term.bold('X'))
+ 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.
-Any string containing sequences may have its printable length measured using the
-``.length`` method. Additionally, ``textwrap.wrap()`` is supplied on the Terminal
-class as method ``.wrap`` method that is also sequence-aware, so now you may
-word-wrap strings containing sequences. The following example displays a poem
-from Tao Te Ching, word-wrapped to 25 columns::
+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 blessings import Terminal
term = Terminal()
- poem = (term.bold_blue('Plan difficult tasks'),
- term.blue('through the simplest tasks'),
+ 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'))
@@ -399,64 +436,77 @@ from Tao Te Ching, word-wrapped to 25 columns::
Keyboard Input
--------------
-The built-in python function ``raw_input`` function does not return a value until
+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 that emit multibyte sequences.
+keypress, much less arrow or function keys.
-Special `termios(4)`_ routines are required to enter Non-canonical mode, known
-in curses as `cbreak(3)`_. When calling read on input stream, only bytes are
-received, which must be decoded to unicode.
+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``: blessings handles all of these special cases for you!
-Blessings handles all of these special cases!!
+keystroke_input
+~~~~~~~~~~~~~~~
-cbreak
-~~~~~~
-
-The context manager ``cbreak`` can be used to enter *key-at-a-time* mode: Any
-keypress by the user is immediately consumed by read calls::
+The context manager :meth:`~.Terminal.keystroke_input` can be used to enter
+*key-at-a-time* mode: Any keypress by the user is immediately consumed by read
+calls::
from blessings import Terminal
import sys
- t = Terminal()
+ term = Terminal()
- with t.cbreak():
- # blocks until any key is pressed.
+ with term.keystroke_input():
+ # block until any single key is pressed.
sys.stdin.read(1)
-raw
-~~~
+The mode entered using :meth:`~.Terminal.keystroke_input` is called
+`cbreak(3)`_ in curses:
-The context manager ``raw`` is the same as ``cbreak``, except interrupt (^C),
-quit (^\\), suspend (^Z), and flow control (^S, ^Q) characters are not trapped,
-but instead sent directly as their natural character. This is necessary if you
-actually want to handle the receipt of Ctrl+C
+ 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.
-inkey
-~~~~~
+:meth:`~.Terminal.keystroke_input` also accepts optional parameter
+:paramref:`~.Terminal.keystroke_input.raw` which may be set as *True*. When
+used, the given behavior is described in `raw(3)`_ as follows:
-The method ``inkey`` resolves many issues with terminal input by returning
-a unicode-derived *Keypress* instance. Although its return value may be
-printed, joined with, or compared to other unicode strings, it also provides
-the special attributes ``is_sequence`` (bool), ``code`` (int),
-and ``name`` (str)::
+ The raw and noraw routines place the terminal into or out of raw mode.
+ Raw mode is similar to cbreak mode, in that characters typed are immediately
+ passed through to the user program. The differences are that in raw mode,
+ the interrupt, quit, suspend, and flow control characters are all passed
+ through uninterpreted, instead of generating a signal.
+
+keystroke
+~~~~~~~~~
+
+The method :meth:`~.Terminal.keystroke` combined with `keystroke_input`_
+completes the circle of providing key-at-a-time keyboard input with multibyte
+encoding and awareness of application keys.
+
+:meth:`~.Terminal.keystroke` 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 blessings import Terminal
- t = Terminal()
+ term = Terminal()
print("press 'q' to quit.")
- with t.cbreak():
- val = None
+ with term.keystroke_input():
+ val = u''
while val not in (u'q', u'Q',):
- val = t.inkey(timeout=5)
+ val = term.keystroke(timeout=5)
if not val:
# timeout
print("It sure is quiet in here ...")
elif val.is_sequence:
- print("got sequence: {}.".format((str(val), val.name, val.code)))
+ print("got sequence: {0}.".format((str(val), val.name, val.code)))
elif val:
- print("got {}.".format(val))
+ print("got {0}.".format(val))
print('bye!')
Its output might appear as::
@@ -473,45 +523,51 @@ Its output might appear as::
got q.
bye!
-A ``timeout`` value of *None* (default) will block forever. Any other value
-specifies the length of time to poll for input, if no input is received after
-such time has elapsed, an empty string is returned. A ``timeout`` value of *0*
-is non-blocking.
+A :paramref:`~.Terminal.keystroke.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.keystroke.timeout` value of *0* is non-blocking.
keyboard codes
~~~~~~~~~~~~~~
-The return value of the *Terminal* method ``inkey`` is an instance of the
-class ``Keystroke``, and may be inspected for its property ``is_sequence``
-(bool). When *True*, the value is a **multibyte sequence**, representing
-a special non-alphanumeric key of your keyboard.
-
-The ``code`` property (int) may then be compared with attributes of
-*Terminal*, which are duplicated from those seen in the manpage
-`curs_getch(3)`_ or the curses_ module, with the following helpful
-aliases:
-
-* use ``KEY_DELETE`` for ``KEY_DC`` (chr(127)).
-* use ``KEY_TAB`` for chr(9).
-* use ``KEY_INSERT`` for ``KEY_IC``.
-* use ``KEY_PGUP`` for ``KEY_PPAGE``.
-* use ``KEY_PGDOWN`` for ``KEY_NPAGE``.
-* use ``KEY_ESCAPE`` for ``KEY_EXIT``.
-* use ``KEY_SUP`` for ``KEY_SR`` (shift + up).
-* use ``KEY_SDOWN`` for ``KEY_SF`` (shift + down).
-* use ``KEY_DOWN_LEFT`` for ``KEY_C1`` (keypad lower-left).
-* use ``KEY_UP_RIGHT`` for ``KEY_A1`` (keypad upper-left).
-* use ``KEY_DOWN_RIGHT`` for ``KEY_C3`` (keypad lower-left).
-* use ``KEY_UP_RIGHT`` for ``KEY_A3`` (keypad lower-right).
-* use ``KEY_CENTER`` for ``KEY_B2`` (keypad center).
-* use ``KEY_BEGIN`` for ``KEY_BEG``.
-
-The *name* property of the return value of ``inkey()`` will prefer
-these aliases over the built-in curses_ names.
-
-The following are **not** available in the curses_ module, but
-provided for keypad support, especially where the ``keypad()``
-context manager is used:
+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 <https://docs.python.org/3/library/curses.html#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:
+
+=================== ============= ====================
+blessings 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``
@@ -521,14 +577,13 @@ context manager is used:
* ``KEY_KP_DIVIDE``
* ``KEY_KP_0`` through ``KEY_KP_9``
-.. _`erikrose/blessings`: https://github.com/erikrose/blessings
-.. _curses: https://docs.python.org/library/curses.html
.. _couleur: https://pypi.python.org/pypi/couleur
.. _wcwidth: https://pypi.python.org/pypi/wcwidth
.. _`cbreak(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak&apropos=0&sektion=3
+.. _`raw(3)`: http://www.openbsd.org/cgi-bin/man.cgi?query=raw&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
+.. _`terminfo(5)`: http://invisible-island.net/ncurses/man/terminfo.5.html
.. _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
diff --git a/fabfile.py b/fabfile.py
deleted file mode 100644
index 27b418e..0000000
--- a/fabfile.py
+++ /dev/null
@@ -1,39 +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/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2a9acf1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index 31dc642..faecaa3 100755
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python
+"""setuptools entry point for package management."""
import subprocess
import sys
import os
@@ -7,76 +8,90 @@ import setuptools
import setuptools.command.develop
import setuptools.command.test
-here = os.path.dirname(__file__)
+HERE = os.path.dirname(__file__)
class SetupDevelop(setuptools.command.develop.develop):
+
+ """Docstring is overwritten."""
+
def run(self):
- # ensure a virtualenv is loaded,
+ """
+ Prepare environment for development.
+
+ - Ensures a virtualenv environmnt is used.
+ - Ensures tox, ipython, wheel is installed for convenience and testing.
+ - Call super()'s run method.
+ """
assert os.getenv('VIRTUAL_ENV'), 'You should be in a virtualenv'
- # ensure tox and ipython is installed
- subprocess.check_call(('pip', 'install', 'tox', 'ipython'))
+ subprocess.check_call(('pip', 'install', 'tox', 'ipython', 'wheel'))
+
# Call super() (except develop is an old-style class, so we must call
# directly). The effect is that the development egg-link is installed.
setuptools.command.develop.develop.run(self)
+SetupDevelop.__doc__ = setuptools.command.develop.develop.__doc__
+
class SetupTest(setuptools.command.test.test):
+
+ """Docstring is overwritten."""
+
def run(self):
+ """Spawn tox."""
self.spawn(('tox',))
+SetupTest.__doc__ = setuptools.command.test.test.__doc__
+
+EXTRA = {
+ 'install_requires': [
+ 'wcwidth>=0.1.4',
+ 'six>=1.9.0',
+ ]
+}
+
+if sys.version_info < (2, 7):
+ # we make use of collections.ordereddict: for python 2.6 we require the
+ # assistance of the 'orderddict' module which backports the same.
+ EXTRA['install_requires'].extend(['ordereddict>=1.1'])
-def main():
- extra = {
- 'install_requires': [
- 'wcwidth>=0.1.4',
- 'six>=1.9.0',
- ]
- }
- if sys.version_info < (2, 7):
- # we make use of collections.ordereddict: for python 2.6 we require the
- # assistance of the 'orderddict' module which backports the same.
- extra['install_requires'].extend(['ordereddict>=1.1'])
-
- setuptools.setup(
- name='blessings',
- version='1.9.5',
- description=('A thin, practical wrapper around terminal coloring, '
- 'styling, positioning, and keyboard input.'),
- long_description=open(os.path.join(here, 'docs/intro.rst')).read(),
- author='Erik Rose, Jeff Quast',
- author_email='erikrose@grinchcentral.com',
- license='MIT',
- packages=['blessings', 'blessings.tests'],
- url='https://github.com/erikrose/blessings',
- include_package_data=True,
- test_suite='blessings.tests',
- classifiers=[
- 'Intended Audience :: Developers',
- 'Natural Language :: English',
- 'Development Status :: 5 - Production/Stable',
- 'Environment :: Console',
- 'Environment :: Console :: Curses',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: POSIX',
- 'Programming Language :: Python :: 2',
- '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',
- 'Topic :: Software Development :: Libraries',
- 'Topic :: Software Development :: User Interfaces',
- 'Topic :: Terminals'
- ],
- keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses',
- 'formatting', 'style', 'color', 'console', 'keyboard',
- 'ansi', 'xterm'],
- cmdclass={'develop': SetupDevelop,
- 'test': SetupTest},
- **extra
- )
-
-if __name__ == '__main__':
- main()
+setuptools.setup(
+ name='blessings',
+ version='1.9.5',
+ description=('A thin, practical wrapper around terminal coloring, '
+ 'styling, positioning, and keyboard input.'),
+ long_description=open(os.path.join(HERE, 'docs/intro.rst')).read(),
+ author='Erik Rose, Jeff Quast',
+ author_email='erikrose@grinchcentral.com',
+ license='MIT',
+ packages=['blessings', 'blessings.tests'],
+ url='https://github.com/erikrose/blessings',
+ include_package_data=True,
+ test_suite='blessings.tests',
+ zip_safe=True,
+ classifiers=[
+ 'Intended Audience :: Developers',
+ 'Natural Language :: English',
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Console',
+ 'Environment :: Console :: Curses',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: POSIX',
+ 'Programming Language :: Python :: 2',
+ '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',
+ 'Topic :: Software Development :: Libraries',
+ 'Topic :: Software Development :: User Interfaces',
+ 'Topic :: Terminals'
+ ],
+ keywords=['terminal', 'sequences', 'tty', 'curses', 'ncurses',
+ 'formatting', 'style', 'color', 'console', 'keyboard',
+ 'ansi', 'xterm'],
+ cmdclass={'develop': SetupDevelop,
+ 'test': SetupTest},
+ **EXTRA
+)
diff --git a/tools/teamcity-runtests.sh b/tools/teamcity-runtests.sh
index 3b3faec..c43766a 100755
--- a/tools/teamcity-runtests.sh
+++ b/tools/teamcity-runtests.sh
@@ -14,7 +14,7 @@ if [ X"$osrel" == X"Linux" ]; then
# cannot create a virtualenv for python2.6 due to use of
# "{}".format in virtualenv, throws exception
# ValueError: zero length field name in format.
- _cmd='tox -epy27,py33,py34,pypy'
+ _cmd='tox -epy27,py33,py34,pypy,docs,sa'
fi
ret=0
diff --git a/tox.ini b/tox.ini
index 40b9b6f..c0b02f6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,7 +10,7 @@ envlist = sa,
skip_missing_interpreters = true
[testenv]
-whitelist_externals = /bin/bash /bin/mv
+whitelist_externals = /bin/mv
setenv = PYTHONIOENCODING=UTF8
deps = pytest-xdist
pytest-cov
@@ -20,23 +20,46 @@ commands = {envbindir}/py.test \
--strict --junit-xml=results.{envname}.xml \
--verbose --verbose \
--cov blessings --cov-report=term-missing \
+ blessings/tests \
{posargs}
/bin/mv {toxinidir}/.coverage {toxinidir}/.coverage.{envname}
[testenv:sa]
-# static analysis
-deps = prospector[with_everything]
-commands = -prospector \
+# Instead of trusting whichever developer's environment python is used to
+# invoke tox, explicitly define python2.7 because the 'doc8' tool does not
+# appear to be python3-compatible:
+# https://github.com/stackforge/doc8/commit/4d82c269ab46f0c5370c1f00be06e0c406164e85#commitcomment-10725927
+basepython=python2.7
+deps = prospector[with_frosted,with_pyroma]
+ restructuredtext_lint
+ doc8
+
+# - prospector is configured using .prospector.yaml, and wraps several
+# static analysis/linting and style-checker tools.
+# - rst-lint ensures that README.rst will present correctly on pypi.
+# - doc8 is like pep8 for rst documents. Namely, enforcing styling.
+# ignore docs/further.rst:21: D000 Bullet list ends without a blank line;
+# unexpected unindent. This is a tool error
+commands = prospector \
--die-on-tool-error \
--doc-warnings \
{toxinidir}
+ rst-lint README.rst
+
+ doc8 --ignore-path docs/_build --ignore D000 docs
+
[testenv:docs]
+whitelist_externals=echo
basepython=python
deps=sphinx
sphinx_rtd_theme
-commands=
- sphinx-build -v -W -b html -d {toxinidir}/docs/_build/doctrees docs {toxinidir}/docs/_build/html
+ sphinx-paramlinks
+
+commands = sphinx-build -v -W -b html -d {toxinidir}/docs/_build/doctrees \
+ docs \
+ {toxinidir}/docs/_build/html
+ echo "open {toxinidir}/docs/_build/html/index.html for review"
[pytest]
# py.test fixtures conflict with pyflakes