Update Emitter: All event handlers now have exactly one argument: object inherited from Event class, which carries any data.
# -*- coding: utf-8 -*-
import curses.wrapper
import curses.ascii
import locale
import logging
from tuikit.driver import Driver
class DriverCurses(Driver):
xterm_codes = (
(0x09, 'tab' ),
(0x0a, 'enter' ),
(0x7f, 'backspace' ),
(0x1b, 'escape' ),
(0x1b,0x4f,0x50, 'f1' ),
(0x1b,0x4f,0x51, 'f2' ),
(0x1b,0x4f,0x52, 'f3' ),
(0x1b,0x4f,0x53, 'f4' ),
(0x1b,0x5b,0x31,0x35,0x7e, 'f5' ),
(0x1b,0x5b,0x31,0x37,0x7e, 'f6' ),
(0x1b,0x5b,0x31,0x38,0x7e, 'f7' ),
(0x1b,0x5b,0x31,0x39,0x7e, 'f8' ),
(0x1b,0x5b,0x31,0x7e, 'home' ), # linux
(0x1b,0x5b,0x32,0x30,0x7e, 'f9' ),
(0x1b,0x5b,0x32,0x31,0x7e, 'f10' ),
(0x1b,0x5b,0x32,0x33,0x7e, 'f11' ),
(0x1b,0x5b,0x32,0x34,0x7e, 'f12' ),
(0x1b,0x5b,0x32,0x7e, 'insert' ),
(0x1b,0x5b,0x33,0x7e, 'delete' ),
(0x1b,0x5b,0x34,0x7e, 'end' ), # linux
(0x1b,0x5b,0x35,0x7e, 'pageup' ),
(0x1b,0x5b,0x36,0x7e, 'pagedown' ),
(0x1b,0x5b,0x41, 'up' ),
(0x1b,0x5b,0x42, 'down' ),
(0x1b,0x5b,0x43, 'right' ),
(0x1b,0x5b,0x44, 'left' ),
(0x1b,0x5b,0x46, 'end' ),
(0x1b,0x5b,0x48, 'home' ),
(0x1b,0x5b,0x4d, 'mouse' ),
(0x1b,0x5b,0x5b,0x41, 'f1' ), # linux
(0x1b,0x5b,0x5b,0x42, 'f2' ), # linux
(0x1b,0x5b,0x5b,0x43, 'f3' ), # linux
(0x1b,0x5b,0x5b,0x44, 'f4' ), # linux
(0x1b,0x5b,0x5b,0x45, 'f5' ), # linux
)
color_map = {
'black' : curses.COLOR_BLACK,
'blue' : curses.COLOR_BLUE,
'cyan' : curses.COLOR_CYAN,
'green' : curses.COLOR_GREEN,
'magenta' : curses.COLOR_MAGENTA,
'red' : curses.COLOR_RED,
'white' : curses.COLOR_WHITE,
'yellow' : curses.COLOR_YELLOW,
}
attr_map = {
'blink' : curses.A_BLINK,
'bold' : curses.A_BOLD,
'dim' : curses.A_DIM,
'standout' : curses.A_STANDOUT,
'underline' : curses.A_UNDERLINE,
}
def __init__(self):
'''Set driver attributes to default values.'''
Driver.__init__(self)
self.log = logging.getLogger('tuikit')
self.screen = None
self.cursor = None
self.colors = {} # maps names to curses attributes
self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
self.inputqueue = []
self.mbtnstack = []
def init_curses(self):
'''Initialize curses'''
self.size.h, self.size.w = self.screen.getmaxyx()
self.screen.immedok(0)
self.screen.keypad(0)
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 start(self, mainfunc):
def main(screen):
self.screen = screen
self.init_curses()
mainfunc()
curses.wrapper(main)
## colors, attributes ##
def _parsecolor(self, name):
name = name.lower().strip()
return self.color_map[name]
def _getcolorpair(self, fg, bg):
pair = (fg, bg)
if pair in self.colorpairs:
return self.colorpairs[pair]
num = len(self.colorpairs) + 1
curses.init_pair(num, fg, bg)
self.colorpairs[pair] = num
return num
def _parseattrs(self, attrs):
res = 0
for a in attrs:
a = a.lower().strip()
res = res | self.attr_map[a]
return res
def setcolor(self, name, desc):
parts = desc.split(',')
fg, bg = parts[0].split(' on ')
attrs = parts[1:]
fg = self._parsecolor(fg)
bg = self._parsecolor(bg)
col = self._getcolorpair(fg, bg)
attr = self._parseattrs(attrs)
self.colors[name] = curses.color_pair(col) | attr
def pushcolor(self, name):
# add prefix if such color is available
if len(self.colorprefix):
prefixname = self.colorprefix[-1] + name
if prefixname in self.colors:
name = prefixname
attr = self.colors[name]
self.screen.attrset(attr)
self.colorstack.append(attr)
def popcolor(self):
self.colorstack.pop()
if len(self.colorstack):
attr = self.colorstack[-1]
else:
attr = 0
self.screen.attrset(attr)
## drawing ##
def putch(self, x, y, c):
if not self.clipstack.test(x, y):
return
try:
if isinstance(c, str) and len(c) == 1:
self.screen.addstr(y, x, c)
else:
self.screen.addch(y, x, c)
except curses.error:
pass
def erase(self):
self.screen.erase()
def commit(self):
if self.cursor:
self.screen.move(*self.cursor)
curses.curs_set(True)
else:
curses.curs_set(False)
self.screen.refresh()
## cursor ##
def showcursor(self, x, y):
if not self.clipstack.test(x, y):
return
self.cursor = (y, x)
def hidecursor(self):
curses.curs_set(False)
self.cursor = None
## input ##
def inputqueue_fill(self, timeout=None):
if timeout is None:
# wait indefinitely
c = self.screen.getch()
self.inputqueue.insert(0, c)
elif timeout > 0:
# wait
curses.halfdelay(timeout)
c = self.screen.getch()
curses.cbreak()
if c == -1:
return
self.inputqueue.insert(0, c)
# timeout = 0 -> no wait
self.screen.nodelay(1)
while True:
c = self.screen.getch()
if c == -1:
break
self.inputqueue.insert(0, c)
self.screen.nodelay(0)
def inputqueue_top(self, num=0):
return self.inputqueue[-1-num]
def inputqueue_get(self):
c = None
try:
c = self.inputqueue.pop()
except IndexError:
pass
return c
def inputqueue_get_wait(self):
c = None
while c is None:
try:
c = self.inputqueue.pop()
except IndexError:
curses.napms(25)
self.inputqueue_fill(0)
return c
def inputqueue_unget(self, c):
self.inputqueue.append(c)
def getevents(self, timeout=None):
# empty queue -> fill
if len(self.inputqueue) == 0:
self.inputqueue_fill(timeout)
res = []
while len(self.inputqueue):
c = self.inputqueue_get()
if c == curses.KEY_MOUSE:
res += self.process_mouse()
elif c == curses.KEY_RESIZE:
self.size.h, self.size.w = self.screen.getmaxyx()
res.append(('resize',))
elif curses.ascii.isctrl(c):
self.inputqueue_unget(c)
res += self.process_control_chars()
elif c >= 192 and c <= 255:
self.inputqueue_unget(c)
res += self.process_utf8_chars()
elif curses.ascii.isprint(c):
res += [('keypress', None, str(chr(c)))]
else:
#self.top.keypress(None, unicode(chr(c)))
self.inputqueue_unget(c)
res += self.process_control_chars()
return res
def process_mouse(self):
try:
_id, x, y, _z, bstate = curses.getmouse()
except curses.error:
return []
out = []
if bstate & curses.REPORT_MOUSE_POSITION:
out += [('mousemove', x, y)]
if bstate & curses.BUTTON1_PRESSED:
out += [('mousedown', x, y, 1)]
if bstate & curses.BUTTON3_PRESSED:
out += [('mousedown', x, y, 3)]
if bstate & curses.BUTTON1_RELEASED:
out += [('mouseup', x, y, 1)]
if bstate & curses.BUTTON3_RELEASED:
out += [('mouseup', x, y, 3)]
return out
def process_utf8_chars(self):
#FIXME read exact number of chars as defined by utf-8
utf = []
while len(utf) <= 6:
c = self.inputqueue_get_wait()
utf.append(c)
try:
uni = str(bytes(utf), 'utf-8')
return [('keypress', None, uni)]
except UnicodeDecodeError:
continue
raise Exception('Invalid UTF-8 sequence: %r' % utf)
def process_control_chars(self):
codes = self.xterm_codes
matchingcodes = []
match = None
consumed = []
# consume next char, filter out matching codes
c = self.inputqueue_get_wait()
consumed.append(c)
while True:
#self.log.debug('c=%s len=%s', c, len(codes))
for code in codes:
if c == code[len(consumed)-1]:
if len(code) - 1 == len(consumed):
match = code
else:
matchingcodes += [code]
#self.log.debug('matching=%s', len(matchingcodes))
# match found, or no matching code found -> stop
if len(matchingcodes) == 0:
break
# match found and some sequencies still match -> continue
if len(matchingcodes) > 0:
if len(self.inputqueue) == 0:
self.inputqueue_fill(1)
c = self.inputqueue_get()
if c:
consumed.append(c)
codes = matchingcodes
matchingcodes = []
else:
break
keyname = None
if match:
# compare match to consumed, return unused chars
l = len(match) - 1
while len(consumed) > l:
self.inputqueue_unget(consumed[-1])
del consumed[-1]
keyname = match[-1]
if match is None:
self.log.debug('Unknown control sequence: %s',
','.join(['0x%x'%x for x in consumed]))
return [('keypress', 'Unknown', None)]
if keyname == 'mouse':
return self.process_xterm_mouse()
return [('keypress', keyname, None)]
def process_xterm_mouse(self):
t = self.inputqueue_get_wait()
x = self.inputqueue_get_wait() - 0x21
y = self.inputqueue_get_wait() - 0x21
out = []
if t in (0x20, 0x21, 0x22): # button press
btn = t - 0x1f
if not btn in self.mbtnstack:
self.mbtnstack.append(btn)
out += [('mousedown', x, y, btn)]
else:
out += [('mousemove', x, y, btn)]
elif t == 0x23: # button release
btn = self.mbtnstack.pop()
out += [('mouseup', x, y, btn)]
elif t in (0x60, 0x61): # wheel up, down
btn = 4 + t - 0x60
out += [('mousewheel', x, y, btn)]
else:
raise Exception('Unknown mouse event: %x' % t)
return out
driverclass = DriverCurses