Merge.
Due to my schizophrenia, I've accidentally forked my own code. The other set of changes were found in another computer.
import curses.ascii
import math
import logging
from tuikit.driver.driver import Driver
from tuikit.core.events import ResizeEvent, KeypressEvent
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 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.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):
out = []
try:
_id, x, y, _z, bstate = curses.getmouse()
except curses.error:
return out
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
out += [('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:
out += [('mousedown', 1, pos)]
if bstate & curses.BUTTON2_PRESSED:
out += [('mousedown', 2, pos)]
if bstate & curses.BUTTON3_PRESSED:
out += [('mousedown', 3, pos)]
if bstate & curses.BUTTON1_RELEASED:
out += [('mouseup', 1, pos)]
if bstate & curses.BUTTON2_RELEASED:
out += [('mouseup', 2, pos)]
if bstate & curses.BUTTON3_RELEASED:
out += [('mouseup', 3, pos)]
# reset last pos when pressed/released
if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'):
self._mouse_last_pos = None
return out
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