--- /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