tuikit/driver/cursesw.py
changeset 110 cf3d49cdd6e2
parent 109 105b1affc3c2
child 111 b055add74b18
equal deleted inserted replaced
109:105b1affc3c2 110:cf3d49cdd6e2
       
     1 import curses.ascii
       
     2 import math
       
     3 import logging
       
     4 
       
     5 from tuikit.driver.driver import Driver
       
     6 
       
     7 
       
     8 class CursesWDriver(Driver):
       
     9 
       
    10     key_names = {
       
    11         '\t':                   'tab',
       
    12         '\n':                   'enter',
       
    13         '\x1b':                 'escape',
       
    14     }
       
    15 
       
    16     key_map = {
       
    17         curses.KEY_UP:          'up',
       
    18         curses.KEY_DOWN:        'down',
       
    19         curses.KEY_LEFT:        'left',
       
    20         curses.KEY_RIGHT:       'right',
       
    21         curses.KEY_IC:          'insert',
       
    22         curses.KEY_DC:          'delete',
       
    23         curses.KEY_HOME:        'home',
       
    24         curses.KEY_END:         'end',
       
    25         curses.KEY_PPAGE:       'pageup',
       
    26         curses.KEY_NPAGE:       'pagedown',
       
    27         curses.KEY_BACKSPACE:   'backspace',
       
    28         curses.KEY_BTAB:        'shift+tab',
       
    29     }
       
    30     for _i in range(1, 13):
       
    31         key_map[curses.KEY_F0 + _i] = 'f' + str(_i)
       
    32 
       
    33     color_map = {
       
    34         'default':      (-1,                    0),
       
    35         'black':        (curses.COLOR_BLACK,    0),
       
    36         'blue':         (curses.COLOR_BLUE,     0),
       
    37         'green':        (curses.COLOR_GREEN,    0),
       
    38         'cyan':         (curses.COLOR_CYAN,     0),
       
    39         'red':          (curses.COLOR_RED,      0),
       
    40         'magenta':      (curses.COLOR_MAGENTA,  0),
       
    41         'brown':        (curses.COLOR_YELLOW,   0),
       
    42         'lightgray':    (curses.COLOR_WHITE,    0),
       
    43         'gray':         (curses.COLOR_BLACK,    curses.A_BOLD),
       
    44         'lightblue':    (curses.COLOR_BLUE,     curses.A_BOLD),
       
    45         'lightgreen':   (curses.COLOR_GREEN,    curses.A_BOLD),
       
    46         'lightcyan':    (curses.COLOR_CYAN,     curses.A_BOLD),
       
    47         'lightred':     (curses.COLOR_RED,      curses.A_BOLD),
       
    48         'lightmagenta': (curses.COLOR_MAGENTA,  curses.A_BOLD),
       
    49         'yellow':       (curses.COLOR_YELLOW,   curses.A_BOLD),
       
    50         'white':        (curses.COLOR_WHITE,    curses.A_BOLD),
       
    51     }
       
    52 
       
    53     attr_map = {
       
    54         'bold':         curses.A_BOLD,
       
    55         'underline':    curses.A_UNDERLINE,
       
    56         'standout':     curses.A_STANDOUT,  # inverse bg/fg
       
    57         'blink':        curses.A_BLINK,
       
    58     }
       
    59 
       
    60     def __init__(self):
       
    61         Driver.__init__(self)
       
    62         self._log = logging.getLogger(__name__)
       
    63         self.stdscr = None
       
    64         self.cursor = None
       
    65         self.colors = {}     # maps names to curses attributes
       
    66         self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
       
    67         self._mouse_last_pos = (None, None)
       
    68         self._mouse_last_bstate = None
       
    69 
       
    70     ## initialization, finalization ##
       
    71 
       
    72     def init(self):
       
    73         """Initialize curses"""
       
    74         self.stdscr = curses.initscr()
       
    75         curses.start_color()
       
    76         curses.use_default_colors()
       
    77         curses.noecho()
       
    78         curses.cbreak()
       
    79         self.stdscr.keypad(1)
       
    80         self.stdscr.immedok(0)
       
    81 
       
    82         self.size.h, self.size.w = self.stdscr.getmaxyx()
       
    83 
       
    84         curses.curs_set(False)  # hide cursor
       
    85         curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
       
    86         curses.mouseinterval(0)  # do not wait to detect clicks, we use only press/release
       
    87 
       
    88     def close(self):
       
    89         self.stdscr.keypad(0)
       
    90         curses.echo()
       
    91         curses.nocbreak()
       
    92         curses.endwin()
       
    93 
       
    94     ## drawing ##
       
    95 
       
    96     def clear(self):
       
    97         self.stdscr.erase()
       
    98 
       
    99     def putch(self, ch, x, y):
       
   100         try:
       
   101             if isinstance(ch, int):
       
   102                 self.stdscr.addch(y, x, ch)
       
   103             elif isinstance(ch, str) and len(ch) == 1:
       
   104                 self.stdscr.addstr(y, x, ch)
       
   105             else:
       
   106                 raise TypeError('Integer or one-char string is required.')
       
   107         except curses.error as e:
       
   108             self._log.exception('putch(%r, %s, %s) error:' % (ch, x, y))
       
   109 
       
   110     def draw(self, buffer, x=0, y=0):
       
   111         for bufy in range(buffer.size.h):
       
   112             for bufx in range(buffer.size.w):
       
   113                 char, attr_desc = buffer.get(bufx, bufy)
       
   114                 self.setattr(attr_desc)
       
   115                 self.putch(char, x + bufx, y + bufy)
       
   116 
       
   117     def flush(self):
       
   118         if self.cursor:
       
   119             self.stdscr.move(self.cursor.y, self.cursor.x)
       
   120             curses.curs_set(True)
       
   121         else:
       
   122             curses.curs_set(False)
       
   123         self.stdscr.refresh()
       
   124 
       
   125     ## colors, attributes ##
       
   126 
       
   127     def setattr(self, attr_desc):
       
   128         """Set attribute to be used for subsequent draw operations."""
       
   129         attr = self.colors.get(attr_desc, None)
       
   130         if attr is None:
       
   131             # first encountered `attr_desc`, initialize
       
   132             fg, bg, attrs = self._parse_attr_desc(attr_desc)
       
   133             fgcol, fgattr = self.color_map[fg]
       
   134             bgcol, _bgattr = self.color_map[bg]
       
   135             colpair = self._getcolorpair(fgcol, bgcol)
       
   136             attr = curses.color_pair(colpair) | self._parseattrs(attrs) | fgattr
       
   137             self.colors[attr_desc] = attr
       
   138         self.stdscr.attrset(attr)
       
   139 
       
   140     def _getcolorpair(self, fgcol, bgcol):
       
   141         pair = (fgcol, bgcol)
       
   142         if pair in self.colorpairs:
       
   143             return self.colorpairs[pair]
       
   144         num = len(self.colorpairs) + 1
       
   145         curses.init_pair(num, fgcol, bgcol)
       
   146         self.colorpairs[pair] = num
       
   147         return num
       
   148 
       
   149     def _parseattrs(self, attrs):
       
   150         res = 0
       
   151         for a in attrs:
       
   152             res = res | self.attr_map[a]
       
   153         return res
       
   154 
       
   155     ## input, events ##
       
   156 
       
   157     def getevents(self, timeout=None):
       
   158         """Process input, return list of events.
       
   159 
       
   160         timeout -- float, in seconds (None=infinite)
       
   161 
       
   162         Returns:
       
   163             [('event', param1, ...), ...]
       
   164 
       
   165         """
       
   166         # Set timeout
       
   167         if timeout is None:
       
   168             # wait indefinitely
       
   169             curses.cbreak()
       
   170         elif timeout > 0:
       
   171             # wait
       
   172             timeout_tenths = math.ceil(timeout * 10)
       
   173             curses.halfdelay(timeout_tenths)
       
   174         else:
       
   175             # timeout = 0 -> no wait
       
   176             self.stdscr.nodelay(1)
       
   177 
       
   178         # Get key or char
       
   179         c = self.stdscr.get_wch()
       
   180 
       
   181         res = []
       
   182 
       
   183         if c == -1:
       
   184             # Timeout
       
   185             return res
       
   186         elif c == curses.KEY_MOUSE:
       
   187             res += self._process_mouse()
       
   188         elif c == curses.KEY_RESIZE:
       
   189             self.size.h, self.size.w = self.stdscr.getmaxyx()
       
   190             res += [('resize', self.size.w, self.size.h)]
       
   191         elif isinstance(c, int):
       
   192             keyname, mod = self._split_keyname_mod(self.key_map[c])
       
   193             res += [('keypress', keyname, None, mod)]
       
   194         else:
       
   195             keyname = self.key_names.get(c)
       
   196             res += [('keypress', keyname, c, set())]
       
   197 
       
   198         return res
       
   199 
       
   200     def _process_mouse(self):
       
   201         out = []
       
   202         try:
       
   203             _id, x, y, _z, bstate = curses.getmouse()
       
   204         except curses.error:
       
   205             return out
       
   206 
       
   207         if bstate & curses.REPORT_MOUSE_POSITION:
       
   208             if self._mouse_last_pos != (x, y):
       
   209                 if self._mouse_last_pos[0] is not None:
       
   210                     relx = x - (self._mouse_last_pos[0] or 0)
       
   211                     rely = y - (self._mouse_last_pos[1] or 0)
       
   212                     out += [('mousemove', 0, x, y, relx, rely)]
       
   213                 self._mouse_last_pos = (x, y)
       
   214 
       
   215         # we are interested only in changes, not buttons already pressed before event
       
   216         if self._mouse_last_bstate is not None:
       
   217             old = self._mouse_last_bstate
       
   218             new = bstate
       
   219             bstate = ~old & new
       
   220             self._mouse_last_bstate = new
       
   221         else:
       
   222             self._mouse_last_bstate = bstate
       
   223 
       
   224         if bstate & curses.BUTTON1_PRESSED:
       
   225             out += [('mousedown', 1, x, y)]
       
   226         if bstate & curses.BUTTON2_PRESSED:
       
   227             out += [('mousedown', 2, x, y)]
       
   228         if bstate & curses.BUTTON3_PRESSED:
       
   229             out += [('mousedown', 3, x, y)]
       
   230         if bstate & curses.BUTTON1_RELEASED:
       
   231             out += [('mouseup', 1, x, y)]
       
   232         if bstate & curses.BUTTON2_RELEASED:
       
   233             out += [('mouseup', 2, x, y)]
       
   234         if bstate & curses.BUTTON3_RELEASED:
       
   235             out += [('mouseup', 3, x, y)]
       
   236 
       
   237         # reset last pos when pressed/released
       
   238         if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'):
       
   239             self._mouse_last_pos = (None, None)
       
   240 
       
   241         return out
       
   242 
       
   243     def _split_keyname_mod(self, keyname):
       
   244         """Parse keynames in form "shift+tab", return (keyname, mod)."""
       
   245         mod_set = set()
       
   246         if '+' in keyname:
       
   247             parts = keyname.split('+')
       
   248             for mod in parts[:-1]:
       
   249                 assert(mod in ('shift', 'alt', 'ctrl', 'meta'))
       
   250                 mod_set.add(mod)
       
   251             keyname = parts[-1]
       
   252 
       
   253         return keyname, mod_set
       
   254 
       
   255 
       
   256 driver_class = CursesWDriver