Add mouse events, event demo.
import curses.ascii
import math
import logging
from tuikit.driver.driver import Driver
from tuikit.core.events import ResizeEvent, KeypressEvent, MouseEvent
from tuikit.core.coords import Point
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 # Point
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:
if x == self.size.w - 1 and y == self.size.h - 1:
# Curses putch to lower-right corner gives error because
# scrolling is disabled and cursor cannot move to next char.
# Let's just ignore that for now.
return
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:
List of Event objects.
"""
# 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.append(ResizeEvent(self.size.w, self.size.h))
elif isinstance(c, int):
keyname, mods = self._split_keyname_mods(self.key_map[c])
res.append(KeypressEvent(keyname, None, mods))
else:
keyname = self.key_names.get(c)
res.append(KeypressEvent(keyname, c, set()))
return res
def _process_mouse(self):
res = []
try:
_id, x, y, _z, bstate = curses.getmouse()
except curses.error:
return res
pos = Point(x, y)
if bstate & curses.REPORT_MOUSE_POSITION:
if self._mouse_last_pos != pos:
if self._mouse_last_pos:
relpos = pos - self._mouse_last_pos
res.append(MouseEvent('mousemove', 0, pos, relpos))
self._mouse_last_pos = pos
# 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:
res.append(MouseEvent('mousedown', 1, pos))
if bstate & curses.BUTTON2_PRESSED:
res.append(MouseEvent('mousedown', 2, pos))
if bstate & curses.BUTTON3_PRESSED:
res.append(MouseEvent('mousedown', 3, pos))
if bstate & curses.BUTTON1_RELEASED:
res.append(MouseEvent('mouseup', 1, pos))
if bstate & curses.BUTTON2_RELEASED:
res.append(MouseEvent('mouseup', 2, pos))
if bstate & curses.BUTTON3_RELEASED:
res.append(MouseEvent('mouseup', 3, pos))
# Reset last pos when pressed/released
if len(res) > 0 and res[-1].name in ('mousedown', 'mouseup'):
self._mouse_last_pos = None
return res
def _split_keyname_mods(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