# HG changeset patch # User Radek Brich # Date 1318158418 -7200 # Node ID 4e72fd2a0e146bc4c2f841ae098fd6947b8fbd5e # Parent 6ca8b2d221c391cd7901eaea044a2bee45ff75dc Rename BackendCurses to DriverCurses. Add DriverDummy - dummy driver for debugging purposes. Move clipping stack from driver to common.ClipStack class. diff -r 6ca8b2d221c3 -r 4e72fd2a0e14 tuikit/application.py --- a/tuikit/application.py Sun Oct 09 11:17:42 2011 +0200 +++ b/tuikit/application.py Sun Oct 09 13:06:58 2011 +0200 @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- -import curses.wrapper import logging import time import math from tuikit.container import Container -from tuikit.backend_curses import BackendCurses +from tuikit.driver_curses import DriverCurses +from tuikit.driver_dummy import DriverDummy class TopWindow(Container): @@ -79,12 +79,14 @@ def __init__(self): '''Create application.''' + self.top = TopWindow() '''Top window.''' - self.top = TopWindow() + self.quit = False - '''Renderer class, i.e. BackendCurses.''' - self.screen = None + #self.driver = DriverDummy() + self.driver = DriverCurses() + '''Driver class (render + input), i.e. DriverCurses.''' self.log = logging.getLogger('tuikit') self.log.setLevel(logging.DEBUG) @@ -94,40 +96,34 @@ self.log.addHandler(handler) self.log.info('=== start ===') - def start(self): '''Start application. Runs main loop.''' - curses.wrapper(self.mainloop) - + self.driver.start(self.mainloop) def terminate(self): '''Terminate application.''' self.quit = True - - def mainloop(self, screen): + def mainloop(self): '''The main loop.''' - self.screen = BackendCurses(screen) self.applytheme() - self.top.width, self.top.height = self.screen.width, self.screen.height + self.top.size = self.driver.size # link top widget size to screen size self.top.emit('resize') while True: - self.top.draw(self.screen) - self.screen.commit() + self.top.draw(self.driver) + self.driver.commit() timeout = None if self.top.has_timeout(): timeout = int(math.ceil(self.top.nearest_timeout() * 10)) - events = self.screen.process_input(timeout) + events = self.driver.process_input(timeout) if self.top.has_timeout(): self.top.process_timeout() for event in events: - if event[0] == 'resize': - self.top.width, self.top.height = self.screen.width, self.screen.height self.top.emit(event[0], *event[1:]) if self.quit: @@ -135,15 +131,15 @@ def applytheme(self): - screen = self.screen - screen.setcolor('normal', 'white on black') - screen.setcolor('strong', 'white on black, bold') - screen.setcolor('active', 'black on cyan') - screen.setcolor('window:normal', 'white on blue') - screen.setcolor('window:controls', 'white on blue, bold') - screen.setcolor('window:controls-active', 'cyan on blue, bold') - screen.setcolor('button', 'black on white') - screen.setcolor('button-active', 'black on cyan') - screen.setcolor('menu', 'black on cyan') - screen.setcolor('menu-active', 'white on cyan, bold') + driver = self.driver + driver.setcolor('normal', 'white on black') + driver.setcolor('strong', 'white on black, bold') + driver.setcolor('active', 'black on cyan') + driver.setcolor('window:normal', 'white on blue') + driver.setcolor('window:controls', 'white on blue, bold') + driver.setcolor('window:controls-active', 'cyan on blue, bold') + driver.setcolor('button', 'black on white') + driver.setcolor('button-active', 'black on cyan') + driver.setcolor('menu', 'black on cyan') + driver.setcolor('menu-active', 'white on cyan, bold') diff -r 6ca8b2d221c3 -r 4e72fd2a0e14 tuikit/backend_curses.py --- a/tuikit/backend_curses.py Sun Oct 09 11:17:42 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,549 +0,0 @@ -# -*- coding: utf-8 -*- - -import curses -import curses.ascii -import locale -import logging - -from tuikit.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.colorprefix = [] # stack of color prefixes - - 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) - - - ## colors, 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): - # add prefix if 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) - - - def pushcolorprefix(self, name): - self.colorprefix.append(name) - - - def popcolorprefix(self): - self.colorprefix.pop() - - - ## drawing ## - - def putch(self, x, y, c): - if not self.testclip(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 puts(self, x, y, s): - for c in s: - self.putch(x, y, c) - x += 1 - - - def hline(self, x, y, w, c=' '): - if isinstance(c, 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 c == curses.KEY_RESIZE: - self.height, self.width = 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 [] - - 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.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 - - 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 diff -r 6ca8b2d221c3 -r 4e72fd2a0e14 tuikit/common.py --- a/tuikit/common.py Sun Oct 09 11:17:42 2011 +0200 +++ b/tuikit/common.py Sun Oct 09 13:06:58 2011 +0200 @@ -2,6 +2,9 @@ class Coords: + + '''2D coordinates.''' + def __init__(self, x=0, y=0): self.x = x self.y = y @@ -25,6 +28,14 @@ class Size: + + '''Size class. + + Implements attribute access (.w, .h), list-like access([0],[1]) + and dict-like access (['w'],['h']). + + ''' + def __init__(self, w=None, h=None): self.w = w self.h = h @@ -38,9 +49,9 @@ return self.__dict__[key] def __setitem__(self, key, value): - if key == 0: + if key in [0, 'w']: self.w = value - if key == 1: + if key in [1, 'h']: self.h = value def __repr__(self): @@ -48,6 +59,9 @@ class Rect: + + '''Rectangle is defined by 2D coordinates and size.''' + def __init__(self, x=0, y=0, w=0, h=0): self.x = x self.y = y @@ -59,6 +73,15 @@ class Borders: + + '''Borders are defined by left, top, right, bottom border size. + + Ordering is clock-wise, starting with left. This may seem weird, + but it corresponds to X/Y or W/H used elsewhere. Left and right are + on X axis, so they are defined first. + + ''' + def __init__(self, l=0, t=0, r=0, b=0): self.l = l # left self.t = t # top @@ -76,3 +99,49 @@ def __repr__(self): return 'Borders(l={0.l},t={0.t},r={0.r},b={0.b})'.format(self) + +class ClipStack: + + '''Stack of clipping regions.''' + + def __init__(self): + self.stack = [] + + def push(self, x, y, w, h): + newclip = Rect(x, y, w, h) + if len(self.stack): + oldclip = self.stack[-1] + newclip = self.intersect(oldclip, newclip) + self.stack.append(newclip) + + def pop(self): + self.stack.pop() + + def test(self, x, y): + # no clip rectangle on stack => passed + if not len(self.stack): + return True + # test against top clip rect from stack + clip = self.stack[-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) + diff -r 6ca8b2d221c3 -r 4e72fd2a0e14 tuikit/container.py --- a/tuikit/container.py Sun Oct 09 11:17:42 2011 +0200 +++ b/tuikit/container.py Sun Oct 09 13:06:58 2011 +0200 @@ -81,30 +81,30 @@ child.emit('resize') - def draw(self, screen, x=0, y=0): + def draw(self, driver, x=0, y=0): if self.hidden: return - screen.pushclip(x, y, self.width, self.height) + driver.clipstack.push(x, y, self.width, self.height) if self.colorprefix: - screen.pushcolorprefix(self.colorprefix) + driver.pushcolorprefix(self.colorprefix) - Widget.draw(self, screen, x, y) + Widget.draw(self, driver, x, y) for child in [x for x in self.children if not x.allowlayout]: - child.draw(screen, x + child.x, y + child.y) + child.draw(driver, x + child.x, y + child.y) l, t, r, b = self.borders - screen.pushclip(x+l, y+t, self.width-l-r, self.height-t-b) + driver.clipstack.push(x+l, y+t, self.width-l-r, self.height-t-b) for child in [x for x in self.children if x.allowlayout]: - child.draw(screen, x + child.x, y + child.y) + child.draw(driver, x + child.x, y + child.y) - screen.popclip() + driver.clipstack.pop() if self.colorprefix: - screen.popcolorprefix() - screen.popclip() + driver.popcolorprefix() + driver.clipstack.pop() def mousedown(self, ev): diff -r 6ca8b2d221c3 -r 4e72fd2a0e14 tuikit/driver_curses.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/driver_curses.py Sun Oct 09 13:06:58 2011 +0200 @@ -0,0 +1,511 @@ +# -*- coding: utf-8 -*- + +import curses +import curses.ascii +import curses.wrapper +import locale +import logging + +from tuikit.common import Size, Rect, ClipStack + + +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 DriverCurses: + 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): + '''Set driver attributes to default values.''' + self.screen = None + self.size = Size() + self.cursor = None + self.clipstack = 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.colorprefix = [] # stack of color prefixes + self.inputqueue = [] + self.mbtnstack = [] + + self.log = logging.getLogger('tuikit') + + # 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 = '┤' + + def init(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() + mainfunc() + curses.wrapper(main) + + + ## colors, 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): + # add prefix if 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) + + + def pushcolorprefix(self, name): + self.colorprefix.append(name) + + + def popcolorprefix(self): + self.colorprefix.pop() + + + ## 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 puts(self, x, y, s): + for c in s: + self.putch(x, y, c) + x += 1 + + + def hline(self, x, y, w, c=' '): + if isinstance(c, 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.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 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 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 [] + + 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.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 + + 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 diff -r 6ca8b2d221c3 -r 4e72fd2a0e14 tuikit/driver_dummy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tuikit/driver_dummy.py Sun Oct 09 13:06:58 2011 +0200 @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +'''Dummy driver. + +Implements basic driver interface. +This is useful for debugging or when writing new driver. + +@author: Radek Brich + +''' + +import logging + +from tuikit.common import Size, ClipStack + + +class DriverDummy: + + '''Dummy driver class''' + + def __init__(self): + '''Initialize instance attributes''' + self.log = logging.getLogger('tuikit') + self.size = Size() + self.clipstack = ClipStack() + + def start(self, mainfunc): + '''Start driver and run mainfunc.''' + self.size.w, self.size.h = 80, 25 + mainfunc() + + + # colors + + def setcolor(self, name, desc): + '''Define color name. + + name - name of color (e.g. 'normal', 'active') + desc - color description - foreground, background, attributes (e.g. 'black on white, bold') + + ''' + self.log.info('DummyDriver.setcolor(name=%r, desc=%r)', name, desc) + + def pushcolor(self, name): + self.log.info('DummyDriver.pushcolor(name=%r)', name) + + def popcolor(self): + self.log.info('DummyDriver.popcolor()') + + + # drawing + + def erase(self): + '''Clear screen.''' + self.log.info('DummyDriver.erase()') + + def puts(self, x, y, s): + '''Output string to specified coordinates.''' + self.log.info('DummyDriver.puts(x=%r, y=%r, s=%r)', x, y, s) + + def commit(self): + '''Commit changes to the screen.''' + self.log.info('DummyDriver.commit()') + + + # input + + def process_input(self, timeout=None): + '''Process input, return list of events. + + This dummy implementation just returns 'q' and Escape key presses. + + ''' + events = [('keypress', None, 'q'), ('keypress', 'escape', None)] + return events +