diff --git a/README.rst b/README.rst index f672672d..1cef6c68 100644 --- a/README.rst +++ b/README.rst @@ -591,32 +591,45 @@ has elapsed, an empty string is returned. A timeout value of 0 is nonblocking. keyboard codes ~~~~~~~~~~~~~~ -The return value of the *Terminal* method ``inkey`` may be inspected for ts property -*is_sequence*. When *True*, it means the value is a *multibyte sequence*, -representing an application key of your terminal. - -The *code* property (int) may then be compared with any of the following -attributes of the *Terminal* instance, which are equivalent to the same -available in `curs_getch(3)`_, with the following exceptions: - -* use ``KEY_DELETE`` instead of ``KEY_DC`` (chr(127)) -* use ``KEY_INSERT`` instead of ``KEY_IC`` -* use ``KEY_PGUP`` instead of ``KEY_PPAGE`` -* use ``KEY_PGDOWN`` instead of ``KEY_NPAGE`` -* use ``KEY_ESCAPE`` instead of ``KEY_EXIT`` -* use ``KEY_SUP`` instead of ``KEY_SR`` (shift + up) -* use ``KEY_SDOWN`` instead of ``KEY_SF`` (shift + down) - -Additionally, use any of the following common attributes: - -* ``KEY_BACKSPACE`` (chr(8)). -* ``KEY_TAB`` (chr(9)). -* ``KEY_DOWN``, ``KEY_UP``, ``KEY_LEFT``, ``KEY_RIGHT``. -* ``KEY_SLEFT`` (shift + left). -* ``KEY_SRIGHT`` (shift + right). -* ``KEY_HOME``, ``KEY_END``. -* ``KEY_F1`` through ``KEY_F22``. - +The return value of the *Terminal* method ``inkey`` is an instance of the +class ``Keystroke``, and may be inspected for its property *is_sequence* +(bool). When *True*, it means the value is a *multibyte sequence*, +representing a special non-alphanumeric key of your keyboard. + +The *code* property (int) may then be compared with attributes of the +*Terminal* instance, which are equivalent to the same of those listed +by `curs_getch(3)`_ or the curses_ module, with the following helpful +aliases: + +* use ``KEY_DELETE`` for ``KEY_DC`` (chr(127)). +* use ``KEY_TAB`` for chr(9). +* use ``KEY_INSERT`` for ``KEY_IC``. +* use ``KEY_PGUP`` for ``KEY_PPAGE``. +* use ``KEY_PGDOWN`` for ``KEY_NPAGE``. +* use ``KEY_ESCAPE`` for ``KEY_EXIT``. +* use ``KEY_SUP`` for ``KEY_SR`` (shift + up). +* use ``KEY_SDOWN`` for ``KEY_SF`` (shift + down). +* use ``KEY_DOWN_LEFT`` for ``KEY_C1`` (keypad lower-left). +* use ``KEY_UP_RIGHT`` for ``KEY_A1`` (keypad upper-left). +* use ``KEY_DOWN_RIGHT`` for ``KEY_C3`` (keypad lower-left). +* use ``KEY_UP_RIGHT`` for ``KEY_A3`` (keypad lower-right). +* use ``KEY_CENTER`` for ``KEY_B2`` (keypad center). +* use ``KEY_BEGIN`` for ``KEY_BEG``. + +The *name* property of the return value of ``inkey()`` will prefer +these aliases over the built-in curses_ names. + +The following are not available in the curses_ module, but provided +for distinguishing a keypress of those keypad keys where num lock is +enabled and the ``keypad()`` context manager is used: + +* ``KEY_KP_MULTIPLY`` +* ``KEY_KP_ADD`` +* ``KEY_KP_SEPARATOR`` +* ``KEY_KP_SUBTRACT`` +* ``KEY_KP_DECIMAL`` +* ``KEY_KP_DIVIDE`` +* ``KEY_KP_0`` through ``KEY_KP_9`` Shopping List ============= @@ -644,8 +657,10 @@ detail and behavior in edge cases make a difference. Here are some ways Blessed does not provide... -* Native color support on the Windows command prompt. However, it should work - when used in concert with colorama_. Patches welcome! +* Native color support on the Windows command prompt. A PDCurses_ build + of python for windows provides only partial support at this time -- there + are plans to merge with the ansi_ module in concert with colorama_ to + resolve this. Patches welcome! Devlopers, Bugs @@ -656,7 +671,9 @@ Bugs or suggestions? Visit the `issue tracker`_. For patches, please construct a test case if possible. -To test, install and execute python package command ``tox``. +To test, execute ``./setup.py develop`` followed by command ``tox``. + +Pull requests are tested by Travis-CI. License @@ -671,6 +688,11 @@ Version History 1.9 * workaround: ignore 'tparm() returned NULL', this occurs on win32 platforms using PDCurses_ where tparm() is not implemented. + * enhancement: new context manager ``keypad()``, which enables + keypad application keys such as the diagonal keys on the numpad. + * bugfix: translate keypad application keys correctly to their + diagonal movement directions ``KEY_LL``, ``KEY_LR``, ``KEY_UL``, + ``KEY_LR``, and ``KEY_CENTER``. 1.8 * enhancement: export keyboard-read function as public method ``getch()``, @@ -689,7 +711,7 @@ Version History * enhancement: better support for detecting the length or sequences of externally-generated *ecma-48* codes when using ``xterm`` or ``aixterm``. * bugfix: if ``locale.getpreferredencoding()`` returns empty string or an - encoding that is not a valid codec for ``codecs.getincrementaldecoder``, + encoding that is not a valid encoding for ``codecs.getincrementaldecoder``, fallback to ascii and emit a warning. * bugfix: ensure ``FormattingString`` and ``ParameterizingString`` may be pickled. @@ -703,13 +725,13 @@ Version History * Forked github project `erikrose/blessings`_ to `jquast/blessed`_, this project was previously known as **blessings** version 1.6 and prior. * introduced: context manager ``cbreak()`` and ``raw()``, which is equivalent - to ``tty.setcbreak()`` and ``tty.setraw()``, allowing input from stdin to be - read as each key is pressed. + to ``tty.setcbreak()`` and ``tty.setraw()``, allowing input from stdin to + be read as each key is pressed. * introduced: ``inkey()`` and ``kbhit()``, which will return 1 or more characters as a unicode sequence, with attributes ``.code`` and ``.name``, with value non-``None`` when a multibyte sequence is received, allowing - application keys (such as UP/DOWN) to be detected. Optional value ``timeout`` - allows timed asynchronous polling or blocking. + application keys (such as UP/DOWN) to be detected. Optional value + ``timeout`` allows timed asynchronous polling or blocking. * introduced: ``center()``, ``rjust()``, ``ljust()``, ``strip()``, and ``strip_seqs()`` methods. Allows text containing sequences to be aligned to screen, or ``width`` specified. diff --git a/bin/dumb_fse.py b/bin/dumb_fse.py deleted file mode 100755 index 462e3d15..00000000 --- a/bin/dumb_fse.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python -# Dumb full-screen editor. It doesn't save anything but to the screen. -# -# "Why wont python let me read memory -# from screen like assembler? That's dumb." -hellbeard -from __future__ import division, print_function -import collections -import functools -from blessed import Terminal - -echo_xy = lambda cursor, text: functools.partial( - print, end='', flush=True)(cursor.term.move(cursor.y, cursor.x) + text) - -Cursor = collections.namedtuple('Point', ('y', 'x', 'term')) - -above = lambda b, n: Cursor( - max(0, b.y - n), b.x, b.term) -below = lambda b, n: Cursor( - min(b.term.height - 1, b.y + n), b.x, b.term) -right_of = lambda b, n: Cursor( - b.y, min(b.term.width - 1, b.x + n), b.term) -left_of = lambda b, n: Cursor( - b.y, max(0, b.x - n), b.term) -home = lambda b: Cursor( - b.y, 1, b.term) - -lookup_move = lambda inp_code, b: { - # arrows - b.term.KEY_LEFT: left_of(b, 1), - b.term.KEY_RIGHT: right_of(b, 1), - b.term.KEY_DOWN: below(b, 1), - b.term.KEY_UP: above(b, 1), - # shift + arrows - b.term.KEY_SLEFT: left_of(b, 10), - b.term.KEY_SRIGHT: right_of(b, 10), - b.term.KEY_SDOWN: below(b, 10), - b.term.KEY_SUP: above(b, 10), - # carriage return - b.term.KEY_ENTER: home(below(b, 1)), - b.term.KEY_HOME: home(b), -}.get(inp_code, b) - -term = Terminal() -csr = Cursor(1, 1, term) -with term.hidden_cursor(), term.raw(), term.location(), term.fullscreen(): - inp = None - while True: - echo_xy(csr, term.reverse(u' ')) - inp = term.inkey() - if inp.code == term.KEY_ESCAPE or inp == chr(3): - break - echo_xy(csr, u' ') - n_csr = lookup_move(inp.code, csr) - if n_csr != csr: - echo_xy(n_csr, u' ') - csr = n_csr - elif not inp.is_sequence: - echo_xy(csr, inp) - csr = right_of(csr, 1) diff --git a/bin/editor.py b/bin/editor.py new file mode 100755 index 00000000..4dff2b70 --- /dev/null +++ b/bin/editor.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# Dumb full-screen editor. It doesn't save anything but to the screen. +# +# "Why wont python let me read memory +# from screen like assembler? That's dumb." -hellbeard +# +# This program makes example how to deal with a keypad for directional +# movement, with both numlock on and off. +from __future__ import division, print_function +import collections +import functools +from blessed import Terminal + +echo = lambda text: ( + functools.partial(print, end='', flush=True)(text)) + +echo_yx = lambda cursor, text: ( + echo(cursor.term.move(cursor.y, cursor.x) + text)) + +Cursor = collections.namedtuple('Point', ('y', 'x', 'term')) + +above = lambda csr, n: ( + Cursor(y=max(0, csr.y - n), + x=csr.x, + term=csr.term)) + +below = lambda csr, n: ( + Cursor(y=min(csr.term.height - 1, csr.y + n), + x=csr.x, + term=csr.term)) + +right_of = lambda csr, n: ( + Cursor(y=csr.y, + x=min(csr.term.width - 1, csr.x + n), + term=csr.term)) + +left_of = lambda csr, n: ( + Cursor(y=csr.y, + x=max(0, csr.x - n), + term=csr.term)) + +home = lambda csr: ( + Cursor(y=csr.y, + x=0, + term=csr.term)) + +end = lambda csr: ( + Cursor(y=csr.y, + x=csr.term.width - 1, + term=csr.term)) + +bottom = lambda csr: ( + Cursor(y=csr.term.height - 1, + x=csr.x, + term=csr.term)) + +top = lambda csr: ( + Cursor(y=1, + x=csr.x, + term=csr.term)) + +center = lambda csr: Cursor( + csr.term.height // 2, + csr.term.width // 2, + csr.term) + + +lookup_move = lambda inp_code, csr, term: { + # arrows, including angled directionals + csr.term.KEY_END: below(left_of(csr, 1), 1), + csr.term.KEY_KP_1: below(left_of(csr, 1), 1), + + csr.term.KEY_DOWN: below(csr, 1), + csr.term.KEY_KP_2: below(csr, 1), + + csr.term.KEY_PGDOWN: below(right_of(csr, 1), 1), + csr.term.KEY_LR: below(right_of(csr, 1), 1), + csr.term.KEY_KP_3: below(right_of(csr, 1), 1), + + csr.term.KEY_LEFT: left_of(csr, 1), + csr.term.KEY_KP_4: left_of(csr, 1), + + csr.term.KEY_CENTER: center(csr), + csr.term.KEY_KP_5: center(csr), + + csr.term.KEY_RIGHT: right_of(csr, 1), + csr.term.KEY_KP_6: right_of(csr, 1), + + csr.term.KEY_HOME: above(left_of(csr, 1), 1), + csr.term.KEY_KP_7: above(left_of(csr, 1), 1), + + csr.term.KEY_UP: above(csr, 1), + csr.term.KEY_KP_8: above(csr, 1), + + csr.term.KEY_PGUP: above(right_of(csr, 1), 1), + csr.term.KEY_KP_9: above(right_of(csr, 1), 1), + + # shift + arrows + csr.term.KEY_SLEFT: left_of(csr, 10), + csr.term.KEY_SRIGHT: right_of(csr, 10), + csr.term.KEY_SDOWN: below(csr, 10), + csr.term.KEY_SUP: above(csr, 10), + + # carriage return + csr.term.KEY_ENTER: home(below(csr, 1)), +}.get(inp_code, csr) + + +def readline(term, width=20): + # a rudimentary readline function + string = u'' + while True: + inp = term.inkey() + if inp.code == term.KEY_ENTER: + break + elif inp.code == term.KEY_ESCAPE or inp == chr(3): + string = None + break + elif not inp.is_sequence and len(string) < width: + string += inp + echo(inp) + elif inp.code in (term.KEY_BACKSPACE, term.KEY_DELETE): + string = string[:-1] + echo('\b \b') + return string + + +def save(screen, fname): + if not fname: + return + with open(fname, 'w') as fp: + cur_row = cur_col = 0 + for (row, col) in sorted(screen): + char = screen[(row, col)] + while row != cur_row: + cur_row += 1 + cur_col = 0 + fp.write(u'\n') + while col > cur_col: + cur_col += 1 + fp.write(u' ') + fp.write(char) + cur_col += 1 + fp.write(u'\n') + + +def redraw(term, screen, start=None, end=None): + if start is None and end is None: + echo(term.clear) + start, end = (Cursor(y=min([y for (y, x) in screen or [(0, 0)]]), + x=min([x for (y, x) in screen or [(0, 0)]]), + term=term), + Cursor(y=max([y for (y, x) in screen or [(0, 0)]]), + x=max([x for (y, x) in screen or [(0, 0)]]), + term=term)) + lastcol, lastrow = -1, -1 + for row, col in sorted(screen): + if (row >= start.y and row <= end.y and + col >= start.x and col <= end.x): + if col >= term.width or row >= term.height: + # out of bounds + continue + if not (row == lastrow and col == lastcol + 1): + # use cursor movement + echo_yx(Cursor(row, col, term), screen[row, col]) + else: + # just write past last one + echo(screen[row, col]) + + +def main(): + term = Terminal() + csr = Cursor(0, 0, term) + screen = {} + with term.hidden_cursor(), \ + term.raw(), \ + term.location(), \ + term.fullscreen(), \ + term.keypad(): + inp = None + while True: + echo_yx(csr, term.reverse(screen.get((csr.y, csr.x), u' '))) + inp = term.inkey() + + if inp == chr(3): + # ^c exits + break + + elif inp == chr(19): + # ^s saves + echo_yx(home(bottom(csr)), + term.ljust(term.bold_white('Filename: '))) + echo_yx(right_of(home(bottom(csr)), len('Filename: ')), u'') + save(screen, readline(term)) + echo_yx(home(bottom(csr)), term.clear_eol) + redraw(term=term, screen=screen, + start=home(bottom(csr)), + end=end(bottom(csr))) + continue + + elif inp == chr(12): + # ^l refreshes + redraw(term=term, screen=screen) + + n_csr = lookup_move(inp.code, csr, term) + if n_csr != csr: + # erase old cursor, + echo_yx(csr, screen.get((csr.y, csr.x), u' ')) + csr = n_csr + + elif not inp.is_sequence and inp.isprintable(): + echo_yx(csr, inp) + screen[(csr.y, csr.x)] = inp.__str__() + n_csr = right_of(csr, 1) + if n_csr == csr: + # wrap around margin + n_csr = home(below(csr, 1)) + csr = n_csr + +if __name__ == '__main__': + main() diff --git a/bin/test_keyboard_keys.py b/bin/keymatrix.py similarity index 95% rename from bin/test_keyboard_keys.py rename to bin/keymatrix.py index af258139..daaad990 100755 --- a/bin/test_keyboard_keys.py +++ b/bin/keymatrix.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import division from blessed import Terminal import sys @@ -31,11 +32,11 @@ def refresh(term, board, level, score, inp): sys.stdout.flush() if bottom >= (term.height - 5): sys.stderr.write( - '\n' * (term.height / 2) + + ('\n' * (term.height // 2)) + term.center(term.red_underline('cheater!')) + '\n') sys.stderr.write( term.center("(use a larger screen)") + - '\n' * (term.height / 2)) + ('\n' * (term.height // 2))) sys.exit(1) for row, inp in enumerate(inps[(term.height - (bottom + 2)) * -1:]): sys.stdout.write(term.move(bottom + row+1)) @@ -72,9 +73,9 @@ def add_score(score, pts, level): gb = build_gameboard(term) inps = [] - with term.raw(): + with term.raw(), term.keypad(), term.location(): inp = term.inkey(timeout=0) - while inp.upper() != 'Q': + while inp != chr(3): if dirty: refresh(term, gb, level, score, inps) dirty = False diff --git a/blessed/keyboard.py b/blessed/keyboard.py index 0d60582b..ec02e219 100644 --- a/blessed/keyboard.py +++ b/blessed/keyboard.py @@ -11,7 +11,7 @@ if hasattr(collections, 'OrderedDict'): OrderedDict = collections.OrderedDict else: - # python 2.6 + # python 2.6 requires 3rd party library import ordereddict OrderedDict = ordereddict.OrderedDict @@ -21,9 +21,34 @@ if keyname.startswith('KEY_')) ) -# Inject missing KEY_TAB -if not hasattr(curses, 'KEY_TAB'): - curses.KEY_TAB = max(get_curses_keycodes().values()) + 1 +# override a few curses constants with easier mnemonics, +# there may only be a 1:1 mapping, so for those who desire +# to use 'KEY_DC' from, perhaps, ported code, recommend +# that they simply compare with curses.KEY_DC. +CURSES_KEYCODE_OVERRIDE_MIXIN = ( + ('KEY_DELETE', curses.KEY_DC), + ('KEY_INSERT', curses.KEY_IC), + ('KEY_PGUP', curses.KEY_PPAGE), + ('KEY_PGDOWN', curses.KEY_NPAGE), + ('KEY_ESCAPE', curses.KEY_EXIT), + ('KEY_SUP', curses.KEY_SR), + ('KEY_SDOWN', curses.KEY_SF), + ('KEY_UP_LEFT', curses.KEY_A1), + ('KEY_UP_RIGHT', curses.KEY_A3), + ('KEY_CENTER', curses.KEY_B2), + ('KEY_BEGIN', curses.KEY_BEG), +) + +# Inject KEY_{names} that we think would be useful, there are no curses +# definitions for the keypad keys. We need keys that generate multibyte +# sequences, though it is useful to have some aliases for basic control +# characters such as TAB. +_lastval = max(get_curses_keycodes().values()) +for key in ('TAB', 'KP_MULTIPLY', 'KP_ADD', 'KP_SEPARATOR', 'KP_SUBTRACT', + 'KP_DECIMAL', 'KP_DIVIDE', 'KP_EQUAL', 'KP_0', 'KP_1', 'KP_2', + 'KP_3', 'KP_4', 'KP_5', 'KP_6', 'KP_7', 'KP_8', 'KP_9'): + _lastval += 1 + setattr(curses, 'KEY_{0}'.format(key), _lastval) class Keystroke(unicode): @@ -156,27 +181,22 @@ def resolve_sequence(text, mapper, codes): return Keystroke(ucs=sequence, code=code, name=codes[code]) return Keystroke(ucs=text and text[0] or u'') -# override a few curses constants with easier mnemonics, -# there may only be a 1:1 mapping, so for those who desire -# to use 'KEY_DC' from, perhaps, ported code, recommend -# that they simply compare with curses.KEY_DC. -CURSES_KEYCODE_OVERRIDE_MIXIN = ( - ('KEY_DELETE', curses.KEY_DC), - ('KEY_INSERT', curses.KEY_IC), - ('KEY_PGUP', curses.KEY_PPAGE), - ('KEY_PGDOWN', curses.KEY_NPAGE), - ('KEY_ESCAPE', curses.KEY_EXIT), - ('KEY_SUP', curses.KEY_SR), - ('KEY_SDOWN', curses.KEY_SF), -) - """In a perfect world, terminal emulators would always send exactly what the terminfo(5) capability database plans for them, accordingly by the value of the ``TERM`` name they declare. But this isn't a perfect world. Many vt220-derived terminals, such as those declaring 'xterm', will continue to send vt220 codes instead of -their native-declared codes. This goes for many: rxvt, putty, iTerm.""" +their native-declared codes, for backwards-compatibility. + +This goes for many: rxvt, putty, iTerm. + +These "mixins" are used for *all* terminals, regardless of their type. + +Furthermore, curses does not provide sequences sent by the keypad, +at least, it does not provide a way to distinguish between keypad 0 +and numeric 0. +""" DEFAULT_SEQUENCE_MIXIN = ( # these common control characters (and 127, ctrl+'?') mapped to # an application key definition. @@ -186,38 +206,64 @@ def resolve_sequence(text, mapper, codes): (unichr(9), curses.KEY_TAB), (unichr(27), curses.KEY_EXIT), (unichr(127), curses.KEY_DC), - # vt100 application keys are still sent by xterm & friends, even if - # their reports otherwise; possibly, for compatibility reasons? - (u"\x1bOA", curses.KEY_UP), - (u"\x1bOB", curses.KEY_DOWN), - (u"\x1bOC", curses.KEY_RIGHT), - (u"\x1bOD", curses.KEY_LEFT), - (u"\x1bOH", curses.KEY_HOME), - (u"\x1bOF", curses.KEY_END), - (u"\x1bOP", curses.KEY_F1), - (u"\x1bOQ", curses.KEY_F2), - (u"\x1bOR", curses.KEY_F3), - (u"\x1bOS", curses.KEY_F4), - # typical for vt220-derived terminals, just in case our terminal - # database reported something different, + (u"\x1b[A", curses.KEY_UP), (u"\x1b[B", curses.KEY_DOWN), (u"\x1b[C", curses.KEY_RIGHT), (u"\x1b[D", curses.KEY_LEFT), - (u"\x1b[U", curses.KEY_NPAGE), - (u"\x1b[V", curses.KEY_PPAGE), - (u"\x1b[H", curses.KEY_HOME), (u"\x1b[F", curses.KEY_END), + (u"\x1b[H", curses.KEY_HOME), + # not sure where these are from .. please report (u"\x1b[K", curses.KEY_END), - # atypical, - # (u"\x1bA", curses.KEY_UP), - # (u"\x1bB", curses.KEY_DOWN), - # (u"\x1bC", curses.KEY_RIGHT), - # (u"\x1bD", curses.KEY_LEFT), - # rxvt, - (u"\x1b?r", curses.KEY_DOWN), - (u"\x1b?x", curses.KEY_UP), - (u"\x1b?v", curses.KEY_RIGHT), - (u"\x1b?t", curses.KEY_LEFT), - (u"\x1b[@", curses.KEY_IC), + (u"\x1b[U", curses.KEY_NPAGE), + (u"\x1b[V", curses.KEY_PPAGE), + + # keys sent after term.smkx (keypad_xmit) is emitted, source: + # http://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys + # http://fossies.org/linux/rxvt/doc/rxvtRef.html#KeyCodes + # + # keypad, numlock on + (u"\x1bOM", curses.KEY_ENTER), # return + (u"\x1bOj", curses.KEY_KP_MULTIPLY), # * + (u"\x1bOk", curses.KEY_KP_ADD), # + + (u"\x1bOl", curses.KEY_KP_SEPARATOR), # , + (u"\x1bOm", curses.KEY_KP_SUBTRACT), # - + (u"\x1bOn", curses.KEY_KP_DECIMAL), # . + (u"\x1bOo", curses.KEY_KP_DIVIDE), # / + (u"\x1bOX", curses.KEY_KP_EQUAL), # = + (u"\x1bOp", curses.KEY_KP_0), # 0 + (u"\x1bOq", curses.KEY_KP_1), # 1 + (u"\x1bOr", curses.KEY_KP_2), # 2 + (u"\x1bOs", curses.KEY_KP_3), # 3 + (u"\x1bOt", curses.KEY_KP_4), # 4 + (u"\x1bOu", curses.KEY_KP_5), # 5 + (u"\x1bOv", curses.KEY_KP_6), # 6 + (u"\x1bOw", curses.KEY_KP_7), # 7 + (u"\x1bOx", curses.KEY_KP_8), # 8 + (u"\x1bOy", curses.KEY_KP_9), # 9 + + # + # keypad, numlock off + (u"\x1b[1~", curses.KEY_FIND), # find + (u"\x1b[2~", curses.KEY_IC), # insert (0) + (u"\x1b[3~", curses.KEY_DC), # delete (.), "Execute" + (u"\x1b[4~", curses.KEY_SELECT), # select + (u"\x1b[5~", curses.KEY_PPAGE), # pgup (9) + (u"\x1b[6~", curses.KEY_NPAGE), # pgdown (3) + (u"\x1b[7~", curses.KEY_HOME), # home + (u"\x1b[8~", curses.KEY_END), # end + (u"\x1b[OA", curses.KEY_UP), # up (8) + (u"\x1b[OB", curses.KEY_DOWN), # down (2) + (u"\x1b[OC", curses.KEY_RIGHT), # right (6) + (u"\x1b[OD", curses.KEY_LEFT), # left (4) + (u"\x1b[OF", curses.KEY_END), # end (1) + (u"\x1b[OH", curses.KEY_HOME), # home (7) + + # The vt220 placed F1-F4 above the keypad, in place of actual + # F1-F4 were local functions (hold screen, print screen, + # set up, data/talk, break). + (u"\x1bOP", curses.KEY_F1), + (u"\x1bOQ", curses.KEY_F2), + (u"\x1bOR", curses.KEY_F3), + (u"\x1bOS", curses.KEY_F4), ) diff --git a/blessed/terminal.py b/blessed/terminal.py index d1db30aa..478c559f 100644 --- a/blessed/terminal.py +++ b/blessed/terminal.py @@ -640,6 +640,28 @@ def raw(self): else: yield + @contextlib.contextmanager + def keypad(self): + """ + Context manager that enables keypad input (*keyboard_transmit* mode). + + This enables the effect of calling the curses function keypad(3x): + display terminfo(5) capability `keypad_xmit` (smkx) upon entering, + and terminfo(5) capability `keypad_local` (rmkx) upon exiting. + + On an IBM-PC keypad of ttype *xterm*, with numlock off, the + lower-left diagonal key transmits sequence ``\\x1b[F``, ``KEY_END``. + + However, upon entering keypad mode, ``\\x1b[OF`` is transmitted, + translating to ``KEY_LL`` (lower-left key), allowing diagonal + direction keys to be determined. + """ + try: + self.stream.write(self.smkx) + yield + finally: + self.stream.write(self.rmkx) + def inkey(self, timeout=None, esc_delay=0.35, _intr_continue=True): """T.inkey(timeout=None, [esc_delay, [_intr_continue]]) -> Keypress() diff --git a/blessed/tests/test_core.py b/blessed/tests/test_core.py index 8cfe281e..08f2560b 100644 --- a/blessed/tests/test_core.py +++ b/blessed/tests/test_core.py @@ -59,6 +59,24 @@ def child(kind): child(all_terms) +def test_yield_keypad(): + "Ensure ``keypad()`` writes keyboard_xmit and keyboard_local." + @as_subprocess + def child(kind): + # given, + t = TestTerminal(stream=StringIO(), force_styling=True) + expected_output = u''.join((t.smkx, t.rmkx)) + + # exercise, + with t.keypad(): + pass + + # verify. + assert (t.stream.getvalue() == expected_output) + + child(kind='xterm') + + def test_null_fileno(): "Make sure ``Terminal`` works when ``fileno`` is ``None``." @as_subprocess diff --git a/blessed/tests/test_keyboard.py b/blessed/tests/test_keyboard.py index b4b5eb7b..6645ac20 100644 --- a/blessed/tests/test_keyboard.py +++ b/blessed/tests/test_keyboard.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- "Tests for keyboard support." +import functools import tempfile import StringIO import signal @@ -269,7 +270,7 @@ def child(): def test_inkey_0s_cbreak_noinput_nokb(): - "0-second inkey without input or keyboard." + "0-second inkey without data in input stream and no keyboard/tty." @as_subprocess def child(): term = TestTerminal(stream=StringIO.StringIO()) @@ -549,7 +550,7 @@ def test_esc_delay_cbreak_035(): assert key_name == u'KEY_ESCAPE' assert os.WEXITSTATUS(status) == 0 assert math.floor(time.time() - stime) == 0.0 - assert 35 <= int(duration_ms) <= 45, duration_ms + assert 34 <= int(duration_ms) <= 45, duration_ms def test_esc_delay_cbreak_135(): @@ -819,3 +820,76 @@ def test_resolve_sequence(): assert ks.code is 6 assert ks.is_sequence is True assert repr(ks) in (u"KEY_L", "KEY_L") + + +def test_keypad_mixins_and_aliases(): + """ Test PC-Style function key translations when in ``keypad`` mode.""" + # Key plain app modified + # Up ^[[A ^[OA ^[[1;mA + # Down ^[[B ^[OB ^[[1;mB + # Right ^[[C ^[OC ^[[1;mC + # Left ^[[D ^[OD ^[[1;mD + # End ^[[F ^[OF ^[[1;mF + # Home ^[[H ^[OH ^[[1;mH + @as_subprocess + def child(kind): + term = TestTerminal(kind=kind, force_styling=True) + from blessed.keyboard import resolve_sequence + + resolve = functools.partial(resolve_sequence, + mapper=term._keymap, + codes=term._keycodes) + + assert resolve(unichr(10)).name == "KEY_ENTER" + assert resolve(unichr(13)).name == "KEY_ENTER" + assert resolve(unichr(8)).name == "KEY_BACKSPACE" + assert resolve(unichr(9)).name == "KEY_TAB" + assert resolve(unichr(27)).name == "KEY_ESCAPE" + assert resolve(unichr(127)).name == "KEY_DELETE" + assert resolve(u"\x1b[A").name == "KEY_UP" + assert resolve(u"\x1b[B").name == "KEY_DOWN" + assert resolve(u"\x1b[C").name == "KEY_RIGHT" + assert resolve(u"\x1b[D").name == "KEY_LEFT" + assert resolve(u"\x1b[U").name == "KEY_PGDOWN" + assert resolve(u"\x1b[V").name == "KEY_PGUP" + assert resolve(u"\x1b[H").name == "KEY_HOME" + assert resolve(u"\x1b[F").name == "KEY_END" + assert resolve(u"\x1b[K").name == "KEY_END" + assert resolve(u"\x1bOM").name == "KEY_ENTER" + assert resolve(u"\x1bOj").name == "KEY_KP_MULTIPLY" + assert resolve(u"\x1bOk").name == "KEY_KP_ADD" + assert resolve(u"\x1bOl").name == "KEY_KP_SEPARATOR" + assert resolve(u"\x1bOm").name == "KEY_KP_SUBTRACT" + assert resolve(u"\x1bOn").name == "KEY_KP_DECIMAL" + assert resolve(u"\x1bOo").name == "KEY_KP_DIVIDE" + assert resolve(u"\x1bOX").name == "KEY_KP_EQUAL" + assert resolve(u"\x1bOp").name == "KEY_KP_0" + assert resolve(u"\x1bOq").name == "KEY_KP_1" + assert resolve(u"\x1bOr").name == "KEY_KP_2" + assert resolve(u"\x1bOs").name == "KEY_KP_3" + assert resolve(u"\x1bOt").name == "KEY_KP_4" + assert resolve(u"\x1bOu").name == "KEY_KP_5" + assert resolve(u"\x1bOv").name == "KEY_KP_6" + assert resolve(u"\x1bOw").name == "KEY_KP_7" + assert resolve(u"\x1bOx").name == "KEY_KP_8" + assert resolve(u"\x1bOy").name == "KEY_KP_9" + assert resolve(u"\x1b[1~").name == "KEY_FIND" + assert resolve(u"\x1b[2~").name == "KEY_INSERT" + assert resolve(u"\x1b[3~").name == "KEY_DELETE" + assert resolve(u"\x1b[4~").name == "KEY_SELECT" + assert resolve(u"\x1b[5~").name == "KEY_PGUP" + assert resolve(u"\x1b[6~").name == "KEY_PGDOWN" + assert resolve(u"\x1b[7~").name == "KEY_HOME" + assert resolve(u"\x1b[8~").name == "KEY_END" + assert resolve(u"\x1b[OA").name == "KEY_UP" + assert resolve(u"\x1b[OB").name == "KEY_DOWN" + assert resolve(u"\x1b[OC").name == "KEY_RIGHT" + assert resolve(u"\x1b[OD").name == "KEY_LEFT" + assert resolve(u"\x1b[OF").name == "KEY_END" + assert resolve(u"\x1b[OH").name == "KEY_HOME" + assert resolve(u"\x1bOP").name == "KEY_F1" + assert resolve(u"\x1bOQ").name == "KEY_F2" + assert resolve(u"\x1bOR").name == "KEY_F3" + assert resolve(u"\x1bOS").name == "KEY_F4" + + child('xterm') diff --git a/docs/conf.py b/docs/conf.py index ca3dcdaa..20c2a233 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,7 @@ # built documents. # # The short X.Y version. -version = '1.9.1' +version = '1.9.2' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index e87e1b4f..faae3313 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def main(): here = os.path.dirname(__file__) setuptools.setup( name='blessed', - version='1.9.1', + version='1.9.2', description="A feature-filled fork of Erik Rose's blessings project", long_description=open(os.path.join(here, 'README.rst')).read(), author='Jeff Quast',