# HG changeset patch # User Radek Brich # Date 1409764417 -7200 # Node ID cf3d49cdd6e26e9520645c5f6a997eab8db478ee # Parent 105b1affc3c24eea1b17b632e9ebcb0311f54d46 Add cursesw driver, using curses get_wch() for unicode input. It alse has enabled keypad() to let curses interpret control keys and mouse input. diff -r 105b1affc3c2 -r cf3d49cdd6e2 tests/curses_get_wch.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/curses_get_wch.py Wed Sep 03 19:13:37 2014 +0200 @@ -0,0 +1,18 @@ +#!/usr/bin/python3 + +import curses +import locale + +locale.setlocale(locale.LC_ALL, "") + +def doStuff(stdscr): + stdscr.keypad(1) + message = "Press 'q' to quit.\n" + stdscr.addstr(0, 0, message, 0) + while True: + c = stdscr.get_wch() # pauses until a key's hit + if c == 'q': + break + stdscr.addstr('%s %r\n' % (c, c)) + +curses.wrapper(doStuff) diff -r 105b1affc3c2 -r cf3d49cdd6e2 tests/curses_getkey.py --- a/tests/curses_getkey.py Wed Sep 03 19:08:21 2014 +0200 +++ b/tests/curses_getkey.py Wed Sep 03 19:13:37 2014 +0200 @@ -1,5 +1,4 @@ -#!/usr/bin/python -# coding=UTF-8 +#!/usr/bin/python3 import curses import locale diff -r 105b1affc3c2 -r cf3d49cdd6e2 tests/curses_keycodes.py --- a/tests/curses_keycodes.py Wed Sep 03 19:08:21 2014 +0200 +++ b/tests/curses_keycodes.py Wed Sep 03 19:13:37 2014 +0200 @@ -1,5 +1,4 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- +#!/usr/bin/python3 import curses import locale diff -r 105b1affc3c2 -r cf3d49cdd6e2 tests/curses_mouse.py --- a/tests/curses_mouse.py Wed Sep 03 19:08:21 2014 +0200 +++ b/tests/curses_mouse.py Wed Sep 03 19:13:37 2014 +0200 @@ -1,5 +1,4 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- +#!/usr/bin/python3 import curses import locale @@ -21,8 +20,8 @@ screen.addstr('key: %x %s\n' % (c, char)) if c == curses.KEY_MOUSE: - m = curses.getmouse() - screen.addstr('(%d %d %d %d %x)\n' % m) + id_, x, y, z, bstate = curses.getmouse() + screen.addstr('(%d %d %d %d %s)\n' % (id_, x, y, z, bin(bstate))) screen.refresh() diff -r 105b1affc3c2 -r cf3d49cdd6e2 tests/curses_unicode.py --- a/tests/curses_unicode.py Wed Sep 03 19:08:21 2014 +0200 +++ b/tests/curses_unicode.py Wed Sep 03 19:13:37 2014 +0200 @@ -1,5 +1,4 @@ -#!/usr/bin/python -# coding=UTF-8 +#!/usr/bin/python3 import curses import locale diff -r 105b1affc3c2 -r cf3d49cdd6e2 tuikit/core/application.py --- a/tuikit/core/application.py Wed Sep 03 19:08:21 2014 +0200 +++ b/tuikit/core/application.py Wed Sep 03 19:13:37 2014 +0200 @@ -15,7 +15,7 @@ """ - def __init__(self, driver='curses'): + def __init__(self, driver='cursesw'): self.log = logging.getLogger(__name__) self.driver = None self.timer = Timer() diff -r 105b1affc3c2 -r cf3d49cdd6e2 tuikit/driver/cursesw.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/driver/cursesw.py Wed Sep 03 19:13:37 2014 +0200 @@ -0,0 +1,256 @@ +import curses.ascii +import math +import logging + +from tuikit.driver.driver import Driver + + +class CursesWDriver(Driver): + + key_names = { + '\t': 'tab', + '\n': 'enter', + '\x1b': 'escape', + } + + key_map = { + curses.KEY_UP: 'up', + curses.KEY_DOWN: 'down', + curses.KEY_LEFT: 'left', + curses.KEY_RIGHT: 'right', + curses.KEY_IC: 'insert', + curses.KEY_DC: 'delete', + curses.KEY_HOME: 'home', + curses.KEY_END: 'end', + curses.KEY_PPAGE: 'pageup', + curses.KEY_NPAGE: 'pagedown', + curses.KEY_BACKSPACE: 'backspace', + curses.KEY_BTAB: 'shift+tab', + } + for _i in range(1, 13): + key_map[curses.KEY_F0 + _i] = 'f' + str(_i) + + color_map = { + 'default': (-1, 0), + 'black': (curses.COLOR_BLACK, 0), + 'blue': (curses.COLOR_BLUE, 0), + 'green': (curses.COLOR_GREEN, 0), + 'cyan': (curses.COLOR_CYAN, 0), + 'red': (curses.COLOR_RED, 0), + 'magenta': (curses.COLOR_MAGENTA, 0), + 'brown': (curses.COLOR_YELLOW, 0), + 'lightgray': (curses.COLOR_WHITE, 0), + 'gray': (curses.COLOR_BLACK, curses.A_BOLD), + 'lightblue': (curses.COLOR_BLUE, curses.A_BOLD), + 'lightgreen': (curses.COLOR_GREEN, curses.A_BOLD), + 'lightcyan': (curses.COLOR_CYAN, curses.A_BOLD), + 'lightred': (curses.COLOR_RED, curses.A_BOLD), + 'lightmagenta': (curses.COLOR_MAGENTA, curses.A_BOLD), + 'yellow': (curses.COLOR_YELLOW, curses.A_BOLD), + 'white': (curses.COLOR_WHITE, curses.A_BOLD), + } + + attr_map = { + 'bold': curses.A_BOLD, + 'underline': curses.A_UNDERLINE, + 'standout': curses.A_STANDOUT, # inverse bg/fg + 'blink': curses.A_BLINK, + } + + def __init__(self): + Driver.__init__(self) + self._log = logging.getLogger(__name__) + self.stdscr = None + self.cursor = None + self.colors = {} # maps names to curses attributes + self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair + self._mouse_last_pos = (None, None) + self._mouse_last_bstate = None + + ## initialization, finalization ## + + def init(self): + """Initialize curses""" + self.stdscr = curses.initscr() + curses.start_color() + curses.use_default_colors() + curses.noecho() + curses.cbreak() + self.stdscr.keypad(1) + self.stdscr.immedok(0) + + self.size.h, self.size.w = self.stdscr.getmaxyx() + + curses.curs_set(False) # hide cursor + curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION) + curses.mouseinterval(0) # do not wait to detect clicks, we use only press/release + + def close(self): + self.stdscr.keypad(0) + curses.echo() + curses.nocbreak() + curses.endwin() + + ## drawing ## + + def clear(self): + self.stdscr.erase() + + def putch(self, ch, x, y): + try: + if isinstance(ch, int): + self.stdscr.addch(y, x, ch) + elif isinstance(ch, str) and len(ch) == 1: + self.stdscr.addstr(y, x, ch) + else: + raise TypeError('Integer or one-char string is required.') + except curses.error as e: + self._log.exception('putch(%r, %s, %s) error:' % (ch, x, y)) + + def draw(self, buffer, x=0, y=0): + for bufy in range(buffer.size.h): + for bufx in range(buffer.size.w): + char, attr_desc = buffer.get(bufx, bufy) + self.setattr(attr_desc) + self.putch(char, x + bufx, y + bufy) + + def flush(self): + if self.cursor: + self.stdscr.move(self.cursor.y, self.cursor.x) + curses.curs_set(True) + else: + curses.curs_set(False) + self.stdscr.refresh() + + ## colors, attributes ## + + def setattr(self, attr_desc): + """Set attribute to be used for subsequent draw operations.""" + attr = self.colors.get(attr_desc, None) + if attr is None: + # first encountered `attr_desc`, initialize + fg, bg, attrs = self._parse_attr_desc(attr_desc) + fgcol, fgattr = self.color_map[fg] + bgcol, _bgattr = self.color_map[bg] + colpair = self._getcolorpair(fgcol, bgcol) + attr = curses.color_pair(colpair) | self._parseattrs(attrs) | fgattr + self.colors[attr_desc] = attr + self.stdscr.attrset(attr) + + def _getcolorpair(self, fgcol, bgcol): + pair = (fgcol, bgcol) + if pair in self.colorpairs: + return self.colorpairs[pair] + num = len(self.colorpairs) + 1 + curses.init_pair(num, fgcol, bgcol) + self.colorpairs[pair] = num + return num + + def _parseattrs(self, attrs): + res = 0 + for a in attrs: + res = res | self.attr_map[a] + return res + + ## input, events ## + + def getevents(self, timeout=None): + """Process input, return list of events. + + timeout -- float, in seconds (None=infinite) + + Returns: + [('event', param1, ...), ...] + + """ + # Set timeout + if timeout is None: + # wait indefinitely + curses.cbreak() + elif timeout > 0: + # wait + timeout_tenths = math.ceil(timeout * 10) + curses.halfdelay(timeout_tenths) + else: + # timeout = 0 -> no wait + self.stdscr.nodelay(1) + + # Get key or char + c = self.stdscr.get_wch() + + res = [] + + if c == -1: + # Timeout + return res + elif c == curses.KEY_MOUSE: + res += self._process_mouse() + elif c == curses.KEY_RESIZE: + self.size.h, self.size.w = self.stdscr.getmaxyx() + res += [('resize', self.size.w, self.size.h)] + elif isinstance(c, int): + keyname, mod = self._split_keyname_mod(self.key_map[c]) + res += [('keypress', keyname, None, mod)] + else: + keyname = self.key_names.get(c) + res += [('keypress', keyname, c, set())] + + return res + + def _process_mouse(self): + out = [] + try: + _id, x, y, _z, bstate = curses.getmouse() + except curses.error: + return out + + if bstate & curses.REPORT_MOUSE_POSITION: + if self._mouse_last_pos != (x, y): + if self._mouse_last_pos[0] is not None: + relx = x - (self._mouse_last_pos[0] or 0) + rely = y - (self._mouse_last_pos[1] or 0) + out += [('mousemove', 0, x, y, relx, rely)] + self._mouse_last_pos = (x, y) + + # we are interested only in changes, not buttons already pressed before event + if self._mouse_last_bstate is not None: + old = self._mouse_last_bstate + new = bstate + bstate = ~old & new + self._mouse_last_bstate = new + else: + self._mouse_last_bstate = bstate + + if bstate & curses.BUTTON1_PRESSED: + out += [('mousedown', 1, x, y)] + if bstate & curses.BUTTON2_PRESSED: + out += [('mousedown', 2, x, y)] + if bstate & curses.BUTTON3_PRESSED: + out += [('mousedown', 3, x, y)] + if bstate & curses.BUTTON1_RELEASED: + out += [('mouseup', 1, x, y)] + if bstate & curses.BUTTON2_RELEASED: + out += [('mouseup', 2, x, y)] + if bstate & curses.BUTTON3_RELEASED: + out += [('mouseup', 3, x, y)] + + # reset last pos when pressed/released + if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'): + self._mouse_last_pos = (None, None) + + return out + + def _split_keyname_mod(self, keyname): + """Parse keynames in form "shift+tab", return (keyname, mod).""" + mod_set = set() + if '+' in keyname: + parts = keyname.split('+') + for mod in parts[:-1]: + assert(mod in ('shift', 'alt', 'ctrl', 'meta')) + mod_set.add(mod) + keyname = parts[-1] + + return keyname, mod_set + + +driver_class = CursesWDriver diff -r 105b1affc3c2 -r cf3d49cdd6e2 tuikit/widgets/textfield.py --- a/tuikit/widgets/textfield.py Wed Sep 03 19:08:21 2014 +0200 +++ b/tuikit/widgets/textfield.py Wed Sep 03 19:13:37 2014 +0200 @@ -60,7 +60,7 @@ else: consumed = False - if char: + if not keyname and char: self.add_char(char) self.move_right() consumed = True