Add cursesw driver, using curses get_wch() for unicode input. It alse has enabled keypad() to let curses interpret control keys and mouse input.
authorRadek Brich <radek.brich@devl.cz>
Wed, 03 Sep 2014 19:13:37 +0200
changeset 110 cf3d49cdd6e2
parent 109 105b1affc3c2
child 111 b055add74b18
Add cursesw driver, using curses get_wch() for unicode input. It alse has enabled keypad() to let curses interpret control keys and mouse input.
tests/curses_get_wch.py
tests/curses_getkey.py
tests/curses_keycodes.py
tests/curses_mouse.py
tests/curses_unicode.py
tuikit/core/application.py
tuikit/driver/cursesw.py
tuikit/widgets/textfield.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)
--- 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
--- 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
--- 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()
 
--- 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
--- 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()
--- /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
--- 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