Refactor Driver, CursesDriver: draw, setattr, input.
authorRadek Brich <radek.brich@devl.cz>
Sat, 15 Mar 2014 14:43:47 +0100
changeset 84 04dfb5ddf031
parent 83 ebe732b9ef19
child 85 6828c5b16087
Refactor Driver, CursesDriver: draw, setattr, input.
demos/02_curses.py
tuikit/driver/curses.py
tuikit/driver/driver.py
--- a/demos/02_curses.py	Sat Mar 15 11:05:12 2014 +0100
+++ b/demos/02_curses.py	Sat Mar 15 14:43:47 2014 +0100
@@ -18,7 +18,9 @@
 driver = CursesDriver()
 with driver:
     driver.draw(buffer)
+    buffer.setattr('red on blue, bold')
+    buffer.puts(8, 4, 'Hello!')
     driver.draw(buffer, 20, 10)
-    driver.stdscr.refresh()
-    driver.stdscr.getch()
+    driver.flush()
+    driver.getevents()
 
--- a/tuikit/driver/curses.py	Sat Mar 15 11:05:12 2014 +0100
+++ b/tuikit/driver/curses.py	Sat Mar 15 14:43:47 2014 +0100
@@ -8,17 +8,18 @@
 
 
 class CursesDriver(Driver):
+
     key_codes = (
         (0x09,                      'tab'           ),
         (0x0a,                      'enter'         ),
         (0x7f,                      'backspace'     ),
         (0x1b,                      'escape'        ),
-        (0x1b,0x4f,0x50,            'f1'            ),  # xterm
-        (0x1b,0x4f,0x51,            'f2'            ),  # xterm
-        (0x1b,0x4f,0x52,            'f3'            ),  # xterm
-        (0x1b,0x4f,0x53,            'f4'            ),  # xterm
-        (0x1b,0x5b,                 'CSI'           ),  # see csi_codes
-        (0x1b,0x5b,0x4d,            'mouse'         ),
+        (0x1b, 0x4f, 0x50,          'f1'            ),  # xterm
+        (0x1b, 0x4f, 0x51,          'f2'            ),  # xterm
+        (0x1b, 0x4f, 0x52,          'f3'            ),  # xterm
+        (0x1b, 0x4f, 0x53,          'f4'            ),  # xterm
+        (0x1b, 0x5b,                'CSI'           ),  # see csi_codes
+        (0x1b, 0x5b, 0x4d,          'mouse'         ),
     )
 
     # http://en.wikipedia.org/wiki/ANSI_escape_code
@@ -44,41 +45,41 @@
         (0x44,              1,      'left'          ),
         (0x46,              1,      'end'           ),  # xterm
         (0x48,              1,      'home'          ),  # xterm
-        (0x5b,0x41,         1,      'f1'            ),  # linux
-        (0x5b,0x42,         1,      'f2'            ),  # linux
-        (0x5b,0x43,         1,      'f3'            ),  # linux
-        (0x5b,0x44,         1,      'f4'            ),  # linux
-        (0x5b,0x45,         1,      'f5'            ),  # linux
+        (0x5b, 0x41,        1,      'f1'            ),  # linux
+        (0x5b, 0x42,        1,      'f2'            ),  # linux
+        (0x5b, 0x43,        1,      'f3'            ),  # linux
+        (0x5b, 0x44,        1,      'f4'            ),  # linux
+        (0x5b, 0x45,        1,      'f5'            ),  # linux
     )
 
     color_map = {
-        '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),
+        '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,
+        'bold':         curses.A_BOLD,
+        'underline':    curses.A_UNDERLINE,
+        'standout':     curses.A_STANDOUT,  # inverse bg/fg
+        'blink':        curses.A_BLINK,
     }
 
     def __init__(self):
-        '''Set driver attributes to default values.'''
         Driver.__init__(self)
         self.log = logging.getLogger('tuikit')
         self.stdscr = None
@@ -97,6 +98,7 @@
         """Initialize curses"""
         self.stdscr = curses.initscr()
         curses.start_color()
+        curses.use_default_colors()
         curses.noecho()
         curses.cbreak()
         self.stdscr.keypad(0)
@@ -114,80 +116,10 @@
         curses.nocbreak()
         curses.endwin()
 
-    def __enter__(self):
-        self.init()
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        self.close()
-
     ## drawing ##
 
-    def draw(self, buffer, x=0, y=0):
-        for bufy in range(buffer.size.h):
-            for bufx in range(buffer.size.w):
-                self.putch(x + bufx, y + bufy,
-                           buffer.get(bufx, bufy)[0])
-
-
-    ## 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 defcolor(self, name, desc):
-        """Define color name."""
-        parts = desc.split(',')
-        fgbg = parts[0].split(' on ', 1)
-        fg = fgbg[0]
-        bg = fgbg[1:] and fgbg[1] or 'black'
-        attrs = parts[1:]
-        fg, fgattr = self._parsecolor(fg)
-        bg, _bgattr = self._parsecolor(bg)
-        col = self._getcolorpair(fg, bg)
-        attr = self._parseattrs(attrs)
-        self.colors[name] = curses.color_pair(col) | fgattr | attr
-
-    def setcolor(self, name):
-        """Set defined color. Previous color is forgotten."""
-        self.stdscr.attrset(self.colors[name])
-
-    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.stdscr.attrset(attr)
-        self.colorstack.append(attr)
-
-    def popcolor(self):
-        self.colorstack.pop()
-        if len(self.colorstack):
-            attr = self.colorstack[-1]
-        else:
-            attr = 0
-        self.stdscr.attrset(attr)
-
-
-    ## drawing ##
+    def erase(self):
+        self.stdscr.erase()
 
     def putch(self, x, y, c):
         if not self.clipstack.test(x, y):
@@ -200,10 +132,14 @@
         except curses.error:
             pass
 
-    def erase(self):
-        self.stdscr.erase()
+    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(x + bufx, y + bufy, char)
 
-    def commit(self):
+    def flush(self):
         if self.cursor:
             self.stdscr.move(*self.cursor)
             curses.curs_set(True)
@@ -211,6 +147,35 @@
             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
 
     ## cursor ##
 
@@ -224,10 +189,52 @@
         curses.curs_set(False)
         self.cursor = None
 
+    ## input, events ##
 
-    ## input ##
+    def getevents(self, timeout=None):
+        """Process input, return list of events.
+
+        timeout -- float, in seconds (None=infinite)
+
+        Returns:
+            [('event', param1, ...), ...]
+
+        """
+        # empty queue -> fill
+        if len(self.inputqueue) == 0:
+            if timeout is not None:
+                timeout = math.ceil(timeout * 10)
+            self._inputqueue_fill(timeout)
+
+        res = []
+        while len(self.inputqueue):
+            c = self._inputqueue_get()
+
+            if c == curses.KEY_MOUSE:
+                res += self._process_mouse()
 
-    def inputqueue_fill(self, timeout=None):
+            elif c == curses.KEY_RESIZE:
+                self.size.h, self.size.w = self.stdscr.getmaxyx()
+                res.append(('resize',))
+
+            elif curses.ascii.isctrl(c):
+                self._inputqueue_unget(c)
+                res += self._process_control_chars()
+
+            elif 192 <= c <= 255:
+                self._inputqueue_unget(c)
+                res += self._process_utf8_chars()
+
+            elif curses.ascii.isprint(c):
+                res += [('keypress', None, str(chr(c)))]
+
+            else:
+                self._inputqueue_unget(c)
+                res += self._process_control_chars()
+
+        return res
+
+    def _inputqueue_fill(self, timeout=None):
         """Wait for curses input, add it to inputqueue.
 
         timeout -- int, tenths of second (None=infinite)
@@ -259,12 +266,7 @@
 
         self.stdscr.nodelay(0)
 
-
-    def inputqueue_top(self, num=0):
-        return self.inputqueue[-1-num]
-
-
-    def inputqueue_get(self):
+    def _inputqueue_get(self):
         c = None
         try:
             c = self.inputqueue.pop()
@@ -272,64 +274,20 @@
             pass
         return c
 
-
-    def inputqueue_get_wait(self):
+    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)
+                self._inputqueue_fill(0)
         return c
 
-
-    def inputqueue_unget(self, c):
+    def _inputqueue_unget(self, c):
         self.inputqueue.append(c)
 
-
-    def getevents(self, timeout=None):
-        '''Process input, return list of events.
-
-        timeout -- float, in seconds
-
-        '''
-        # empty queue -> fill
-        if len(self.inputqueue) == 0:
-            if timeout is not None:
-                timeout = math.ceil(timeout * 10)
-            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.stdscr.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.inputqueue_unget(c)
-                res += self.process_control_chars()
-
-        return res
-
-
-    def process_mouse(self):
+    def _process_mouse(self):
         try:
             _id, x, y, _z, bstate = curses.getmouse()
         except curses.error:
@@ -373,12 +331,11 @@
 
         return out
 
-
-    def process_utf8_chars(self):
+    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()
+            c = self._inputqueue_get_wait()
             utf.append(c)
             try:
                 uni = str(bytes(utf), 'utf-8')
@@ -387,15 +344,14 @@
                 continue
         raise Exception('Invalid UTF-8 sequence: %r' % utf)
 
-
-    def process_control_chars(self):
+    def _process_control_chars(self):
         codes = self.key_codes
         matchingcodes = []
         match = None
         consumed = []
 
         # consume next char, filter out matching codes
-        c = self.inputqueue_get_wait()
+        c = self._inputqueue_get_wait()
         consumed.append(c)
 
         while True:
@@ -416,9 +372,9 @@
             # match found and some sequencies still match -> continue
             if len(matchingcodes) > 0:
                 if len(self.inputqueue) == 0:
-                    self.inputqueue_fill(1)
+                    self._inputqueue_fill(1)
 
-            c = self.inputqueue_get()
+            c = self._inputqueue_get()
             if c:
                 consumed.append(c)
                 codes = matchingcodes
@@ -431,28 +387,27 @@
             # compare match to consumed, return unused chars
             l = len(match) - 1
             while len(consumed) > l:
-                self.inputqueue_unget(consumed[-1])
+                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]))
+                           ','.join(['0x%x' % x for x in consumed]))
             return [('keypress', 'Unknown', None)]
 
         if keyname == 'mouse':
-            return self.process_xterm_mouse()
+            return self._process_xterm_mouse()
 
         if keyname == 'CSI':
-            return self.process_control_sequence()
+            return self._process_control_sequence()
 
         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
+    def _process_xterm_mouse(self):
+        t = self._inputqueue_get_wait()
+        x = self._inputqueue_get_wait() - 0x21
+        y = self._inputqueue_get_wait() - 0x21
 
         out = []
 
@@ -485,10 +440,10 @@
 
         return out
 
-    def process_control_sequence(self):
+    def _process_control_sequence(self):
         codes = self.csi_codes
         debug_seq = [0x1b, 0x5b]
-        c = self.inputqueue_get_wait()
+        c = self._inputqueue_get_wait()
         debug_seq.append(c)
 
         # numeric parameters?
@@ -496,7 +451,7 @@
         if chr(c).isdigit():
             params.append(chr(c))
             while True:
-                c = self.inputqueue_get_wait()
+                c = self._inputqueue_get_wait()
                 debug_seq.append(c)
                 if chr(c).isdigit():
                     params[-1] += chr(c)
@@ -530,7 +485,7 @@
                 break
             else:
                 # more than one matching -> continue loop
-                c = self.inputqueue_get_wait()
+                c = self._inputqueue_get_wait()
                 debug_seq.append(c)
 
         # filter codes using first parameter
@@ -556,4 +511,3 @@
             mod = params[1] - 1
 
         return [('keypress', keyname, None, mod)]
-
--- a/tuikit/driver/driver.py	Sat Mar 15 11:05:12 2014 +0100
+++ b/tuikit/driver/driver.py	Sat Mar 15 14:43:47 2014 +0100
@@ -16,8 +16,8 @@
         self.size = Size()
         #: Clipping region stack.
         self.clipstack = ClipStack()
-        #: Stack of color prefixes.
-        self.colorprefix = []
+
+    ## initialization, finalization ##
 
     def init(self):
         """Initialize the driver and screen."""
@@ -27,26 +27,60 @@
         """Clean up the screen etc."""
         pass
 
+    def __enter__(self):
+        self.init()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.close()
+
+    ## drawing ##
+
+    def erase(self):
+        pass
+
+    def putch(self, x, y, c):
+        pass
+
     def draw(self, buffer, x=0, y=0):
         for bufy in range(buffer.size.h):
             for bufx in range(buffer.size.w):
                 print(buffer.get(bufx, bufy)[0], end='')
             print()
 
+    def flush(self):
+        pass
 
-    ## drawing ##
+    ## colors, attributes ##
+
+    def setattr(self, attr_desc):
+        """Set attribute to be used for subsequent draw operations."""
+        pass
 
-    def fill_clip(self, c=' '):
-        """Fill current clip region."""
-        rect = self.clipstack.top()
-        self.fill(rect.x, rect.y, rect.w, rect.h, c)
+    def _parse_attr_desc(self, attr_desc):
+        parts = attr_desc.split(',')
+        fgbg = parts[0].split(' on ', 1)
+        fg = fgbg[0].strip().lower()
+        bg = fgbg[1:] and fgbg[1].strip().lower() or 'default'
+        attrs = (part.strip().lower() for part in parts[1:])
+        return fg, bg, attrs
 
+    ## cursor ##
 
-    ## colors ##
+    def showcursor(self, x, y):
+        pass
+
+    def hidecursor(self):
+        pass
+
+    ## input, events ##
 
-    def pushcolorprefix(self, name):
-        self.colorprefix.append(name)
+    def getevents(self, timeout=None):
+        """Process input, return list of events.
+
+        timeout -- float, in seconds (None=infinite)
 
-    def popcolorprefix(self):
-        self.colorprefix.pop()
+        Returns:
+            [('event', param1, ...), ...]
 
+        """
+        return []