tuikit/driver/cursesw.py
author Radek Brich <radek.brich@devl.cz>
Wed, 03 Sep 2014 21:56:20 +0200
changeset 113 6796adfdc7eb
parent 106 tuikit/driver/curses.py@abcadb7e2ef1
parent 111 tuikit/driver/curses.py@b055add74b18
child 118 8c7970520632
permissions -rw-r--r--
Merge. Due to my schizophrenia, I've accidentally forked my own code. The other set of changes were found in another computer.

import curses.ascii
import math
import logging

from tuikit.driver.driver import Driver
from tuikit.core.events import ResizeEvent, KeypressEvent
from tuikit.core.coords import Point


class CursesWDriver(Driver):

    key_names = {
        '\t':                   'tab',
        '\n':                   'enter',
        '\x1b':                 'escape',
    }

    key_map = {
        curses.KEY_UP:          'up',
        curses.KEY_DOWN:        'down',
        curses.KEY_LEFT:        'left',
        curses.KEY_RIGHT:       'right',
        curses.KEY_IC:          'insert',
        curses.KEY_DC:          'delete',
        curses.KEY_HOME:        'home',
        curses.KEY_END:         'end',
        curses.KEY_PPAGE:       'pageup',
        curses.KEY_NPAGE:       'pagedown',
        curses.KEY_BACKSPACE:   'backspace',
        curses.KEY_BTAB:        'shift+tab',
    }
    for _i in range(1, 13):
        key_map[curses.KEY_F0 + _i] = 'f' + str(_i)

    color_map = {
        '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,
    }

    def __init__(self):
        Driver.__init__(self)
        self._log = logging.getLogger(__name__)
        self.stdscr = None
        self.cursor = None
        self.colors = {}     # maps names to curses attributes
        self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
        self._mouse_last_pos = None  # Point
        self._mouse_last_bstate = None

    ## initialization, finalization ##

    def init(self):
        """Initialize curses"""
        self.stdscr = curses.initscr()
        curses.start_color()
        curses.use_default_colors()
        curses.noecho()
        curses.cbreak()
        self.stdscr.keypad(1)
        self.stdscr.immedok(0)

        self.size.h, self.size.w = self.stdscr.getmaxyx()

        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 close(self):
        self.stdscr.keypad(0)
        curses.echo()
        curses.nocbreak()
        curses.endwin()

    ## drawing ##

    def clear(self):
        self.stdscr.erase()

    def putch(self, ch, x, y):
        try:
            if isinstance(ch, int):
                self.stdscr.addch(y, x, ch)
            elif isinstance(ch, str) and len(ch) == 1:
                self.stdscr.addstr(y, x, ch)
            else:
                raise TypeError('Integer or one-char string is required.')
        except curses.error as e:
            self._log.exception('putch(%r, %s, %s) error:' % (ch, x, y))

    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(char, x + bufx, y + bufy)

    def flush(self):
        if self.cursor:
            self.stdscr.move(self.cursor.y, self.cursor.x)
            curses.curs_set(True)
        else:
            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

    ## input, events ##

    def getevents(self, timeout=None):
        """Process input, return list of events.

        timeout -- float, in seconds (None=infinite)

        Returns:
            [('event', param1, ...), ...]

        """
        # Set timeout
        if timeout is None:
            # wait indefinitely
            curses.cbreak()
        elif timeout > 0:
            # wait
            timeout_tenths = math.ceil(timeout * 10)
            curses.halfdelay(timeout_tenths)
        else:
            # timeout = 0 -> no wait
            self.stdscr.nodelay(1)

        # Get key or char
        c = self.stdscr.get_wch()

        res = []

        if c == -1:
            # Timeout
            return res
        elif c == curses.KEY_MOUSE:
            res += self._process_mouse()
        elif c == curses.KEY_RESIZE:
            self.size.h, self.size.w = self.stdscr.getmaxyx()
            res.append(ResizeEvent(self.size.w, self.size.h))
        elif isinstance(c, int):
            keyname, mods = self._split_keyname_mods(self.key_map[c])
            res.append(KeypressEvent(keyname, None, mods))
        else:
            keyname = self.key_names.get(c)
            res.append(KeypressEvent(keyname, c, set()))

        return res

    def _process_mouse(self):
        out = []
        try:
            _id, x, y, _z, bstate = curses.getmouse()
        except curses.error:
            return out

        pos = Point(x, y)
        if bstate & curses.REPORT_MOUSE_POSITION:
            if self._mouse_last_pos != pos:
                if self._mouse_last_pos:
                    relpos = pos - self._mouse_last_pos
                    out += [('mousemove', 0, pos, relpos)]
                self._mouse_last_pos = pos

        # we are interested only in changes, not buttons already pressed before event
        if self._mouse_last_bstate is not None:
            old = self._mouse_last_bstate
            new = bstate
            bstate = ~old & new
            self._mouse_last_bstate = new
        else:
            self._mouse_last_bstate = bstate

        if bstate & curses.BUTTON1_PRESSED:
            out += [('mousedown', 1, pos)]
        if bstate & curses.BUTTON2_PRESSED:
            out += [('mousedown', 2, pos)]
        if bstate & curses.BUTTON3_PRESSED:
            out += [('mousedown', 3, pos)]
        if bstate & curses.BUTTON1_RELEASED:
            out += [('mouseup', 1, pos)]
        if bstate & curses.BUTTON2_RELEASED:
            out += [('mouseup', 2, pos)]
        if bstate & curses.BUTTON3_RELEASED:
            out += [('mouseup', 3, pos)]

        # reset last pos when pressed/released
        if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'):
            self._mouse_last_pos = None

        return out

    def _split_keyname_mods(self, keyname):
        """Parse keynames in form "shift+tab", return (keyname, mod)."""
        mod_set = set()
        if '+' in keyname:
            parts = keyname.split('+')
            for mod in parts[:-1]:
                assert(mod in ('shift', 'alt', 'ctrl', 'meta'))
                mod_set.add(mod)
            keyname = parts[-1]

        return keyname, mod_set


driver_class = CursesWDriver