New color management - named colors.
# -*- 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, '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_names = {
'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,
}
def __init__(self, screen):
self.screen = screen
self.height, self.width = screen.getmaxyx()
self.cursor = None
self.clipstack = []
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 = []
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)
# 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 _parsecolor(self, name):
name = name.lower().strip()
return self.color_names[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()
trans = {
'blink' : curses.A_BLINK,
'bold' : curses.A_BOLD,
'dim' : curses.A_DIM,
'standout' : curses.A_STANDOUT,
'underline' : curses.A_UNDERLINE,
}
res = res | trans[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):
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.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):
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)
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 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):
try:
id, x, y, z, bstate = curses.getmouse()
except curses.error:
return []
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