Fixed escape sequence handling.
# -*- coding: utf-8 -*-
import curses
import curses.ascii
import locale
import logging
from .common import Rect
class MouseEvent:
def __init__(self, x=0, y=0):
self.x = x # global coordinates
self.y = y
self.wx = x # local widget coordinates
self.wy = y
self.px = 0 # parent coordinates
self.py = 0
self.button = 0
def childevent(self, child):
ev = MouseEvent(self.x, self.y)
# original local coordinates are new parent coordinates
ev.px = self.wx
ev.py = self.wy
# update local coordinates
ev.wx = self.wx - child.x
ev.wy = self.wy - child.y
return ev
class BackendCurses:
xterm_codes = (
(0x09, 'tab' ),
(0x0a, 'enter' ),
(0x7f, 'backspace' ),
(0x1b,0x4f,0x50, 'f1' ),
(0x1b,0x4f,0x51, 'f2' ),
(0x1b,0x4f,0x52, 'f3' ),
(0x1b,0x4f,0x53, 'f4' ),
(0x1b,0x5b,0x4d, 'mouse' ),
(0x1b,0x5b,0x41, 'up' ),
(0x1b,0x5b,0x42, 'down' ),
(0x1b,0x5b,0x43, 'right' ),
(0x1b,0x5b,0x44, 'left' ),
(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,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,0x35,0x7e, 'pageup' ),
(0x1b,0x5b,0x36,0x7e, 'pagedown' ),
(0x1b,0x5b,0x46, 'end' ),
(0x1b,0x5b,0x48, 'home' ),
(0x1b, 'escape' ),
)
def __init__(self, screen):
self.screen = screen
self.height, self.width = screen.getmaxyx()
self.clipstack = []
self.colorstack = []
self.inputqueue = []
self.mbtnstack = []
self.log = logging.getLogger('tuikit')
# initialize curses
curses.curs_set(False)
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
curses.mouseinterval(0) # do not wait to detect clicks, we use only press/release
screen.immedok(0)
screen.keypad(0)
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN)
curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_CYAN)
curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE)
curses.init_pair(5, curses.COLOR_CYAN, curses.COLOR_BLUE)
self.BOLD = curses.A_BOLD
self.BLINK = curses.A_BLINK
self.UNDERLINE = curses.A_UNDERLINE
# http://en.wikipedia.org/wiki/List_of_Unicode_characters#Geometric_shapes
self.UP_ARROW = '▲' #curses.ACS_UARROW
self.DOWN_ARROW = '▼' #curses.ACS_DARROW
# http://en.wikipedia.org/wiki/Box-drawing_characters
self.LIGHT_SHADE = '░' #curses.ACS_BOARD
self.MEDIUM_SHADE = '▒'
self.DARK_SHADE = '▓'
self.BLOCK = '█'
self.COLUMN = '▁▂▃▄▅▆▇█'
self.CORNER_ROUND = '╭╮╰╯'
self.CORNER = '┌┐└┘'
self.LINE = '─━│┃┄┅┆┇┈┉┊┋'
self.HLINE = '─' # curses.ACS_HLINE
self.VLINE = '│' # curses.ACS_VLINE
self.ULCORNER = '┌' # curses.ACS_ULCORNER
self.URCORNER = '┐' # curses.ACS_URCORNER
self.LLCORNER = '└' # curses.ACS_LLCORNER
self.LRCORNER = '┘' # curses.ACS_LRCORNER
self.LTEE = '├'
self.RTEE = '┤'
## clip operations ##
def pushclip(self, x, y, w, h):
newclip = Rect(x, y, w, h)
if len(self.clipstack):
oldclip = self.clipstack[-1]
newclip = self.intersect(oldclip, newclip)
self.clipstack.append(newclip)
def popclip(self):
self.clipstack.pop()
def testclip(self, x, y):
# no clip rectangle on stack => passed
if not len(self.clipstack):
return True
# test against top clip rect from stack
clip = self.clipstack[-1]
if x < clip.x or y < clip.y \
or x >= clip.x + clip.w or y >= clip.y + clip.h:
return False
# passed
return True
def intersect(self, r1, r2):
x1 = max(r1.x, r2.x)
y1 = max(r1.y, r2.y)
x2 = min(r1.x + r1.w, r2.x + r2.w)
y2 = min(r1.y + r1.h, r2.y + r2.h)
if x1 >= x2 or y1 >= y2:
return Rect()
return Rect(x1, y1, x2-x1, y2-y1)
def union(self, r1, r2):
x = min(r1.x, r2.x)
y = min(r1.y, r2.y)
w = max(r1.x + r1.w, r2.x + r2.w) - x
h = max(r1.y + r1.h, r2.y + r2.h) - y
return Rect(x, y, w, h)
## attributes ##
def pushcolor(self, col, attr=0):
if type(col) is tuple:
col, attr = col
if attr == 'bold':
attr = self.BOLD
self.screen.attrset(curses.color_pair(col) | attr)
self.colorstack.append((col, attr))
def popcolor(self):
self.colorstack.pop()
if len(self.colorstack):
col, attr = self.colorstack[-1]
else:
col, attr = 0, 0
self.screen.attrset(curses.color_pair(col) | attr)
## drawing ##
def putch(self, x, y, c):
if not self.testclip(x, y):
return
try:
if type(c) is str and len(c) == 1:
self.screen.addstr(y, x, c)
else:
self.screen.addch(y, x, c)
except curses.error:
pass
def puts(self, x, y, s):
for c in s:
self.putch(x, y, c)
x += 1
def hline(self, x, y, w, c=' '):
if type(c) is str:
s = c*w
else:
s = [c]*w
self.puts(x, y, s)
def vline(self, x, y, h, c=' '):
for i in range(h):
self.putch(x, y+i, c)
def frame(self, x, y, w, h):
self.putch(x, y, self.ULCORNER)
self.putch(x+w-1, y, self.URCORNER)
self.putch(x, y+h-1, self.LLCORNER)
self.putch(x+w-1, y+h-1, self.LRCORNER)
self.hline(x+1, y, w-2, self.HLINE)
self.hline(x+1, y+h-1, w-2, self.HLINE)
self.vline(x, y+1, h-2, self.VLINE)
self.vline(x+w-1, y+1, h-2, self.VLINE)
def fill(self, x, y, w, h, c=' '):
for i in range(h):
self.hline(x, y + i, w, c)
def erase(self):
curses.curs_set(False)
self.cursor = None
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.testclip(x, y):
return
self.cursor = (y, x)
## 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 process_input(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 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):
id, x, y, z, bstate = curses.getmouse()
ev = MouseEvent(x, y)
out = []
if bstate & curses.REPORT_MOUSE_POSITION:
out += [('mousemove', ev)]
if bstate & curses.BUTTON1_PRESSED:
ev.button = 1
out += [('mousedown', ev)]
if bstate & curses.BUTTON3_PRESSED:
ev.button = 3
out += [('mousedown', ev)]
if bstate & curses.BUTTON1_RELEASED:
ev.button = 1
out += [('mouseup', ev)]
if bstate & curses.BUTTON3_RELEASED:
ev.button = 3
out += [('mouseup', ev)]
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 += chr(c)
try:
uni = str(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
ev = MouseEvent(x, y)
out = []
if t in (0x20, 0x21, 0x22): # button press
btn = t - 0x1f
ev.button = btn
if not btn in self.mbtnstack:
self.mbtnstack.append(btn)
out += [('mousedown', ev)]
else:
out += [('mousemove', ev)]
elif t == 0x23: # button release
ev.button = self.mbtnstack.pop()
out += [('mouseup', ev)]
elif t in (0x60, 0x61): # wheel up, down
ev.button = 4 + t - 0x60
out += [('mousewheel', ev)]
else:
raise Exception('Unknown mouse event: %x' % t)
return out