Last active
December 25, 2015 18:09
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 withassert
statements being stripped out when people ran withpython -o
, and ended up replacing many of them with conditional blocks that raised a more specificException
manually.