Skip to content

Instantly share code, notes, and snippets.

@jquast
Last active December 25, 2015 18:09
Show Gist options
  • Save jquast/7018509 to your computer and use it in GitHub Desktop.
Save jquast/7018509 to your computer and use it in GitHub Desktop.
for EK's feedback: specificly, what do you think of @cbreak, the _init_keystrokes, term.inkey(), @mouse_tracking, the Keystroke() unicode-subclass, at least: where they are impl. and how they are named. How the user uses them. you'll notice a lot of "legacy" code that was refactored in the current pull requests, so those may be ignored, also ple…
from contextlib import contextmanager
import curses
import curses.has_key
import curses.ascii
from curses import tigetstr, tigetnum, setupterm, tparm
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"""
import os
from platform import python_version_tuple
import textwrap
import codecs
import locale
import struct
import time
import math
import sys
import re
if sys.platform == 'win32':
import msvcrt
else:
import termios
import select
import fcntl
import tty
__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.')
MMODE_X10 = 9
MMODE_VT200 = 1000
MMODE_VT200_HIGHLIGHT = 1001
MMODE_BTN_EVENT = 1002
MMODE_ANY_EVENT = 1003
MMODE_FOCUS_EVENT = 1004
MMODE_EXT_MODE = 1005
MMODE_SGR_EXT_MODE = 1006
MMODE_URXVT_EXT_MODE = 1015
MMODE_ALTERNATE_SCROLL = 1007
MMODE_XTERM_262 = 262
MMODE_URXVT_910 = 910
MMODE_DEFAULT = MMODE_URXVT_910
class Terminal(object):
"""An abstraction around terminal capabilities
Unlike curses, this doesn't require clearing the screen before doing
anything, and it's friendlier to use. It keeps the endless calls to
``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently
when somebody pipes your output to a non-terminal.
Instance attributes:
``stream``
The stream the terminal outputs to. It's convenient to pass the stream
around with the terminal; it's almost always needed when the terminal
is and saves sticking lots of extra args on client functions in
practice.
``encoding``
The encoding used for keyboard input in the ``inkey()`` method; by
default it is the preferred encoding of the environment locale.
"""
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``.
"""
# any desire for passing in a 'virtual keyboard' input stream??
i_stream = sys.__stdin__
if stream is None:
o_stream = sys.__stdout__
else:
o_stream = stream
# for StringIO's and such, a file descriptor is not necessarily
# always available for the output stream; that is ok, though, as
# long as the stream offers a .write method.
try:
o_fd = (o_stream.fileno() if hasattr(o_stream, 'fileno')
and callable(o_stream.fileno) else None)
except IOUnsupportedOperation:
o_fd = None
# os.isatty returns True if output stream is an open file
# descriptor connected to the slave end of a terminal.
self._is_a_tty = o_fd is not None and os.isatty(o_fd)
self._do_styling = ((self.is_a_tty or force_styling) and
force_styling is not None)
# an open file descriptor of some sort is necessary for setupterm(),
# even though it goes unused when an alternative stream is specified.
self.o_fd = sys.__stdout__.fileno() if o_fd is None else o_fd
if self.do_styling:
# curses.setupterm is the foot in the water, providing access to
# the extensive terminal capabilities database so necessary for
# blessings.
# Note: we go through great lengths to prevent diving into curses
# fully by avoiding a call to curses.initscr().
# Prefer the stream file descriptor if available, using stdout as a
# a fallback. Even though it is unused, it is required by setuperm.
setupterm(kind or os.environ.get('TERM', 'unknown'), self.o_fd)
self.stream = o_stream
self.i_stream = i_stream
# a beginning state of echo ON and canonical mode is assumed.
self._state_echo = True
self._state_canonical = True
# dictionary of multibyte sequences to be paired with key codes
self._keymap = {}
# list of key code names
self._keycodes = []
# Inherit curses keycap capability names, such as KEY_DOWN, to be
# used with Keystroke ``code`` property values for comparison to the
# Terminal class instance it was received on.
max_int = 256 # curses keycode values begin beyond 8-bit range
for keycode in [kc for kc in dir(curses) if kc.startswith('KEY_')]:
value = getattr(curses, keycode)
self._keycodes.append(keycode)
setattr(self, keycode, value)
max_int = max(max_int, value)
# Holy smokes; I made this up! Ctrl+Space sends \x00.
# Ctrl+Space is used in interfaces fe. midnight commander.
self.KEY_CSPACE = max_int + 1
if self.is_a_tty:
# determine encoding of input stream. Only used for keyboard
# input on posix systems. win32 systems use getwche which returns
# unicode, and does not require decoding.
locale.setlocale(locale.LC_ALL, '')
self.encoding = locale.getpreferredencoding()
if sys.platform != 'win32':
self._idecoder = codecs.getincrementaldecoder(self.encoding)()
self.i_buf = []
# create lookup dictionary for multibyte keyboard input sequences
self._init_keystrokes()
# Friendly mnemonics for 'KEY_DELETE', 'KEY_INSERT', 'KEY_PGUP',
# and 'KEY_PGDOWN', 'KEY_SPGUP', 'KEY_SPGDOWN'.
self.KEY_DELETE = self.KEY_DC
self.KEY_INSERT = self.KEY_IC
self.KEY_PGUP = self.KEY_PPAGE
self.KEY_PGDOWN = self.KEY_NPAGE
self.KEY_SUP = self.KEY_SR # scroll reverse (shift+pgup)
self.KEY_SDOWN = self.KEY_SF # scroll forward (shift+pgdown)
self.KEY_ESCAPE = self.KEY_EXIT
# 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 _init_keystrokes(self):
# curses.has_key._capability_names is a dictionary keyed by termcap
# capabilities, with integer values to be paired with KEY_ names;
# using this dictionary, we query for the terminal sequence of the
# terminal capability, and, if any result is found, store the sequence
# in the _keymap lookup table with the integer valued to be paired by
# key codes.
for i_val, capability in curses.has_key._capability_names.iteritems():
seq = curses.tigetstr(capability)
if seq is not None:
self._keymap[seq.decode('iso8859-1')] = i_val
# monkey-patch non-destructive space as KEY_RIGHT,
# in 'xterm-256color' 'kcuf1' is '\x1bOC' and 'cuf1' = '\x1b[C'.
# xterm sends '\x1b[C'
ndsp = curses.tigetstr('cuf1')
if ndsp is not None:
self._keymap[ndsp.decode('iso8859-1')] = self.KEY_RIGHT
# ... as well as a list of general NVT sequences you would
# expect to receive from any remote terminals. Notably the
# variables of ENTER (^J and ^M), backspace (^H), delete (127),
# and common sequencess across putty, rxvt, kermit, minicom,
# SyncTerm, windows telnet, HyperTerminal, netrunner ...
self._keymap.update([
(unichr(10), self.KEY_ENTER), (unichr(13), self.KEY_ENTER),
(unichr(8), self.KEY_BACKSPACE),
(u"\x1bOA", self.KEY_UP), (u"\x1bOB", self.KEY_DOWN),
(u"\x1bOC", self.KEY_RIGHT), (u"\x1bOD", self.KEY_LEFT),
(u"\x1bOH", self.KEY_LEFT),
(u"\x1bOF", self.KEY_END),
(u"\x1b[A", self.KEY_UP), (u"\x1b[B", self.KEY_DOWN),
(u"\x1b[C", self.KEY_RIGHT), (u"\x1b[D", self.KEY_LEFT),
(u"\x1b[U", self.KEY_NPAGE), (u"\x1b[V", self.KEY_PPAGE),
(u"\x1b[H", self.KEY_HOME), (u"\x1b[F", self.KEY_END),
(u"\x1b[K", self.KEY_END),
(u"\x1bA", self.KEY_UP), (u"\x1bB", self.KEY_DOWN),
(u"\x1bC", self.KEY_RIGHT), (u"\x1bD", self.KEY_LEFT),
(u"\x1b?x", self.KEY_UP), (u"\x1b?r", self.KEY_DOWN),
(u"\x1b?v", self.KEY_RIGHT), (u"\x1b?t", self.KEY_LEFT),
(u"\x1b[@", self.KEY_IC), # insert
(unichr(127), self.KEY_BACKSPACE), # ^? is backspace (int 127)
])
# windows 'multibyte' translation, not tested.
if sys.platform == 'win32':
# http://msdn.microsoft.com/en-us/library/aa299374%28VS.60%29.aspx
self._keymap.update([
(u'\xe0\x48', self.KEY_UP), (u'\xe0\x50', self.KEY_DOWN),
(u'\xe0\x4D', self.KEY_RIGHT), (u'\xe0\x4B', self.KEY_LEFT),
(u'\xe0\x51', self.KEY_NPAGE), (u'\xe0\x49', self.KEY_PPAGE),
(u'\xe0\x47', self.KEY_HOME), (u'\xe0\x4F', self.KEY_END),
(u'\xe0\x3B', self.KEY_F1), (u'\xe0\x3C', self.KEY_F2),
(u'\xe0\x3D', self.KEY_F3), (u'\xe0\x3E', self.KEY_F4),
(u'\xe0\x3F', self.KEY_F5), (u'\xe0\x40', self.KEY_F6),
(u'\xe0\x41', self.KEY_F7), (u'\xe0\x42', self.KEY_F8),
(u'\xe0\x43', self.KEY_F9), (u'\xe0\x44', self.KEY_F10),
(u'\xe0\x85', self.KEY_F11), (u'\xe0\x86', self.KEY_F12),
(u'\xe0\x4C', self.KEY_B2), # center
(u'\xe0\x52', self.KEY_IC), # insert
(u'\xe0\x53', self.KEY_DC), # delete
])
def __getattr__(self, attr):
"""Return parametrized terminal capabilities, like bold.
For example, you can say ``term.bold`` to get the string that turns on
bold formatting and ``term.normal`` to get the string that turns it off
again. Or you can take a shortcut: ``term.bold('hi')`` bolds its
argument and sets everything to normal afterward. You can even combine
things: ``term.bold_underline_red_on_bright_green('yowzers!')``.
For a parametrized capability like ``cup``, pass the parameters too:
``some_term.cup(line, column)``.
``man terminfo`` for a complete list of capabilities.
Return values are always Unicode.
"""
resolution = (self._resolve_formatter(attr) if self.do_styling
else NullCallableString())
setattr(self, attr, resolution) # Cache capability codes.
return resolution
@property
def do_styling(self):
"""Wether the terminal will attempt to output sequences for
all styling attributes, such as term.bold(). When False, a
null string ('') is used for all styling attributes, this behavior
is forced when force_styling=None, or when the output is not a tty.
"""
return self._do_styling
@property
def is_a_tty(self):
"""Wether the stream is connected to the slave end of a Terminal.
This property is used to identify if the terminal can be interacted
with, for instance, with the ``inkey()`` method. """
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 ``is_a_tty`` is
true before doing any math on the result.)
"""
return self._height_and_width()[0]
@property
def width(self):
"""The width of the terminal in characters
See ``height()`` for some corner cases.
"""
return self._height_and_width()[1]
def _height_and_width_win32(self):
# based on anatoly techtonik's work from pager.py, MIT/Pub Domain.
# completely untested ... please report !!
# WIN32_I_FD = -10
WIN32_O_FD = -11
from ctypes import windll, Structure, byref
from ctypes.wintypes import SHORT, WORD, DWORD
console_handle = windll.kernel32.GetStdHandle(WIN32_O_FD)
# CONSOLE_SCREEN_BUFFER_INFO Structure
class COORD(Structure):
_fields_ = [("X", SHORT), ("Y", SHORT)]
class SMALL_RECT(Structure):
_fields_ = [("Left", SHORT), ("Top", SHORT),
("Right", SHORT), ("Bottom", SHORT)]
class CONSOLE_SCREEN_BUFFER_INFO(Structure):
_fields_ = [("dwSize", COORD),
("dwCursorPosition", COORD),
("wAttributes", WORD),
("srWindow", SMALL_RECT),
("dwMaximumWindowSize", DWORD)]
sbi = CONSOLE_SCREEN_BUFFER_INFO()
ret = windll.kernel32.GetConsoleScreenBufferInfo(
console_handle, byref(sbi))
if ret != 0:
return (sbi.srWindow.Right - sbi.srWindow.Left + 1,
sbi.srWindow.Bottom - sbi.srWindow.Top + 1)
return None, None
def _height_and_width(self):
"""Return a tuple of (terminal height, terminal width).
Returns (None, None) if terminal window size is indeterminate.
"""
# tigetnum('lines') and tigetnum('cols') update only if we call
# setupterm() again.
buf = struct.pack('HHHH', 0, 0, 0, 0)
if sys.platform == 'win32':
value = self._height_and_width_win32()
if value is not None:
return value
for fd in self.o_fd, sys.__stdout__.fileno():
try:
value = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
return struct.unpack('hhhh', value)[0:2]
except IOError:
pass
# throw a hail mairy for environment values
lines, cols = (os.environ.get('LINES', None),
os.environ.get('COLUMNS', None))
if None not in (lines, cols):
return lines, cols
# ! should never be reached
return None, None
def kbhit(self, timeout=0):
""" Returns True if a keypress has been detected on input.
A subsequent call to getch() will not block on cbreak mode.
A timeout of 0 returns immediately (default), A timeout of
``None`` blocks indefinitely. A timeout of non-zero blocks
until timeout seconds elapsed. """
fds = select.select([self.i_stream.fileno()], [], [], timeout)[0]
return self.i_stream.fileno() in fds
def getch(self):
""" Read a single byte from input stream. """
return os.read(self.i_stream.fileno(), 1)
def inkey(self, timeout=None, esc_delay=0.35):
""" Read a single keystroke from input up to timeout in seconds,
translating special application keys, such as KEY_LEFT.
Ensure you use 'cbreak mode', by using the ``cbreak`` context
manager, otherwise input is not received until return is pressed!
When ``timeout`` is None (default), block until input is available.
If ``timeout`` is 0, this function is non-blocking and None is
returned if no input is available. If ``timeout`` is non-zero,
None is returned after ``timeout`` seconds have elapsed without input.
This method differs from using kbhit in combination with getch in that
Multibyte sequences are translated to key code values, and string
sequence of length greater than 1 may be returned.
The result is a unicode-typed instance of the Keystroke class, with
additional properties ``is_sequence`` (bool), ``name`` (str),
and ``value`` (int). esc_delay defines the time after which
an escape key is pressed that the stream awaits a multibyte sequence.
with term.cbreak():
inp = None
while inp not in (u'q', u'Q'):
inp = term.inkey(3)
if inp is None:
print 'timeout after 3 seconds'
elif inp.code == term.KEY_UP:
print 'moving up!'
else:
print 'pressed', 'sequence' if inp.is_sequence else 'ascii'
print 'key:', inp, inp.code
"""
assert self.is_a_tty, u'stream is not a a tty.'
esc = curses.ascii.ESC # posix multibyte sequence (MBS) start mark
wsb = ord('\xe0') # win32 MBS start mark
# return keystrokes buffered by previous calls immediately,
# regardless of timeout
if len(self.i_buf):
return self.i_buf.pop()
# returns time-relative remaining for user-specified timeout
timeleft = lambda cmp_time: (
None if timeout is None else
timeout - (time.time() - cmp_time)
if timeout != 0 else 0)
# returns True if byte appears to mark the beginning of a MBS
chk_start = lambda char: (ord(char) == esc or (
sys.platform == 'win32' and ord(char) == wsb))
stime = time.time()
ready = self.kbhit(timeleft(stime))
if not ready:
return None
byte = self.getch()
buf = [byte, ]
# check for MSB; or just buffer for a rapidly firing input stream
# TODO: meta sends escape", where alt+1 would send '\x1b1' may be imposed
# a performance hit by the multibyte sequence decoder. It must be made
# aware.
while True:
if chk_start(buf[0]):
if (len(buf) == 1 and self.kbhit(esc_delay)) or self.kbhit():
byte = self.getch()
buf.append(byte)
detect = self.resolve_mbs(self._decode_istream(buf)).next()
if (detect.is_sequence and detect.code != self.KEY_EXIT):
# end of MBS,
break
else:
# a poll for a 2nd+ byte failed; simple old ASCII escape.
break
elif self.kbhit():
# more still immediately available; buffer. this also catches
# utf-8, I would suppose, as its not caught by chk_start().
byte = self.getch()
buf.append(byte)
else:
# no more bytes available in 'check for multibyte seq' loop
break
for keystroke in self.resolve_mbs(self._decode_istream(buf)):
self.i_buf.insert(0, keystroke)
item = self.i_buf.pop()
return item
@contextmanager
def mouse_tracking(self, mode=MMODE_DEFAULT):
""" Return a context manager for sending the DEC control sequence
for entering the specified mouse tracking mode. The default mode is
the urxvt extension found in xterm, urxvt, iTerm2, and gnome-terminal."""
# this guy egmot made a push to get this mode in all utf-8 terminals,
# http://www.midnight-commander.org/ticket/2662
# http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
# https://bugzilla.gnome.org/show_bug.cgi?id=662423
# http://web.fis.unico.it/public/rxvt/refer.html#Mouse
if mode == MMODE_XTERM_262:
self.stream.write('\x1b[?%dh' % (MMODE_VT200,))
self.stream.write('\x1b[?%dh' % (MMODE_EXT_MODE,))
elif mode == MMODE_URXVT_910:
self.stream.write('\x1b[?%dh' % (MMODE_VT200,))
self.stream.write('\x1b[?%dh' % (MMODE_URXVT_EXT_MODE,))
else:
self.stream.write('\x1b[?%dh' % (mode,))
self.stream.flush()
try:
yield
finally:
if mode in (MMODE_XTERM_262, MMODE_URXVT_910):
self.stream.write('\x1b[?%dl' % (MMODE_VT200,))
else:
self.stream.write('\x1b[?%dl' % (mode,))
@contextmanager
def cbreak(self):
"""Return a context manager for entering 'cbreak' mode.
This is anagolous to calling python's tty.setcbreak(), except
that a finally clause restores the terminal state.
In cbreak mode (sometimes called "rare" mode) normal tty line
buffering is turned off and characters are available to be read
one by one by ``getch()``. echo of input is also disabled, the
application must explicitly copy-out any input received.
More information can be found in the manual page for curses.h,
http://www.openbsd.org/cgi-bin/man.cgi?query=cbreak
Or 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
This is anagolous to the curses.wrapper() helper function, and
the python standard module tty.setcbreak(), with the exception
that the original terminal mode is restored when leaving context.
"""
assert self.is_a_tty, u'stream is not a a tty.'
mode = termios.tcgetattr(self.i_stream.fileno())
tty.setcbreak(self.i_stream.fileno(), termios.TCSANOW)
try:
yield
finally:
termios.tcsetattr(self.i_stream.fileno(), termios.TCSAFLUSH, mode)
@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))
yield
# Restore original cursor position:
self.stream.write(self.restore)
@contextmanager
def fullscreen(self):
"""Return a context manager that enters fullscreen mode while inside it and restores normal mode on leaving."""
self.stream.write(self.enter_fullscreen)
yield
self.stream.write(self.exit_fullscreen)
@contextmanager
def hidden_cursor(self):
"""Return a context manager that hides the cursor while inside it and makes it visible on leaving."""
self.stream.write(self.hide_cursor)
yield
self.stream.write(self.normal_cursor)
@property
def color(self):
"""Return a capability that sets the foreground color.
The capability is unparametrized until called and passed a number
(0-15), at which point it returns another string which represents a
specific color change. This second string can further be called to
color a piece of text and set everything back to normal afterward.
:arg num: The number, 0-15, of the color
"""
return ParametrizingString(self._foreground_color, self.normal)
def wrap(self, ucs, width=None, **kwargs):
"""
A.wrap(S, [width=None, indent=u'']) -> unicode
Like textwrap.wrap, but honor existing linebreaks and understand
printable length of a unicode string that contains ANSI sequences,
such as colors, bold, etc. if width is not specified, the terminal
width is used.
"""
if width is None:
width = self.width
lines = []
for line in ucs.splitlines():
if line.strip():
for wrapped in ansiwrap(line, width, **kwargs):
lines.append(wrapped)
else:
lines.append(u'')
return lines
@property
def on_color(self):
"""Return a capability that sets the background color.
See ``color()``.
"""
return ParametrizingString(self._background_color, self.normal)
@property
def number_of_colors(self):
"""Return the number of colors the terminal supports.
Common values are 0, 8, 16, 88, and 256.
Though the underlying capability returns -1 when there is no color
support, we return 0. This lets you test more Pythonically::
if term.number_of_colors:
...
We also return 0 if the terminal won't tell us how many colors it
supports, which I think is rare.
"""
# This is actually the only remotely useful numeric capability. We
# don't name it after the underlying capability, because we deviate
# slightly from its behavior, and we might someday wish to give direct
# access to it.
# Returns -1 if no color support, -2 if no such cap.
colors = tigetnum('colors')
# 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."""
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:
# We can encode escape sequences as UTF-8 because they never
# contain chars > 127, and UTF-8 never changes anything within that
# range..
return code.decode('utf-8')
return u''
def _decode_istream(self, buf, end=True):
""" T._decode_istream(buf, end=True)
Incrementaly decode input byte buffer, ``buf``, using the encoding
specified by ``.encoding``. By default, encoding is the locale's
preferred encoding detected into Unicode.
"""
decoded = []
for num, byte in enumerate(buf):
is_final = end and num == (len(buf) - 1)
ucs = self._idecoder.decode(byte, final=is_final)
if ucs is not None:
decoded.append(ucs)
return u''.join(decoded)
def resolve_mbs(self, ucs):
""" T._resolve_mbs(ucs) -> Keystroke
This generator yields unicode sequences with additional
``.is_sequence``, ``.name``, and ``.code`` properties that
describle matching multibyte input sequences to keycode
translations (if any) detected in input unicode buffer, ``ucs``.
For win32 systems, the input buffer is a list of unicode values
received by getwch. For Posix systems, the input buffer is a list
of bytes recieved by sys.stdin.read(1), to be decoded to Unicode by
the preferred locale.
"""
CR_NVT = u'\r\x00' # NVT return (telnet, etc.)
CR_LF = u'\r\n' # carriage return + newline
CR_CHAR = u'\n' # returns only '\n' when return is detected.
esc = curses.ascii.ESC
def resolve_keycode(integer):
"""
Returns printable string to represent matched multibyte sequence,
such as 'KEY_LEFT'. For purposes of __repr__ or __str__ ?
"""
assert type(integer) is int
for keycode in self._keycodes:
if getattr(self, keycode) == integer:
return keycode
def scan_keymap(ucs):
"""
Return sequence and keycode if ucs begins with any known sequence.
"""
for (keyseq, keycode) in self._keymap.iteritems():
if ucs.startswith(keyseq):
return (keyseq, resolve_keycode(keycode), keycode)
return (None, None, None) # no match
# special care is taken to pass over the ineveitably troublesome
# carriage return, which is a multibyte sequence issue of its own;
# expect to receieve any of '\r\00', '\r\n', '\r', or '\n', but
# yield only a single byte, u'\n'.
while len(ucs):
if ucs[:2] in (CR_NVT, CR_LF): # telnet return or dos CR+LF
yield Keystroke(CR_CHAR, ('KEY_ENTER', self.KEY_ENTER))
ucs = ucs[2:]
continue
elif ucs[:1] in (u'\r', u'\n'): # single-byte CR
yield Keystroke(CR_CHAR, ('KEY_ENTER', self.KEY_ENTER))
ucs = ucs[1:]
continue
elif 1 == len(ucs) and ucs == unichr(esc):
yield Keystroke(ucs[0], ('KEY_ESCAPE', self.KEY_ESCAPE))
break
elif 1 == len(ucs) and ucs == unichr(0):
# null byte isn't; actually is ctrl+spacebar;
yield Keystroke(ucs[0], ('KEY_CSPACE', self.KEY_CSPACE))
break
keyseq, keyname, keycode = scan_keymap(ucs)
if (keyseq, keyname, keycode) == (None, None, None):
yield Keystroke(ucs[0], None)
ucs = ucs[1:]
else:
yield Keystroke(keyseq, (keyname, keycode))
ucs = ucs[len(keyseq):]
def _resolve_color(self, color):
"""Resolve a color like red or on_bright_green into a callable capability."""
# TODO: Does curses automatically exchange red and blue and cyan and
# yellow when a terminal supports setf/setb rather than setaf/setab?
# I'll be blasted if I can find any documentation. The following
# assumes it does.
color_cap = (self._background_color if 'on_' in color else
self._foreground_color)
# curses constants go up to only 7, so add an offset to get at the
# bright colors at 8-15:
offset = 8 if 'bright_' in color else 0
base_color = color.rsplit('_', 1)[-1]
return self._formatting_string(
color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset))
@property
def _foreground_color(self):
return self.setaf or self.setf
@property
def _background_color(self):
return self.setab or self.setb
def _formatting_string(self, formatting):
"""Return a new ``FormattingString`` which implicitly receives my notion of "normal"."""
return FormattingString(formatting, self.normal)
def ljust(self, ucs, width=None, fillchar=u' '):
"""T.ljust(ucs, [width], [fillchar]) -> string
Return ucs left-justified in a string of length width. Padding is
done using the specified fill character (default is a space).
Default width is terminal width. ucs is escape sequence safe."""
if width is None:
width = self.width
return AnsiString(ucs).ljust(width, fillchar)
def rjust(self, ucs, width=None, fillchar=u' '):
"""T.rjust(ucs, [width], [fillchar]) -> string
Return ucs right-justified in a string of length width. Padding is
done using the specified fill character (default is a space)
Default width is terminal width. ucs is escape sequence safe."""
if width is None:
width = self.width
return AnsiString(ucs).rjust(width, fillchar)
def center(self, ucs, width=None, fillchar=u' '):
"""T.center(ucs, [width], [fillchar]) -> string
Return ucs centered in a string of length width. Padding is
done using the specified fill character (default is a space).
Default width is terminal width. ucs is escape sequence safe."""
if width is None:
width = self.width
return AnsiString(ucs).center(width, fillchar)
def derivative_colors(colors):
"""Return the names of valid color variants, given the base colors."""
return set([('on_' + c) for c in colors] +
[('bright_' + c) for c in colors] +
[('on_bright_' + c) for c in colors])
COLORS = set(['black', 'red', 'green',
'yellow', 'blue', 'magenta', 'cyan', 'white'])
COLORS.update(derivative_colors(COLORS))
COMPOUNDABLES = (COLORS |
set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic',
'shadow', 'standout', 'subscript', 'superscript']))
class ParametrizingString(unicode):
"""A Unicode string which can be called to parametrize it as a terminal capability"""
def __new__(cls, formatting, normal=None):
"""Instantiate.
:arg normal: If non-None, indicates that, once parametrized, this can
be used as a ``FormattingString``. The value is used as the
"normal" capability.
"""
new = unicode.__new__(cls, formatting)
new._normal = normal
return new
def __call__(self, *args):
try:
# Re-encode the cap, because tparm() takes a bytestring in Python
# 3. However, appear to be a plain Unicode string otherwise so
# concats work.
parametrized = tparm(self.encode('utf-8'), *args).decode('utf-8')
return (parametrized if self._normal is None else
FormattingString(parametrized, self._normal))
except curses.error:
# Catch "must call (at least) setupterm() first" errors, as when
# running simply `nosetests` (without progressive) on nose-
# progressive. Perhaps the terminal has gone away between calling
# tigetstr and calling tparm.
return u''
except TypeError:
# If the first non-int (i.e. incorrect) arg was a string, suggest
# something intelligent:
if len(args) >= 1 and isinstance(args[0], basestring):
raise TypeError(
'A native or nonexistent capability template received '
'%r when it was expecting ints. You probably misspelled a '
'formatting call like bright_red_on_white(...).' % args)
else:
# Somebody passed a non-string; I don't feel confident
# guessing what they were trying to do.
raise
class FormattingString(unicode):
"""A Unicode string which can be called upon a piece of text to wrap it in formatting"""
def __new__(cls, formatting, normal):
new = unicode.__new__(cls, formatting)
new._normal = normal
return new
def __call__(self, text):
"""Return a new string that is ``text`` formatted with my contents.
At the beginning of the string, I prepend the formatting that is my
contents. At the end, I append the "normal" sequence to set everything
back to defaults. The return value is always a Unicode.
"""
return self + text + self._normal
class NullCallableString(unicode):
"""A dummy class to stand in for ``FormattingString`` and ``ParametrizingString``
A callable bytestring that returns an empty Unicode when called with an int
and the arg otherwise. We use this when there is no tty and so all
capabilities are blank.
"""
def __new__(cls):
new = unicode.__new__(cls, u'')
return new
def __call__(self, arg):
if isinstance(arg, int):
return u''
return arg # TODO: Force even strs in Python 2.x to be unicodes? Nah. How would I know what encoding to use to convert it?
class Keystroke(unicode):
""" A unicode-derived class for matched multibyte input sequences. If the
unicode string is a multibyte input sequence, then the ``is_sequence``
property is True, and the ``name`` and ``value`` properties return a
string and integer value.
"""
def __new__(cls, ucs, keystroke=None):
new = unicode.__new__(cls, ucs)
new._keystroke = keystroke
return new
@property
def is_sequence(self):
""" Returns True if value represents a multibyte sequence. """
return self._keystroke is not None
@property
def name(self):
""" Returns string name of multibyte sequence, such as 'KEY_HOME'."""
if self._keystroke is None:
return str(None)
return self._keystroke[0]
@property
def code(self):
""" Returns curses integer value of multibyte sequence, such as 323."""
if self._keystroke is not None:
return self._keystroke[1]
assert 1 == len(self), (
'No integer value available for multibyte sequence')
return ord(self)
def split_into_formatters(compound):
"""Split a possibly compound format string into segments.
>>> split_into_formatters('bold_underline_bright_blue_on_red')
['bold', 'underline', 'bright_blue', 'on_red']
"""
merged_segs = []
# These occur only as prefixes, so they can always be merged:
mergeable_prefixes = ['on', 'bright', 'on_bright']
for s in compound.split('_'):
if merged_segs and merged_segs[-1] in mergeable_prefixes:
merged_segs[-1] += '_' + s
else:
merged_segs.append(s)
return merged_segs
class AnsiWrapper(textwrap.TextWrapper):
# pylint: disable=C0111
# Missing docstring
def _wrap_chunks(self, chunks):
"""
ANSI-safe varient of wrap_chunks, with exception of movement seqs!
"""
lines = []
if self.width <= 0:
raise ValueError("invalid width %r (must be > 0)" % self.width)
chunks.reverse()
while chunks:
cur_line = []
cur_len = 0
if lines:
indent = self.subsequent_indent
else:
indent = self.initial_indent
width = self.width - len(indent)
if (not hasattr(self, 'drop_whitespace') or self.drop_whitespace
) and (chunks[-1].strip() == '' and lines):
del chunks[-1]
while chunks:
chunk_len = len(AnsiString(chunks[-1]))
if cur_len + chunk_len <= width:
cur_line.append(chunks.pop())
cur_len += chunk_len
else:
break
if chunks and len(AnsiString(chunks[-1])) > width:
self._handle_long_word(chunks, cur_line, cur_len, width)
if (not hasattr(self, 'drop_whitespace') or self.drop_whitespace
) and (cur_line and cur_line[-1].strip() == ''):
del cur_line[-1]
if cur_line:
lines.append(indent + u''.join(cur_line))
return lines
def ansiwrap(ucs, width=70, **kwargs):
""" Wrap a single paragraph of Unicode terminal sequences,
returning a list of wrapped lines. ucs is ANSI-color safe.
"""
assert ('break_long_words' not in kwargs
or not kwargs['break_long_words']), (
'break_long_words is not sequence-safe')
kwargs['break_long_words'] = False
return AnsiWrapper(width=width, **kwargs).wrap(ucs)
_ANSI_COLOR = re.compile(r'\033\[(\d{2,3})m')
_ANSI_RIGHT = re.compile(r'\033\[(\d{1,4})C')
_ANSI_CODEPAGE = re.compile(r'\033[\(\)][AB012]')
_ANSI_WILLMOVE = re.compile(r'\033\[[HuABCDEFG]')
_ANSI_WONTMOVE = re.compile(r'\033\[[smJK]')
class AnsiString(unicode):
"""
This unicode variation understands the effect of ANSI sequences of
printable length, allowing a properly implemented .rjust(), .ljust(),
.center(), and .len() with bytes using terminal sequences
Other ANSI helper functions also provided as methods.
"""
# this is really bad; kludge dating as far back as 2002
def __new__(cls, ucs):
new = unicode.__new__(cls, ucs)
return new
def ljust(self, width, fillchar=u' '):
return self + fillchar * (max(0, width - self.__len__()))
def rjust(self, width, fillchar=u' '):
return fillchar * (max(0, width - self.__len__())) + self
def center(self, width, fillchar=u' '):
split = max(0.0, float(width) - self.__len__()) / 2
return (fillchar * (max(0, int(math.floor(split)))) + self
+ fillchar * (max(0, int(math.ceil(split)))))
def __len__(self):
"""
Return the printed length of a string that contains (some types) of
ANSI sequences. Although accounted for, strings containing sequences
such as cls() will not give accurate returns (0). backspace, delete,
and double-wide east-asian characters are accounted for.
"""
# 'nxt' points to first *ch beyond current ansi sequence, if any.
# 'width' is currently estimated display length.
nxt, width = 0, 0
def get_padding(ucs):
"""
get_padding(S) -> integer
Returns int('nn') in CSI sequence \\033[nnC for use with replacing
ansi.right(nn) with printable characters. prevents bleeding when
used with scrollable art. Otherwise 0 if not \033[nnC sequence.
Needed to determine the 'width' of art that contains this padding.
"""
right = _ANSI_RIGHT.match(ucs)
if right is not None:
return int(right.group(1))
return 0
for idx in range(0, unicode.__len__(self)):
width += get_padding(self[idx:])
if idx == nxt:
nxt = idx + _seqlen(self[idx:])
if nxt <= idx:
# 'East Asian Fullwidth' and 'East Asian Wide' characters
# can take 2 cells, see http://www.unicode.org/reports/tr11/
# and http://www.gossamer-threads.com/lists/python/bugs/972834
# TODO: could use wcswidth, but i've ommitted it -jq
width += 1
nxt = idx + _seqlen(self[idx:]) + 1
return width
def _is_movement(ucs):
"""
_is_movement(ucs) -> bool
Returns True if ucs begins with a terminal sequence that is
"unhealthy for padding", that is, it has effects on the
cursor position that are indeterminate.
"""
# pylint: disable=R0911,R09120
# Too many return statements (20/6)
# Too many branches (23/12)
# this isn't the best, perhaps for readability a giant REGEX can and
# probably and already has been made.
esc = curses.ascii.ESC
slen = unicode.__len__(ucs)
if 0 == slen:
return False
elif ucs[0] in u'\r\n\b':
return True
elif ucs[0] != unichr(esc):
return False
elif slen > 1 and ucs[1] == u'c':
# reset
return True
elif slen < 3:
# unknown
return False
elif _ANSI_CODEPAGE.match(ucs):
return False
elif (ucs[0], ucs[1], ucs[2]) == (u'\x1b', u'#', u'8'):
# 'fill the screen'
return True
elif _ANSI_WILLMOVE.match(ucs):
return True
elif _ANSI_WONTMOVE.match(ucs):
return False
elif slen < 4:
# unknown
return False
elif ucs[2] == '?':
# DEC private modes
# http://web.fis.unico.it/public/rxvt/refer.html#PrivateModes
# fe., show/hide cursor is CSI + '?25[hl]'
ptr2 = 3
while (ucs[ptr2].isdigit()):
ptr2 += 1
if not ucs[ptr2] in u'tsrhl': # toggle,save,restore,set,reset
return False
return False
elif ucs[2] in ('(', ')'):
# CSI + '\([AB012]' # set G0/G1
assert ucs[3] in (u'A', 'B', '0', '1', '2',)
return False
elif not ucs[2].isdigit():
# illegal nondigit in seq
return False
ptr2 = 2
while (ucs[ptr2].isdigit()):
ptr2 += 1
# multi-attribute SGR '[01;02(..)'(m|H)
n_tries = 0
while ptr2 < slen and ucs[ptr2] == ';' and n_tries < 64:
n_tries += 1
ptr2 += 1
try:
while (ucs[ptr2].isdigit()):
ptr2 += 1
if ucs[ptr2] == 'H':
# 'H' pos,
return True
elif ucs[ptr2] == 'm':
# 'm' color;attr
return False
elif ucs[ptr2] == ';':
# multi-attribute SGR
continue
except IndexError:
# out-of-range in multi-attribute SGR
return False
# illegal multi-attribtue SGR
return False
if ptr2 >= slen:
# unfinished sequence, hrm ..
return False
elif ucs[ptr2] in u'ABCD':
# numeric up, down, right, left
return True
elif ucs[ptr2] in u'KJ':
# clear_bol is 1K; clear_eos is 2J
return False
elif ucs[ptr2] == 'm':
# normal
return False
# illegal single value, UNKNOWN
return False
def _seqlen(ucs):
"""
_seqlen(S) -> integer
Returns non-zero for string S that begins with an ansi sequence, with
value of bytes until sequence is complete. Use as a 'next' pointer to
skip past sequences.
"""
# pylint: disable=R0911,R0912
# Too many return statements (19/6)
# Too many branches (22/12)
# it is regretable that this duplicates much of is_movement, but
# they do serve different means .. again, more REGEX would help
# readability.
slen = unicode.__len__(ucs)
esc = curses.ascii.ESC
if 0 == slen:
return 0 # empty string
elif ucs[0] != unichr(esc):
return 0 # not a sequence
elif 1 == slen:
return 0 # just esc,
elif ucs[1] == u'c':
return 2 # reset
elif 2 == slen:
return 0 # not a sequence
elif (ucs[1], ucs[2]) == (u'#', u'8'):
return 3 # fill screen (DEC)
elif _ANSI_CODEPAGE.match(ucs) or _ANSI_WONTMOVE.match(ucs):
return 3
elif _ANSI_WILLMOVE.match(ucs):
return 3
elif ucs[1] == '[':
# all sequences are at least 4 (\033,[,0,m)
if slen < 4:
# not a sequence !?
return 0
elif ucs[2] == '?':
# CSI + '?25(h|l)' # show|hide
ptr2 = 3
while (ucs[ptr2].isdigit()):
ptr2 += 1
if (ucs[ptr2]) == ';':
# u'\x1b[?12;25h' cvvis
ptr2 += 1
if not ucs[ptr2] in u'hl':
# ? followed illegaly, UNKNOWN
return 0
return ptr2 + 1
# SGR
elif ucs[2].isdigit() or ucs[2] == ';':
ptr2 = 2
while (ucs[ptr2].isdigit()):
ptr2 += 1
if ptr2 == unicode.__len__(ucs):
return 0
# multi-attribute SGR '[01;02(..)'(m|H)
while ucs[ptr2] == ';':
ptr2 += 1
if ptr2 == unicode.__len__(ucs):
return 0
try:
while (ucs[ptr2].isdigit()):
ptr2 += 1
except IndexError:
return 0
if ucs[ptr2] in u'Hm':
return ptr2 + 1
elif ucs[ptr2] == ';':
# multi-attribute SGR
continue
# 'illegal multi-attribute sgr'
return 0
# single attribute SGT '[01(A|B|etc)'
if ucs[ptr2] in u'ABCDEFGJKSTHm':
# single attribute,
# up/down/right/left/bnl/bpl,pos,cls,cl,
# pgup,pgdown,color,attribute.
return ptr2 + 1
# illegal single value
return 0
# illegal nondigit
return 0
# unknown...
return 0
#!/usr/bin/env python
import contextlib
import blessings
import random
import sys
"""Human tests; made interesting with various 'video games' """
def main():
play_whack_a_key()
play_newtons_nightmare()
def play_newtons_nightmare():
"""
Demonstration of animation and 'movement'.
Go ahead, make yourself a rouge-like :-)
"""
term = blessings.Terminal()
n_balls = 6
tail_length = 10
def newball():
ball = {
'x': float(random.randint(1, term.width)),
'y': float(random.randint(1, term.height)),
'x_velocity': 1.0 - (float(random.randint(1, 20)) * .1),
'y_velocity': 1.0 - (float(random.randint(1, 20)) * .1),
'color': random.randint(1, 7),
'tail': [],
}
ball['x_pos'] = ball['y_pos'] = -1
return ball
balls = list()
for n in range(n_balls):
balls.append (newball())
gravity_x = term.width / 2
gravity_y = term.height / 2
gravity_xs = 0.0
gravity_ys = 0.0
def cycle(ball):
if ball['x'] > gravity_x:
ball['x_velocity'] -= 0.01
else:
ball['x_velocity'] += 0.01
if ball['y'] > gravity_y:
ball['y_velocity'] -= 0.01
else:
ball['y_velocity'] += 0.01
ball['x_velocity'] = max(ball['x_velocity'], -1.0)
ball['x_velocity'] = min(ball['x_velocity'], 1.0)
ball['y_velocity'] = max(ball['y_velocity'], -1.0)
ball['y_velocity'] = min(ball['y_velocity'], 1.0)
return ball
def chk_die(balls):
for ball in balls:
if (ball['x_pos'] == int(gravity_x)
and ball['y_pos'] == int(gravity_y)):
return True
def step(balls):
for ball in balls:
ball = cycle(ball)
xv = ball['x_velocity']
yv = ball['y_velocity']
ball['x'] += xv
ball['y'] += yv
return balls
def refresh(term, balls):
# erase last gravity glpyh
sys.stdout.write(' ')
def outofrange(ball):
return (ball['y_pos'] < 0
or ball['y_pos'] > term.height
or ball['x_pos'] < 0
or ball['x_pos'] > term.width)
def erase(ball):
sys.stdout.write(term.move(ball['y_pos'], ball['x_pos']))
sys.stdout.write(term.color(ball['color']))
sys.stdout.write(term.bold)
sys.stdout.write('.')
sys.stdout.write(term.normal)
def draw(ball):
sys.stdout.write(term.color(ball['color']))
sys.stdout.write(term.reverse)
sys.stdout.write(term.move(ball['y_pos'], ball['x_pos']) + u'*')
sys.stdout.write(term.normal)
for ball in balls:
sx = int(ball['x'])
sy = int(ball['y'])
if ball['x_pos'] != sx or ball['y_pos'] != sy:
# erase old ball
if not outofrange(ball):
erase(ball)
# update position and draw
ball['x_pos'] = sx
ball['y_pos'] = sy
ball['tail'].insert(0, (sy, sx))
# erase last tail-end
if len(ball['tail']) > tail_length:
y, x = ball['tail'].pop()
if (x >= 0 and x <= term.width
and y >= 0 and y <= term.height):
sys.stdout.write(term.move(y, x))
sys.stdout.write(' ')
if not outofrange(ball):
draw(ball)
sys.stdout.write(term.move(int(gravity_y), int(gravity_x)))
sys.stdout.write('+\b')
sys.stdout.flush()
delay = 0.05
score = 0
with contextlib.nested(term.hidden_cursor(), term.cbreak()):
sys.stdout.write(term.clear + term.home)
while True:
score += 1
if 0 == (score % 50):
balls.append (newball())
delay = max(delay - 0.00001, 0.01)
balls = step(balls)
gravity_x += gravity_xs
gravity_y += gravity_ys
if gravity_x >= (term.width - 2) or gravity_x <= 0:
gravity_xs *= -1
gravity_x = min(gravity_x, term.width - 2)
gravity_x = max(gravity_x, 0)
if gravity_y >= (term.height - 1) or gravity_y <= 0:
gravity_ys *= -1
gravity_y = min(gravity_y, term.height - 1)
gravity_y = max(gravity_y, 0)
refresh(term, balls)
if chk_die(balls):
sys.stdout.write(term.move(int(gravity_y), int(gravity_x)))
for n in range(1, 20):
if 0 == (n % 2):
sys.stdout.write(term.white_on_red)
else:
sys.stdout.write(term.red_on_white)
sys.stdout.write('*\b')
term.inkey(0.1)
sys.stdout.flush()
sys.stdout.write(term.normal)
break
inp = term.inkey(delay)
if inp is None:
continue
if inp in (u'q', 'Q'):
break
if (inp.code == term.KEY_UP and gravity_y >= 1.0
and gravity_ys >= -1.5):
gravity_ys -= 0.2
elif inp.code == term.KEY_SUP and gravity_y >= 2.0:
gravity_y -= 2.0
gravity_ys = 0
elif (inp.code == term.KEY_DOWN and gravity_y <= (term.height - 1)
and gravity_ys <= 1.5):
gravity_ys += 0.2
elif inp.code == term.KEY_SDOWN and gravity_y <= (term.height - 2):
gravity_y += 2.0
gravity_ys = 0
elif (inp.code == term.KEY_LEFT and gravity_x >= 1.0
and gravity_xs >= -1.5):
gravity_xs -= 0.3
elif inp.code == term.KEY_SLEFT and gravity_x >= 3.0:
gravity_x -= 3.0
gravity_xs = 0
elif (inp.code == term.KEY_RIGHT and gravity_x <= (term.width - 1)
and gravity_xs <= 1.5):
gravity_xs += 0.3
elif inp.code == term.KEY_SRIGHT and gravity_x <= (term.width - 3):
gravity_x += 3.0
gravity_xs = 0
print term.move(term.height - 4, 0)
print term.clear_eol + 'Your final score was', score
print term.clear_eol + 'press any key'
print term.clear_eol,
sys.stdout.flush()
term.inkey()
def play_whack_a_key():
"""
Displays all known key capabilities that may match the terminal.
As each key is pressed on input, it is lit up and points are scored.
"""
term = blessings.Terminal()
score = 0
level = 0
hit_highbit = 0
hit_unicode = 0
dirty = True
def refresh(term, board, level, score, inp):
sys.stdout.write(term.home + term.clear)
level_color = level % 7
if level_color == 0:
level_color = 4
for keycode, attr in board.iteritems():
sys.stdout.write(term.move(attr['row'], attr['column'])
+ term.color(level_color)
+ (term.reverse if attr['hit'] else term.bold)
+ keycode + term.normal)
sys.stdout.write(term.move(term.height, 0)
+ 'level: %s score: %s' % (level, score,))
sys.stdout.write(' %r, %s, %s' % (inp,
inp.code if inp is not None else None,
inp.name if inp is not None else None, ))
sys.stdout.flush()
def build_gameboard(term):
column, row = 0, 0
board = dict()
spacing = 2
for keycode in term._keycodes:
if keycode.startswith('KEY_F') and keycode[-1].isdigit():
p = 0
while not keycode[p].isdigit():
p += 1
if int(keycode[p:]) > 24:
continue
if column + len(keycode) + (spacing * 2) >= term.width:
column = 0
row += 1
board[keycode] = { 'column': column,
'row': row,
'hit': 0,
}
column += len(keycode) + (spacing * 2)
if row >= term.height:
sys.stderr.write('cheater!\n')
break
return board
def add_score(score, pts, level):
lvl_multiplier = 10
score += pts
if 0 == (score % (pts * lvl_multiplier)):
level += 1
return score, level
gb = build_gameboard(term)
with term.cbreak():
inp = None
while True:
if dirty:
refresh(term, gb, level, score, inp)
dirty = False
inp = term.inkey(timeout=5.0)
dirty = True
if inp is None:
dirty = False
continue
if inp in (u'q', 'Q'):
break
if (inp.is_sequence and
inp.name in gb and
0 == gb[inp.name]['hit']):
gb[inp.name]['hit'] = 1
score, level = add_score (score, 100, level)
elif inp.code > 128 and inp.code < 256:
hit_highbit += 1
if hit_highbit < 5:
score, level = add_score (score, 100, level)
elif not inp.is_sequence and inp.code > 256:
hit_unicode += 1
if hit_unicode < 5:
score, level = add_score (score, 100, level)
print term.move(term.height - 4, 0)
print term.clear_eol + 'Your final score was', score, 'at level', level
if hit_highbit > 0:
print term.clear_eol + 'You hit', hit_highbit,
print 'extended ascii characters'
if hit_unicode > 0:
print term.clear_eol + 'You hit', hit_unicode, 'unicode characters'
print term.clear_eol + 'press any key'
print term.clear_eol,
sys.stdout.flush()
term.inkey()
if __name__ == '__main__':
main()
#!/usr/bin/env python
import contextlib
import blessings
import string
import random
import time
import sys
import re
"""Human tests; made interesting with various 'video games' """
def main():
play_boxes()
play_pong()
MOUSE_REPORT = re.compile('\x1b\[(\d+);(\d+);(\d+)M')
MOUSE_BUTTON_LEFT = 0
MOUSE_BUTTON_RIGHT = 1
MOUSE_BUTTON_MIDDLE = 2
MOUSE_BUTTON_RELEASED = 3
MOUSE_SCROLL_REVERSE = 64
MOUSE_SCROLL_FORWARD = 65
class MouseEvent(object):
code = None
x = None
y = None
def get_mouseaction(buf):
""" Seek 'buf' for mouse event terminal sequence, returning MouseEvent instance. """
action = MouseEvent()
event = MOUSE_REPORT.match(buf)
if event is None:
return None
code, x, y = event.groups()
code = int(code) - 32
x, y = int(x) - 1, int(y) - 1
action.code, action.x, action.y = code, x, y
return action
def get_mousebytes(term, buf):
inp = None
while inp != u'M':
inp = term.inkey(timeout=0.1)
if inp is not None:
buf.append(inp)
return ''.join(buf)
def play_pong():
"""
This example plays a game of pong with the mouse scroll wheel.
"""
term = blessings.Terminal()
paddles = {}
def newball():
return {
'x': float(term.width) / 2,
'y': float(term.height) / 2,
'xv': random.choice([-1, 1]),
'yv': 1 - random.randint(1, 20) * .1,
'tail': [(term.width / 2, term.height / 2),],
}
ball = newball()
paddle_height = max(4, term.height / 7)
paddle_width = max(2, term.width / 30)
tracer_length = term.width / 10
paddle_ymax = (term.height - paddle_height - 1)
paddles['left'] = {
'x': 1,
'y': float(ball['y']),
}
paddles['right'] = {
'x': term.width - paddle_width,
'y': ball['y'],
}
delay = 0.03
score_player, score_computer = 0, 0
difficulty = 0.25
def draw_score():
sys.stdout.write(term.move(term.height, paddle_width + 1))
sys.stdout.write(term.green('score'))
sys.stdout.write(term.bold_green(': '))
sys.stdout.write(term.bold_white(str(score_computer)))
sys.stdout.write(term.move(term.height,
term.width - (8 + paddle_width)))
sys.stdout.write(term.green('score'))
sys.stdout.write(term.bold_green(': '))
sys.stdout.write(term.bold_white(str(score_player)))
def refresh():
sys.stdout.write(term.home + term.clear)
draw_paddle(paddles['left'])
draw_paddle(paddles['right'])
draw_score()
draw_ball(ball)
msg = 'Mouse scrollwheel, KEY_UP, or KEY_DOWN moves paddle.'
sys.stdout.write(term.move(term.height-1, (term.width/2)-(len(msg)/2)))
sys.stdout.write(msg)
sys.stdout.flush()
def draw_paddle(paddle, erase=False):
""" Display bounding box from top-left to bottom-right. """
if not erase:
sys.stdout.write(term.reverse)
sys.stdout.write(term.green)
for row in range(int(paddle['y']),
int(paddle['y']) + paddle_height + 1):
sys.stdout.write(term.move(row, paddle['x']))
sys.stdout.write(' ' * paddle_width)
sys.stdout.write(term.normal)
def move_paddle(paddle, y):
# bounds checking
y = min(y, paddle_ymax)
y = max(0, y)
dirty = int(paddle['y']) != int(y)
if dirty:
draw_paddle(paddle, erase=True)
paddle['y'] = y
if dirty:
draw_paddle(paddle)
def leave_tail(x, y):
sys.stdout.write(term.move(y, x))
sys.stdout.write(term.bold_white(u'.'))
def erase_tail(x, y):
sys.stdout.write(term.move(y, x))
sys.stdout.write(u' ')
def draw_ball(ball):
x, y = int(ball['x']), int(ball['y'])
if (y >= 0 and y < term.height and
x >= 0 and x < term.width):
sys.stdout.write(term.move(y, x))
sys.stdout.write(term.green + term.reverse)
sys.stdout.write(u' ')
sys.stdout.write(term.normal)
def move_ball(ball):
x, y = int(ball['x']), int(ball['y'])
ball['x'] += ball['xv']
ball['y'] += ball['yv']
if x != int(ball['x']) or y != int(ball['y']):
leave_tail(x, y)
ball['tail'].insert(0, (x, y))
if len(ball['tail']) > tracer_length:
erase_tail(*ball['tail'].pop())
draw_ball(ball)
def move_paddle_ai(paddle, ball):
y, x = ball['y'], ball['x']
if y > paddle['y'] + paddle_height / 2:
move_paddle(paddle, paddle['y'] + (min(difficulty, 1)))
elif y < paddle['y'] + paddle_height / 2:
move_paddle(paddle, paddle['y'] - (min(difficulty, 1)))
def bounce_ball(ball):
if ball['y'] <= 1 and ball['yv'] < 0:
ball['yv'] *= -1
elif ball['y'] >= (term.height - 2) and ball['yv'] > 0:
ball['yv'] *= -1
def hit_detect(ball, paddles):
if (ball['y'] >= paddles['right']['y'] and
ball['y'] <= paddles['right']['y'] + (paddle_height + 1) and
ball['x'] >= (paddles['right']['x'] - 1) and
ball['xv'] > 0):
# bounce off player's paddle
ball['xv'] *= -1
diff = ball['y'] - (paddles['right']['y'] + (paddle_height / 2))
ball['yv'] += diff / paddle_height
return True
elif (ball['y'] >= paddles['left']['y'] and
ball['y'] <= paddles['left']['y'] + (paddle_height + 1) and
ball['x'] <= paddles['left']['x'] + (paddle_width + 1) and
ball['xv'] < 0):
# bounce off computer's paddle
ball['xv'] *= -1
diff = ball['y'] - (paddles['left']['y'] + (paddle_height / 2))
ball['yv'] += diff / paddle_height
return True
def die_detect(ball):
# returns 1: die on left, 2: die on right, 0: no death
if ball['x'] <= 0:
return 1
elif ball['x'] >= term.width:
return 2
return 0
with contextlib.nested(
term.cbreak(),
term.mouse_tracking(),
term.hidden_cursor()):
inp = None
refresh()
dirty = time.time()
term.inkey() # wait for player start
while score_player < 20 and score_computer < 20:
if dirty == True:
refresh()
dirty = time.time()
if time.time() - dirty >= delay:
move_ball(ball)
bounce_ball(ball)
hit_detect(ball, paddles)
move_paddle_ai(paddles['left'], ball)
dirty = time.time()
if die_detect(ball) == 1:
score_player += 1
ball = newball()
difficulty += .01
dirty = True
elif die_detect(ball) == 2:
score_computer += 1
ball = newball()
dirty = True
sys.stdout.flush()
inp = term.inkey(delay)
if inp is None:
continue
if inp in (u'q', 'Q'):
break
if inp == '\x1b' and not inp.is_sequence:
# we received only a single '\x1b' from inkey().
# It could be a single escape key. Or, it could be a sequence
# not handled by inkey; here, mouse scroll event is saught
# after.
buf = get_mousebytes(term, [inp,])
mousemove = get_mouseaction(buf)
if mousemove is None:
print term.move(term.height-2, 0) + term.clear_eol
print term.move(term.height-1, 0) + (
"You inputted a sequence I didn't "
"particulary like.")
break
if mousemove.code == MOUSE_SCROLL_FORWARD:
move_paddle(paddles['right'], paddles['right']['y'] + 1)
elif mousemove.code == MOUSE_SCROLL_REVERSE:
move_paddle(paddles['right'], paddles['right']['y'] - 1)
elif inp.code == term.KEY_UP:
move_paddle(paddles['right'], paddles['right']['y'] - int(paddle_height* .7))
elif inp.code == term.KEY_DOWN:
move_paddle(paddles['right'], paddles['right']['y'] + int(paddle_height* .7))
elif inp.code == term.KEY_EXIT:
break
sys.stdout.flush()
sys.stdout.write(term.move(term.height -1, 0))
sys.stdout.write(term.clear_eol)
if score_computer > score_player:
print 'You lost!'
elif score_player > score_computer:
print 'You won!'
def play_boxes():
"""
blessings.inkey does not handle mouse events, as it would require a
more complex interface than Keystroke, and a terse understanding of
all of the available mouse modes.
This example parses mouse reporting events after enabling it with
the mouse_tracking context manager. It reads button down events for
the left mouse button, stores the (x, y), and when the button releases,
draws a bounding box between the two points.
"""
term = blessings.Terminal()
def display_scroll(direction, x, y):
""" Report scrolling at (x,y) position. """
sys.stdout.write(term.move(term.height -3, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.write('scroll %s; %s,%s' % (direction, x, y))
sys.stdout.write(term.move(term.height -2, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.write(term.move(term.height -1, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.flush()
def display_down(x, y):
""" Report mouse button down (x,y) position. """
sys.stdout.write(term.move(term.height -3, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.write(term.move(term.height -2, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.write('button1 down %s,%s' % (x, y))
sys.stdout.write(term.move(term.height -1, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.flush()
def display_release(x, y):
""" Report mouse button release (x,y) position. """
sys.stdout.write(term.move(term.height -3, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.write(term.move(term.height -1, 0))
sys.stdout.write(term.clear_eol)
sys.stdout.write('button1 release %s,%s' % (x, y))
sys.stdout.flush()
def display_box(top, botom, left, right):
""" Display bounding box from top-left to bottom-right. """
idx = top + bottom + left + right
color_value = max(idx % 7, 1)
width = max(1, (right - left))
sys.stdout.write(term.reverse)
sys.stdout.write(term.color(color_value))
for row in range(top, bottom + 1):
ucs = string.printable[idx % len(string.printable)]
sys.stdout.write(term.move(row, left))
sys.stdout.write(ucs * width)
sys.stdout.write(term.normal)
sys.stdout.flush()
with contextlib.nested(term.cbreak(), term.mouse_tracking()):
inp = None
drawing = False
start_col, start_row = -1, -1
print term.clear + term.home + 'click and drag to draw a box'
while True:
inp = term.inkey()
if inp in (u'q', u'Q') or inp.code == term.KEY_EXIT:
break
if inp == '\x1b' and not inp.is_sequence:
buf = get_mousebytes(term, [inp,])
action = MOUSE_REPORT.match(buf)
assert action is not None, (
'Unexpected escape sequence: %r' % (buf,))
action, x, y = action.groups()
# button is offset by 32, except wheel mice buttons 3 & 4,
# which are offset by addtiona 64.
action = int(action) - 32
# Screen dimensions (1,1) starting.
x = int(x) - 1
y = int(y) - 1
if action == MOUSE_BUTTON_LEFT:
drawing = True
start_col, start_row = x, y
display_down(x, y)
elif action == MOUSE_BUTTON_RELEASED and drawing:
left, right = min(x, start_col), max(x, start_col)
top, bottom = min(y, start_row), max(y, start_row)
display_release(x, y)
display_box(top, bottom, left, right)
drawing = False
elif action == MOUSE_SCROLL_FORWARD:
display_scroll('forward', x, y)
elif action == MOUSE_SCROLL_REVERSE:
display_scroll('reverse', x, y)
sys.stdout.flush()
print term.move(term.height - 1, 0)
if __name__ == '__main__':
main()
@ch3pjw
Copy link

ch3pjw commented Oct 24, 2013

Are you intending your assert statements to provide user feedback, or are the just intended for use by the developer using blessings? I had a problem a while back with assert statements being stripped out when people ran with python -o, and ended up replacing many of them with conditional blocks that raised a more specific Exception manually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment