summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Quast <jquast@io.com>2015-01-04 14:02:48 -0800
committerJeff Quast <jquast@io.com>2015-01-04 14:02:48 -0800
commit1965724fb28cd08522e2443bf5960ec82dda5865 (patch)
treec02e12f2258e7868015110a5cff70add700ba96c
parent22c1eea0eaa2f2ab1a8114c6bc9fec84777612c5 (diff)
downloadblessings-1965724fb28cd08522e2443bf5960ec82dda5865.tar.gz
fix the rename thing
-rw-r--r--blessed/terminal.py931
-rw-r--r--blessed/terminal.py.bak785
2 files changed, 578 insertions, 1138 deletions
diff --git a/blessed/terminal.py b/blessed/terminal.py
index a2254b1..1e17d73 100644
--- a/blessed/terminal.py
+++ b/blessed/terminal.py
@@ -1,42 +1,72 @@
-"""A thin, practical wrapper around terminal coloring, styling, and
-positioning"""
-
-from contextlib import contextmanager
+"This primary module provides the Terminal class."
+# standard modules
+import collections
+import contextlib
+import functools
+import warnings
+import platform
+import codecs
import curses
-from curses import setupterm, tigetnum, tigetstr, tparm
-from fcntl import ioctl
+import locale
+import select
+import struct
+import time
+import sys
+import os
+
+try:
+ import termios
+ import fcntl
+ import tty
+except ImportError:
+ tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width')
+ msg_nosupport = (
+ "One or more of the modules: 'termios', 'fcntl', and 'tty' "
+ "are not found on your platform '{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)
+ 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"""
-
-from os import isatty, environ
-from platform import python_version_tuple
-import struct
-import sys
-from termios import TIOCGWINSZ
-
+ ``io.UnsupportedOperation`` in Python 2.5"""
-__all__ = ['Terminal']
-
-
-if ('3', '0', '0') <= python_version_tuple() < ('3', '2', '2+'): # Good till
- # 3.2.10
- # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string.
- raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 '
- 'support due to http://bugs.python.org/issue10570.')
+try:
+ _ = InterruptedError
+ del _
+except NameError:
+ # alias py2 exception to py3
+ InterruptedError = select.error
+
+# local imports
+from .formatters import (
+ ParameterizingString,
+ NullCallableString,
+ resolve_capability,
+ resolve_attribute,
+)
+
+from .sequences import (
+ init_sequence_patterns,
+ SequenceTextWrapper,
+ Sequence,
+)
+
+from .keyboard import (
+ get_keyboard_sequences,
+ get_keyboard_codes,
+ resolve_sequence,
+)
class Terminal(object):
- """An abstraction around terminal capabilities
-
- Unlike curses, this doesn't require clearing the screen before doing
- anything, and it's friendlier to use. It keeps the endless calls to
- ``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently
- when somebody pipes your output to a non-terminal.
+ """A wrapper for curses and related terminfo(5) terminal capabilities.
Instance attributes:
@@ -45,8 +75,44 @@ class Terminal(object):
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.
-
"""
+
+ #: Sugary names for commonly-used capabilities
+ _sugar = dict(
+ save='sc',
+ restore='rc',
+ # 'clear' clears the whole screen.
+ clear_eol='el',
+ clear_bol='el1',
+ clear_eos='ed',
+ position='cup', # deprecated
+ enter_fullscreen='smcup',
+ exit_fullscreen='rmcup',
+ move='cup',
+ move_x='hpa',
+ move_y='vpa',
+ move_left='cub1',
+ move_right='cuf1',
+ move_up='cuu1',
+ move_down='cud1',
+ hide_cursor='civis',
+ normal_cursor='cnorm',
+ reset_colors='op', # oc doesn't work on my OS X terminal.
+ normal='sgr0',
+ reverse='rev',
+ italic='sitm',
+ no_italic='ritm',
+ shadow='sshm',
+ no_shadow='rshm',
+ standout='smso',
+ no_standout='rmso',
+ subscript='ssubm',
+ no_subscript='rsubm',
+ superscript='ssupm',
+ no_superscript='rsupm',
+ underline='smul',
+ no_underline='rmul')
+
def __init__(self, kind=None, stream=None, force_styling=False):
"""Initialize the terminal.
@@ -75,172 +141,186 @@ class Terminal(object):
``force_styling=None``.
"""
- if stream is None:
+ 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
+ if stream is None or stream == sys.__stdout__:
stream = sys.__stdout__
+ self.keyboard_fd = sys.__stdin__.fileno()
+
try:
- stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno')
- and callable(stream.fileno)
- else None)
+ stream_fd = (stream.fileno() if hasattr(stream, 'fileno')
+ and callable(stream.fileno) else None)
except IOUnsupportedOperation:
- stream_descriptor = None
+ stream_fd = None
- self._is_a_tty = (stream_descriptor is not None and
- isatty(stream_descriptor))
+ self._is_a_tty = stream_fd is not None and os.isatty(stream_fd)
self._does_styling = ((self.is_a_tty or force_styling) and
force_styling is not None)
+ # keyboard_fd only non-None if both stdin and stdout is a tty.
+ self.keyboard_fd = (self.keyboard_fd
+ if self.keyboard_fd is not None and
+ self.is_a_tty and os.isatty(self.keyboard_fd)
+ else None)
+ self._normal = None # cache normal attr, preventing recursive lookups
+
# The descriptor to direct terminal initialization sequences to.
# sys.__stdout__ seems to always have a descriptor of 1, even if output
# is redirected.
- self._init_descriptor = (sys.__stdout__.fileno()
- if stream_descriptor is None
- else stream_descriptor)
+ 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:
# Make things like tigetstr() work. Explicit args make setupterm()
# work even when -s is passed to nosetests. Lean toward sending
# init sequences to the stream if it has a file descriptor, and
# send them to stdout as a fallback, since they have to go
# somewhere.
- setupterm(kind or environ.get('TERM', 'unknown'),
- self._init_descriptor)
+ try:
+ if (platform.python_implementation() == 'PyPy' and
+ 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)
+ else:
+ curses.setupterm(self._kind, self._init_descriptor)
+ except curses.error as err:
+ warnings.warn('Failed to setupterm(kind={0!r}): {1}'
+ .format(self._kind, err))
+ self._kind = None
+ self._does_styling = False
+ else:
+ if _CUR_TERM is None or self._kind == _CUR_TERM:
+ _CUR_TERM = self._kind
+ else:
+ warnings.warn(
+ 'A terminal of kind "%s" has been requested; due to an'
+ ' internal python curses bug, terminal capabilities'
+ ' for a terminal of kind "%s" will continue to be'
+ ' returned for the remainder of this process.' % (
+ self._kind, _CUR_TERM,))
+
+ for re_name, re_val in init_sequence_patterns(self).items():
+ setattr(self, re_name, re_val)
+
+ # build database of int code <=> KEY_NAME
+ self._keycodes = get_keyboard_codes()
+
+ # store attributes as: self.KEY_NAME = code
+ for key_code, key_name in self._keycodes.items():
+ setattr(self, key_name, key_code)
+
+ # build database of sequence <=> KEY_NAME
+ self._keymap = get_keyboard_sequences(self)
+
+ self._keyboard_buf = collections.deque()
+ if self.keyboard_fd is not None:
+ locale.setlocale(locale.LC_ALL, '')
+ self._encoding = locale.getpreferredencoding() or 'ascii'
+ try:
+ self._keyboard_decoder = codecs.getincrementaldecoder(
+ self._encoding)()
+ except LookupError as err:
+ warnings.warn('%s, fallback to ASCII for keyboard.' % (err,))
+ self._encoding = 'ascii'
+ self._keyboard_decoder = codecs.getincrementaldecoder(
+ self._encoding)()
self.stream = stream
- # Sugary names for commonly-used capabilities, intended to help avoid trips
- # to the terminfo man page and comments in your code:
- _sugar = dict(
- # Don't use "on" or "bright" as an underscore-separated chunk in any of
- # these (e.g. on_cology or rock_on) so we don't interfere with
- # __getattr__.
- save='sc',
- restore='rc',
-
- clear_eol='el',
- clear_bol='el1',
- clear_eos='ed',
- # 'clear' clears the whole screen.
- position='cup', # deprecated
- enter_fullscreen='smcup',
- exit_fullscreen='rmcup',
- move='cup',
- move_x='hpa',
- move_y='vpa',
- move_left='cub1',
- move_right='cuf1',
- move_up='cuu1',
- move_down='cud1',
-
- hide_cursor='civis',
- normal_cursor='cnorm',
-
- reset_colors='op', # oc doesn't work on my OS X terminal.
-
- normal='sgr0',
- reverse='rev',
- # 'bold' is just 'bold'. Similarly...
- # blink
- # dim
- # flash
- italic='sitm',
- no_italic='ritm',
- shadow='sshm',
- no_shadow='rshm',
- standout='smso',
- no_standout='rmso',
- subscript='ssubm',
- no_subscript='rsubm',
- superscript='ssupm',
- no_superscript='rsupm',
- underline='smul',
- no_underline='rmul')
-
def __getattr__(self, attr):
- """Return a terminal capability, like bold.
-
- For example, you can say ``term.bold`` to get the string that turns on
- bold formatting and ``term.normal`` to get the string that turns it off
- again. Or you can take a shortcut: ``term.bold('hi')`` bolds its
- argument and sets everything to normal afterward. You can even combine
- things: ``term.bold_underline_red_on_bright_green('yowzers!')``.
+ """Return a terminal capability as Unicode string.
- For a parametrized capability like ``cup``, pass the parameters too:
- ``some_term.cup(line, column)``.
+ 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``.
- ``man terminfo`` for a complete list of capabilities.
+ This capability is also callable, so you can use ``term.bold("hi")``
+ which results in the joining of (term.bold, "hi", term.normal).
- Return values are always Unicode.
+ 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.
"""
- resolution = (self._resolve_formatter(attr) if self.does_styling
- else NullCallableString())
- setattr(self, attr, resolution) # Cache capability codes.
- return resolution
+ if not self.does_styling:
+ return NullCallableString()
+ val = resolve_attribute(self, attr)
+ # Cache capability codes.
+ setattr(self, attr, val)
+ return val
@property
- def does_styling(self):
- """Whether attempt to emit capabilities
-
- This is influenced by the ``is_a_tty`` property and by the
- ``force_styling`` argument to the constructor. You can examine
- this value to decide whether to draw progress bars or other frippery.
+ def kind(self):
+ """Name of this terminal type as string."""
+ return self._kind
- """
+ @property
+ def does_styling(self):
+ """Whether this instance will emit terminal sequences (bool)."""
return self._does_styling
@property
def is_a_tty(self):
- """Whether my ``stream`` appears to be associated with a terminal"""
+ """Whether the ``stream`` associated with this instance is a terminal
+ (bool)."""
return self._is_a_tty
@property
def height(self):
- """The height of the terminal in characters
-
- If no stream or a stream not representing a terminal was passed in at
- construction, return the dimension of the controlling terminal so
- piping to things that eventually display on the terminal (like ``less
- -R``) work. If a stream representing a terminal was passed in, return
- the dimensions of that terminal. If there somehow is no controlling
- terminal, return ``None``. (Thus, you should check that the property
- ``is_a_tty`` is true before doing any math on the result.)
+ """T.height -> int
+ The height of the terminal in characters.
"""
- return self._height_and_width()[0]
+ return self._height_and_width().ws_row
@property
def width(self):
- """The width of the terminal in characters
+ """T.width -> int
+
+ The width of the terminal in characters.
+ """
+ return self._height_and_width().ws_col
+
+ @staticmethod
+ def _winsize(fd):
+ """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel)
- See ``height()`` for some corner cases.
+ The tty connected by file desriptor fd is queried for its window size,
+ and returned as a collections.namedtuple instance WINSZ.
+ May raise exception IOError.
"""
- return self._height_and_width()[1]
+ if HAS_TTY:
+ data = fcntl.ioctl(fd, 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).
-
- Start by trying TIOCGWINSZ (Terminal I/O-Control: Get Window Size),
- falling back to environment variables (LINES, COLUMNS), and returning
- (None, None) if those are unavailable or invalid.
-
"""
- # tigetnum('lines') and tigetnum('cols') update only if we call
- # setupterm() again.
- for descriptor in self._init_descriptor, sys.__stdout__:
+ # 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__):
try:
- return struct.unpack(
- 'hhhh', ioctl(descriptor, TIOCGWINSZ, '\000' * 8))[0:2]
+ if fd is not None:
+ return self._winsize(fd)
except IOError:
- # when the output stream or init descriptor is not a tty, such
- # as when when stdout is piped to another program, fe. tee(1),
- # these ioctls will raise IOError
pass
- try:
- return int(environ.get('LINES')), int(environ.get('COLUMNS'))
- except TypeError:
- return None, None
- @contextmanager
+ return WINSZ(ws_row=int(os.getenv('LINES', '25')),
+ ws_col=int(os.getenv('COLUMNS', '80')),
+ ws_xpixel=None,
+ ws_ypixel=None)
+
+ @contextlib.contextmanager
def location(self, x=None, y=None):
"""Return a context manager for temporarily moving the cursor.
@@ -274,20 +354,29 @@ class Terminal(object):
# Restore original cursor position:
self.stream.write(self.restore)
- @contextmanager
+ @contextlib.contextmanager
def fullscreen(self):
"""Return a context manager that enters fullscreen mode while inside it
- and restores normal mode on leaving."""
+ and restores normal mode on leaving.
+
+ 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 call may not be tested; only one screen state may be saved at a
+ time.
+ """
self.stream.write(self.enter_fullscreen)
try:
yield
finally:
self.stream.write(self.exit_fullscreen)
- @contextmanager
+ @contextlib.contextmanager
def hidden_cursor(self):
- """Return a context manager that hides the cursor while inside it and
- makes it visible on leaving."""
+ """Return a context manager that hides the cursor upon entering,
+ and makes it visible again upon exiting."""
self.stream.write(self.hide_cursor)
try:
yield
@@ -296,9 +385,9 @@ class Terminal(object):
@property
def color(self):
- """Return a capability that sets the foreground color.
+ """Returns capability that sets the foreground color.
- The capability is unparametrized until called and passed a number
+ 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.
@@ -306,95 +395,39 @@ class Terminal(object):
:arg num: The number, 0-15, of the color
"""
- return ParametrizingString(self._foreground_color, self.normal)
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._foreground_color,
+ self.normal, 'color')
@property
def on_color(self):
- """Return a capability that sets the background color.
-
- See ``color()``.
+ "Returns capability that sets the background color."
+ if not self.does_styling:
+ return NullCallableString()
+ return ParameterizingString(self._background_color,
+ self.normal, 'on_color')
- """
- return ParametrizingString(self._background_color, self.normal)
+ @property
+ def normal(self):
+ "Returns sequence that resets video attribute."
+ if self._normal:
+ return self._normal
+ self._normal = resolve_capability(self, 'normal')
+ return self._normal
@property
def number_of_colors(self):
"""Return the number of colors the terminal supports.
- Common values are 0, 8, 16, 88, and 256.
-
- Though the underlying capability returns -1 when there is no color
- support, we return 0. This lets you test more Pythonically::
+ Common values are 0, 8, 16, 88, and 256. Most commonly
+ this may be used to test color capabilities at all::
if term.number_of_colors:
- ...
-
- We also return 0 if the terminal won't tell us how many colors it
- supports, which I think is rare.
-
- """
- # This is actually the only remotely useful numeric capability. We
- # don't name it after the underlying capability, because we deviate
- # slightly from its behavior, and we might someday wish to give direct
- # access to it.
- colors = tigetnum('colors') # Returns -1 if no color support, -2 if no
- # such cap.
- # self.__dict__['colors'] = ret # Cache it. It's not changing.
- # (Doesn't work.)
- return colors if colors >= 0 else 0
-
- def _resolve_formatter(self, attr):
- """Resolve a sugary or plain capability name, color, or compound
- formatting function name into a callable capability.
-
- Return a ``ParametrizingString`` or a ``FormattingString``.
-
- """
- if attr in COLORS:
- return self._resolve_color(attr)
- elif attr in COMPOUNDABLES:
- # Bold, underline, or something that takes no parameters
- return self._formatting_string(self._resolve_capability(attr))
- else:
- formatters = split_into_formatters(attr)
- if all(f in COMPOUNDABLES for f in formatters):
- # It's a compound formatter, like "bold_green_on_red". Future
- # optimization: combine all formatting into a single escape
- # sequence.
- return self._formatting_string(
- u''.join(self._resolve_formatter(s) for s in formatters))
- else:
- return ParametrizingString(self._resolve_capability(attr))
-
- def _resolve_capability(self, atom):
- """Return a terminal code for a capname or a sugary name, or an empty
- Unicode.
-
- The return value is always Unicode, because otherwise it is clumsy
- (especially in Python 3) to concatenate with real (Unicode) strings.
-
- """
- code = tigetstr(self._sugar.get(atom, atom))
- if code:
- # See the comment in ParametrizingString for why this is latin1.
- return code.decode('latin1')
- return u''
-
- def _resolve_color(self, color):
- """Resolve a color like red or on_bright_green into a callable
- capability."""
- # TODO: Does curses automatically exchange red and blue and cyan and
- # yellow when a terminal supports setf/setb rather than setaf/setab?
- # I'll be blasted if I can find any documentation. The following
- # assumes it does.
- color_cap = (self._background_color if 'on_' in color else
- self._foreground_color)
- # curses constants go up to only 7, so add an offset to get at the
- # bright colors at 8-15:
- offset = 8 if 'bright_' in color else 0
- base_color = color.rsplit('_', 1)[-1]
- return self._formatting_string(
- color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset))
+ ..."""
+ # trim value to 0, as tigetnum('colors') returns -1 if no support,
+ # -2 if no such capability.
+ return max(0, self.does_styling and curses.tigetnum('colors') or -1)
@property
def _foreground_color(self):
@@ -404,157 +437,349 @@ class Terminal(object):
def _background_color(self):
return self.setab or self.setb
- def _formatting_string(self, formatting):
- """Return a new ``FormattingString`` which implicitly receives my
- notion of "normal"."""
- return FormattingString(formatting, self.normal)
+ def 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."""
+ 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."""
+ 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."""
+ if width is None:
+ width = self.width
+ return Sequence(text, self).center(width, fillchar)
+
+ def length(self, text):
+ """T.length(text) -> int
+
+ 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*..
+ """
+ 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.
+ """
+ return Sequence(text, self).strip(chars)
-def derivative_colors(colors):
- """Return the names of valid color variants, given the base colors."""
- return set([('on_' + c) for c in colors] +
- [('bright_' + c) for c in colors] +
- [('on_bright_' + c) for c in colors])
+ def rstrip(self, text, chars=None):
+ """T.rstrip(text) -> unicode
+ Return string ``text`` with terminal sequences and trailing whitespace
+ removed.
+ """
+ return Sequence(text, self).rstrip(chars)
-COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan',
- 'white'])
-COLORS.update(derivative_colors(COLORS))
-COMPOUNDABLES = (COLORS |
- set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic',
- 'shadow', 'standout', 'subscript', 'superscript']))
+ def lstrip(self, text, chars=None):
+ """T.lstrip(text) -> unicode
+
+ Return string ``text`` with terminal sequences and leading whitespace
+ removed.
+ """
+ return Sequence(text, self).lstrip(chars)
+ def strip_seqs(self, text):
+ """T.strip_seqs(text) -> unicode
-class ParametrizingString(unicode):
- """A Unicode string which can be called to parametrize it as a terminal
- capability"""
+ Return string ``text`` stripped only of its sequences.
+ """
+ return Sequence(text, self).strip_seqs()
- def __new__(cls, formatting, normal=None):
- """Instantiate.
+ def wrap(self, text, width=None, **kwargs):
+ """T.wrap(text, [width=None, **kwargs ..]) -> list[unicode]
- :arg normal: If non-None, indicates that, once parametrized, this can
- be used as a ``FormattingString``. The value is used as the
- "normal" capability.
+ 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``.
"""
- new = unicode.__new__(cls, formatting)
- new._normal = normal
- return new
+ width = self.width if width is None else width
+ lines = []
+ for line in text.splitlines():
+ lines.extend(
+ (_linewrap for _linewrap in SequenceTextWrapper(
+ width=width, term=self, **kwargs).wrap(text))
+ if line.strip() else (u'',))
- def __call__(self, *args):
- try:
- # Re-encode the cap, because tparm() takes a bytestring in Python
- # 3. However, appear to be a plain Unicode string otherwise so
- # concats work.
- #
- # We use *latin1* encoding so that bytes emitted by tparm are
- # encoded to their native value: some terminal kinds, such as
- # 'avatar' or 'kermit', emit 8-bit bytes in range 0x7f to 0xff.
- # latin1 leaves these values unmodified in their conversion to
- # unicode byte values. The terminal emulator will "catch" and
- # handle these values, even if emitting utf8-encoded text, where
- # these bytes would otherwise be illegal utf8 start bytes.
- parametrized = tparm(self.encode('latin1'), *args).decode('latin1')
- return (parametrized if self._normal is None else
- FormattingString(parametrized, self._normal))
- except curses.error:
- # Catch "must call (at least) setupterm() first" errors, as when
- # running simply `nosetests` (without progressive) on nose-
- # progressive. Perhaps the terminal has gone away between calling
- # tigetstr and calling tparm.
- return u''
- except TypeError:
- # If the first non-int (i.e. incorrect) arg was a string, suggest
- # something intelligent:
- if len(args) == 1 and isinstance(args[0], basestring):
- raise TypeError(
- 'A native or nonexistent capability template received '
- '%r when it was expecting ints. You probably misspelled a '
- 'formatting call like bright_red_on_white(...).' % args)
- else:
- # Somebody passed a non-string; I don't feel confident
- # guessing what they were trying to do.
- raise
+ return lines
+ def getch(self):
+ """T.getch() -> unicode
-class FormattingString(unicode):
- """A Unicode string which can be called upon a piece of text to wrap it in
- formatting"""
+ 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.kbhit() returns True.
+
+ Implementors of input streams other than os.read() on the stdin fd
+ should derive and 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 __new__(cls, formatting, normal):
- new = unicode.__new__(cls, formatting)
- new._normal = normal
- return new
+ def kbhit(self, timeout=None, _intr_continue=True):
+ """T.kbhit([timeout=None]) -> bool
- def __call__(self, text):
- """Return a new string that is ``text`` formatted with my contents.
+ Returns True if a keypress has been detected on keyboard.
- At the beginning of the string, I prepend the formatting that is my
- contents. At the end, I append the "normal" sequence to set everything
- back to defaults. The return value is always a Unicode.
+ 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.
"""
- return self + text + self._normal
+ # Special care is taken to handle a custom SIGWINCH handler, which
+ # causes select() to be interrupted with errno 4 (EAGAIN) --
+ # it is ignored, and a new timeout value is derived from the previous,
+ # unless timeout becomes negative, because signal handler has blocked
+ # 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, ]
+ 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)
+ except InterruptedError:
+ if not _intr_continue:
+ return u''
+ if timeout is not None:
+ # subtract time already elapsed,
+ timeout -= time.time() - stime
+ if timeout > 0:
+ continue
+ # no time remains after handling exception (rare)
+ ready_r = []
+ break
+ else:
+ break
+ return False if self.keyboard_fd is None else check_r == ready_r
-class NullCallableString(unicode):
- """A dummy callable Unicode to stand in for ``FormattingString`` and
- ``ParametrizingString``
+ @contextlib.contextmanager
+ def cbreak(self):
+ """Return a context manager that enters 'cbreak' mode: disabling line
+ buffering of keyboard input, making characters typed by the user
+ immediately available to the program. Also referred to as 'rare'
+ mode, this is the opposite of 'cooked' mode, the default for most
+ shells.
- We use this when there is no tty and thus all capabilities should be blank.
+ In 'cbreak' mode, echo of input is also disabled: the application must
+ explicitly print any input received, if they so wish.
- """
- def __new__(cls):
- new = unicode.__new__(cls, u'')
- return new
-
- def __call__(self, *args):
- """Return a Unicode or whatever you passed in as the first arg
- (hopefully a string of some kind).
-
- When called with an int as the first arg, return an empty Unicode. An
- int is a good hint that I am a ``ParametrizingString``, as there are
- only about half a dozen string-returning capabilities on OS X's
- terminfo man page which take any param that's not an int, and those are
- seldom if ever used on modern terminal emulators. (Most have to do with
- programming function keys. Blessings' story for supporting
- non-string-returning caps is undeveloped.) And any parametrized
- capability in a situation where all capabilities themselves are taken
- to be blank are, of course, themselves blank.
-
- When called with a non-int as the first arg (no no args at all), return
- the first arg. I am acting as a ``FormattingString``.
+ 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
"""
- if len(args) != 1 or isinstance(args[0], int):
- # I am acting as a ParametrizingString.
+ if HAS_TTY and self.keyboard_fd is not None:
+ # save current terminal mode,
+ save_mode = termios.tcgetattr(self.keyboard_fd)
+ tty.setcbreak(self.keyboard_fd, termios.TCSANOW)
+ try:
+ yield
+ finally:
+ # restore prior mode,
+ termios.tcsetattr(self.keyboard_fd,
+ termios.TCSAFLUSH,
+ save_mode)
+ else:
+ yield
- # tparm can take not only ints but also (at least) strings as its
- # second...nth args. But we don't support callably parametrizing
- # caps that take non-ints yet, so we can cheap out here. TODO: Go
- # through enough of the motions in the capability resolvers to
- # determine which of 2 special-purpose classes,
- # NullParametrizableString or NullFormattingString, to return, and
- # retire this one.
- return u''
- return args[0] # Should we force even strs in Python 2.x to be
- # unicodes? No. How would I know what encoding to use
- # to convert it?
+ @contextlib.contextmanager
+ def raw(self):
+ """Return a context manager that enters *raw* mode. Raw mode is
+ similar to *cbreak* mode, in that characters typed are immediately
+ available to ``inkey()`` with one exception: the interrupt, quit,
+ suspend, and flow control characters are all passed through as their
+ raw character values instead of generating a signal.
+ """
+ if HAS_TTY and self.keyboard_fd is not None:
+ # save current terminal mode,
+ save_mode = termios.tcgetattr(self.keyboard_fd)
+ tty.setraw(self.keyboard_fd, termios.TCSANOW)
+ try:
+ yield
+ finally:
+ # restore prior mode,
+ termios.tcsetattr(self.keyboard_fd,
+ termios.TCSAFLUSH,
+ save_mode)
+ else:
+ yield
+ @contextlib.contextmanager
+ def keypad(self):
+ """
+ Context manager that enables keypad input (*keyboard_transmit* mode).
-def split_into_formatters(compound):
- """Split a possibly compound format string into segments.
+ 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.
- >>> split_into_formatters('bold_underline_bright_blue_on_red')
- ['bold', 'underline', 'bright_blue', 'on_red']
+ On an IBM-PC keypad of ttype *xterm*, with numlock off, the
+ lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``.
- """
- merged_segs = []
- # These occur only as prefixes, so they can always be merged:
- mergeable_prefixes = ['on', 'bright', 'on_bright']
- for s in compound.split('_'):
- if merged_segs and merged_segs[-1] in mergeable_prefixes:
- merged_segs[-1] += '_' + s
- else:
- merged_segs.append(s)
- return merged_segs
+ However, upon entering keypad mode, ``\\x1b[OF`` is transmitted,
+ translating to ``KEY_LL`` (lower-left key), allowing diagonal
+ direction keys to be determined.
+ """
+ try:
+ self.stream.write(self.smkx)
+ yield
+ finally:
+ self.stream.write(self.rmkx)
+
+ def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True):
+ """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress()
+
+ 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 a value
+ of ``False`` for ``_intr_continue``.
+ """
+ # 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') ?
+ def _timeleft(stime, timeout):
+ """_timeleft(stime, timeout) -> float
+
+ Returns time-relative time remaining before ``timeout``
+ after time elapsed since ``stime``.
+ """
+ if timeout is not None:
+ if timeout is 0:
+ return 0
+ return max(0, timeout - (time.time() - stime))
+
+ resolve = functools.partial(resolve_sequence,
+ mapper=self._keymap,
+ codes=self._keycodes)
+
+ stime = time.time()
+
+ # re-buffer previously received keystrokes,
+ ucs = u''
+ while self._keyboard_buf:
+ ucs += self._keyboard_buf.pop()
+
+ # receive all immediately available bytes
+ while self.kbhit(0):
+ ucs += self.getch()
+
+ # decode keystroke, if any
+ ks = resolve(text=ucs)
+
+ # so long as the most immediately received or buffered keystroke is
+ # incomplete, (which may be a multibyte encoding), block until until
+ # one is received.
+ while not ks and self.kbhit(_timeleft(stime, timeout), _intr_continue):
+ ucs += self.getch()
+ ks = 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 is self.KEY_ESCAPE:
+ esctime = time.time()
+ while (ks.code is self.KEY_ESCAPE and
+ self.kbhit(_timeleft(esctime, esc_delay))):
+ ucs += self.getch()
+ ks = resolve(text=ucs)
+
+ # buffer any remaining text received
+ self._keyboard_buf.extendleft(ucs[len(ks):])
+ return ks
+
+# 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
+
+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)
diff --git a/blessed/terminal.py.bak b/blessed/terminal.py.bak
deleted file mode 100644
index 1e17d73..0000000
--- a/blessed/terminal.py.bak
+++ /dev/null
@@ -1,785 +0,0 @@
-"This primary module provides the Terminal class."
-# standard modules
-import collections
-import contextlib
-import functools
-import warnings
-import platform
-import codecs
-import curses
-import locale
-import select
-import struct
-import time
-import sys
-import os
-
-try:
- import termios
- import fcntl
- import tty
-except ImportError:
- tty_methods = ('setraw', 'cbreak', 'kbhit', 'height', 'width')
- msg_nosupport = (
- "One or more of the modules: 'termios', 'fcntl', and 'tty' "
- "are not found on your platform '{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)
- 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:
- # alias py2 exception to py3
- InterruptedError = select.error
-
-# local imports
-from .formatters import (
- ParameterizingString,
- NullCallableString,
- resolve_capability,
- resolve_attribute,
-)
-
-from .sequences import (
- init_sequence_patterns,
- SequenceTextWrapper,
- Sequence,
-)
-
-from .keyboard import (
- get_keyboard_sequences,
- get_keyboard_codes,
- resolve_sequence,
-)
-
-
-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.
- """
-
- #: Sugary names for commonly-used capabilities
- _sugar = dict(
- save='sc',
- restore='rc',
- # 'clear' clears the whole screen.
- clear_eol='el',
- clear_bol='el1',
- clear_eos='ed',
- position='cup', # deprecated
- enter_fullscreen='smcup',
- exit_fullscreen='rmcup',
- move='cup',
- move_x='hpa',
- move_y='vpa',
- move_left='cub1',
- move_right='cuf1',
- move_up='cuu1',
- move_down='cud1',
- hide_cursor='civis',
- normal_cursor='cnorm',
- reset_colors='op', # oc doesn't work on my OS X terminal.
- normal='sgr0',
- reverse='rev',
- italic='sitm',
- no_italic='ritm',
- shadow='sshm',
- no_shadow='rshm',
- standout='smso',
- no_standout='rmso',
- subscript='ssubm',
- no_subscript='rsubm',
- superscript='ssupm',
- no_superscript='rsupm',
- underline='smul',
- no_underline='rmul')
-
- def __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``.
-
- """
- 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
- 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 = None
-
- self._is_a_tty = stream_fd is not None and os.isatty(stream_fd)
- self._does_styling = ((self.is_a_tty or force_styling) and
- force_styling is not None)
-
- # keyboard_fd only non-None if both stdin and stdout is a tty.
- self.keyboard_fd = (self.keyboard_fd
- if self.keyboard_fd is not None and
- self.is_a_tty and os.isatty(self.keyboard_fd)
- else None)
- self._normal = None # cache normal attr, preventing recursive lookups
-
- # The descriptor to direct terminal initialization sequences to.
- # 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._kind = kind or os.environ.get('TERM', 'unknown')
-
- if self.does_styling:
- # Make things like tigetstr() work. Explicit args make setupterm()
- # work even when -s is passed to nosetests. Lean toward sending
- # init sequences to the stream if it has a file descriptor, and
- # send them to stdout as a fallback, since they have to go
- # somewhere.
- try:
- if (platform.python_implementation() == 'PyPy' and
- 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)
- else:
- curses.setupterm(self._kind, self._init_descriptor)
- except curses.error as err:
- warnings.warn('Failed to setupterm(kind={0!r}): {1}'
- .format(self._kind, err))
- self._kind = None
- self._does_styling = False
- else:
- if _CUR_TERM is None or self._kind == _CUR_TERM:
- _CUR_TERM = self._kind
- else:
- warnings.warn(
- 'A terminal of kind "%s" has been requested; due to an'
- ' internal python curses bug, terminal capabilities'
- ' for a terminal of kind "%s" will continue to be'
- ' returned for the remainder of this process.' % (
- self._kind, _CUR_TERM,))
-
- for re_name, re_val in init_sequence_patterns(self).items():
- setattr(self, re_name, re_val)
-
- # build database of int code <=> KEY_NAME
- self._keycodes = get_keyboard_codes()
-
- # store attributes as: self.KEY_NAME = code
- for key_code, key_name in self._keycodes.items():
- setattr(self, key_name, key_code)
-
- # build database of sequence <=> KEY_NAME
- self._keymap = get_keyboard_sequences(self)
-
- self._keyboard_buf = collections.deque()
- if self.keyboard_fd is not None:
- locale.setlocale(locale.LC_ALL, '')
- self._encoding = locale.getpreferredencoding() or 'ascii'
- try:
- self._keyboard_decoder = codecs.getincrementaldecoder(
- self._encoding)()
- except LookupError as err:
- warnings.warn('%s, fallback to ASCII for keyboard.' % (err,))
- self._encoding = 'ascii'
- self._keyboard_decoder = codecs.getincrementaldecoder(
- self._encoding)()
-
- self.stream = stream
-
- def __getattr__(self, attr):
- """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).
-
- 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.
- """
- if not self.does_styling:
- return NullCallableString()
- val = resolve_attribute(self, attr)
- # Cache capability codes.
- setattr(self, attr, val)
- return val
-
- @property
- def kind(self):
- """Name of this terminal type as string."""
- return self._kind
-
- @property
- def does_styling(self):
- """Whether this instance will emit terminal sequences (bool)."""
- return self._does_styling
-
- @property
- def is_a_tty(self):
- """Whether the ``stream`` associated with this instance is a terminal
- (bool)."""
- return self._is_a_tty
-
- @property
- def height(self):
- """T.height -> int
-
- The height of the terminal in characters.
- """
- return self._height_and_width().ws_row
-
- @property
- def width(self):
- """T.width -> int
-
- The width of the terminal in characters.
- """
- return self._height_and_width().ws_col
-
- @staticmethod
- def _winsize(fd):
- """T._winsize -> WINSZ(ws_row, ws_col, ws_xpixel, ws_ypixel)
-
- The tty connected by file desriptor fd is queried for its window size,
- and returned as a collections.namedtuple instance WINSZ.
-
- May raise exception IOError.
- """
- if HAS_TTY:
- data = fcntl.ioctl(fd, 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__):
- try:
- if fd is not None:
- return self._winsize(fd)
- except IOError:
- pass
-
- return WINSZ(ws_row=int(os.getenv('LINES', '25')),
- ws_col=int(os.getenv('COLUMNS', '80')),
- ws_xpixel=None,
- ws_ypixel=None)
-
- @contextlib.contextmanager
- def location(self, x=None, y=None):
- """Return a context manager for temporarily moving the cursor.
-
- Move the cursor to a certain position on entry, let you print stuff
- there, then return the cursor to its original position::
-
- term = Terminal()
- with term.location(2, 5):
- print 'Hello, world!'
- for x in xrange(10):
- print 'I can do it %i times!' % x
-
- Specify ``x`` to move to a certain column, ``y`` to move to a certain
- row, both, or neither. If you specify neither, only the saving and
- restoration of cursor position will happen. This can be useful if you
- simply want to restore your place after doing some manual cursor
- movement.
-
- """
- # Save position and move to the requested column, row, or both:
- self.stream.write(self.save)
- if x is not None and y is not None:
- self.stream.write(self.move(y, x))
- elif x is not None:
- self.stream.write(self.move_x(x))
- elif y is not None:
- self.stream.write(self.move_y(y))
- try:
- yield
- finally:
- # Restore original cursor position:
- self.stream.write(self.restore)
-
- @contextlib.contextmanager
- def fullscreen(self):
- """Return a context manager that enters fullscreen mode while inside it
- and restores normal mode on leaving.
-
- 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 call may not be tested; only one screen state may be saved at a
- time.
- """
- self.stream.write(self.enter_fullscreen)
- try:
- yield
- finally:
- self.stream.write(self.exit_fullscreen)
-
- @contextlib.contextmanager
- def hidden_cursor(self):
- """Return a context manager that hides the cursor upon entering,
- and makes it visible again upon exiting."""
- self.stream.write(self.hide_cursor)
- try:
- yield
- finally:
- self.stream.write(self.normal_cursor)
-
- @property
- def color(self):
- """Returns capability that sets the foreground color.
-
- 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()
- return ParameterizingString(self._foreground_color,
- self.normal, 'color')
-
- @property
- def on_color(self):
- "Returns capability that sets the background color."
- if not self.does_styling:
- return NullCallableString()
- return ParameterizingString(self._background_color,
- self.normal, 'on_color')
-
- @property
- def normal(self):
- "Returns sequence that resets video attribute."
- if self._normal:
- return self._normal
- self._normal = resolve_capability(self, 'normal')
- return self._normal
-
- @property
- def number_of_colors(self):
- """Return the number of colors the terminal supports.
-
- Common values are 0, 8, 16, 88, and 256. Most commonly
- this may be used to test color capabilities at all::
-
- if term.number_of_colors:
- ..."""
- # trim value to 0, as tigetnum('colors') returns -1 if no support,
- # -2 if no such capability.
- return max(0, self.does_styling and curses.tigetnum('colors') or -1)
-
- @property
- def _foreground_color(self):
- return self.setaf or self.setf
-
- @property
- def _background_color(self):
- 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."""
- 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."""
- 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."""
- if width is None:
- width = self.width
- return Sequence(text, self).center(width, fillchar)
-
- def length(self, text):
- """T.length(text) -> int
-
- 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*..
- """
- 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.
- """
- return Sequence(text, self).strip(chars)
-
- def rstrip(self, text, chars=None):
- """T.rstrip(text) -> unicode
-
- Return string ``text`` with terminal sequences and trailing whitespace
- removed.
- """
- return Sequence(text, self).rstrip(chars)
-
- def lstrip(self, text, chars=None):
- """T.lstrip(text) -> unicode
-
- Return string ``text`` with terminal sequences and leading whitespace
- removed.
- """
- return Sequence(text, self).lstrip(chars)
-
- def strip_seqs(self, text):
- """T.strip_seqs(text) -> unicode
-
- Return string ``text`` stripped only of its sequences.
- """
- 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``.
- """
- width = self.width if width is None else width
- lines = []
- for line in text.splitlines():
- lines.extend(
- (_linewrap for _linewrap in SequenceTextWrapper(
- width=width, term=self, **kwargs).wrap(text))
- if line.strip() else (u'',))
-
- return lines
-
- def getch(self):
- """T.getch() -> unicode
-
- 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.kbhit() returns True.
-
- Implementors of input streams other than os.read() on the stdin fd
- should derive and 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 kbhit(self, timeout=None, _intr_continue=True):
- """T.kbhit([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.
- """
- # Special care is taken to handle a custom SIGWINCH handler, which
- # causes select() to be interrupted with errno 4 (EAGAIN) --
- # it is ignored, and a new timeout value is derived from the previous,
- # unless timeout becomes negative, because signal handler has blocked
- # 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, ]
- 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)
- except InterruptedError:
- if not _intr_continue:
- return u''
- if timeout is not None:
- # subtract time already elapsed,
- timeout -= time.time() - stime
- if timeout > 0:
- continue
- # no time remains after handling exception (rare)
- ready_r = []
- break
- else:
- break
-
- return False if self.keyboard_fd is None else check_r == ready_r
-
- @contextlib.contextmanager
- def cbreak(self):
- """Return a context manager that enters 'cbreak' mode: disabling line
- buffering of keyboard input, making characters typed by the user
- immediately available to the program. Also referred to as 'rare'
- mode, this is the opposite of 'cooked' mode, the default for most
- shells.
-
- In 'cbreak' mode, echo of input is also disabled: the application must
- explicitly print any input received, if they so wish.
-
- 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
- """
- if HAS_TTY and self.keyboard_fd is not None:
- # save current terminal mode,
- save_mode = termios.tcgetattr(self.keyboard_fd)
- tty.setcbreak(self.keyboard_fd, termios.TCSANOW)
- try:
- yield
- finally:
- # restore prior mode,
- termios.tcsetattr(self.keyboard_fd,
- termios.TCSAFLUSH,
- save_mode)
- else:
- yield
-
- @contextlib.contextmanager
- def raw(self):
- """Return a context manager that enters *raw* mode. Raw mode is
- similar to *cbreak* mode, in that characters typed are immediately
- available to ``inkey()`` with one exception: the interrupt, quit,
- suspend, and flow control characters are all passed through as their
- raw character values instead of generating a signal.
- """
- if HAS_TTY and self.keyboard_fd is not None:
- # save current terminal mode,
- save_mode = termios.tcgetattr(self.keyboard_fd)
- tty.setraw(self.keyboard_fd, termios.TCSANOW)
- try:
- yield
- finally:
- # restore prior mode,
- termios.tcsetattr(self.keyboard_fd,
- termios.TCSAFLUSH,
- save_mode)
- else:
- yield
-
- @contextlib.contextmanager
- def keypad(self):
- """
- 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.
-
- On an IBM-PC keypad of ttype *xterm*, with numlock off, the
- lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``.
-
- However, upon entering keypad mode, ``\\x1b[OF`` is transmitted,
- translating to ``KEY_LL`` (lower-left key), allowing diagonal
- direction keys to be determined.
- """
- try:
- self.stream.write(self.smkx)
- yield
- finally:
- self.stream.write(self.rmkx)
-
- def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True):
- """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress()
-
- 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 a value
- of ``False`` for ``_intr_continue``.
- """
- # 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') ?
- def _timeleft(stime, timeout):
- """_timeleft(stime, timeout) -> float
-
- Returns time-relative time remaining before ``timeout``
- after time elapsed since ``stime``.
- """
- if timeout is not None:
- if timeout is 0:
- return 0
- return max(0, timeout - (time.time() - stime))
-
- resolve = functools.partial(resolve_sequence,
- mapper=self._keymap,
- codes=self._keycodes)
-
- stime = time.time()
-
- # re-buffer previously received keystrokes,
- ucs = u''
- while self._keyboard_buf:
- ucs += self._keyboard_buf.pop()
-
- # receive all immediately available bytes
- while self.kbhit(0):
- ucs += self.getch()
-
- # decode keystroke, if any
- ks = resolve(text=ucs)
-
- # so long as the most immediately received or buffered keystroke is
- # incomplete, (which may be a multibyte encoding), block until until
- # one is received.
- while not ks and self.kbhit(_timeleft(stime, timeout), _intr_continue):
- ucs += self.getch()
- ks = 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 is self.KEY_ESCAPE:
- esctime = time.time()
- while (ks.code is self.KEY_ESCAPE and
- self.kbhit(_timeleft(esctime, esc_delay))):
- ucs += self.getch()
- ks = resolve(text=ucs)
-
- # buffer any remaining text received
- self._keyboard_buf.extendleft(ucs[len(ks):])
- return ks
-
-# 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
-
-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)