Update SDL driver: Enlarge char, attr to 32 bits, 64 bits per terminal cell. Colors and attributes are complete, only blink does not work.
# -*- coding: utf-8 -*-
'''PyGame driver.'''
import pygame
import logging
from tuikit.driver import Driver
from tuikit.common import Coords, Size
class TerminalScreen:
'''Provide character-level output to screen SDL surface.
This is performance bottleneck and should be optimized as much as possible.
'''
def __init__(self):
fontselect = 'dejavusansmono,liberationmono,freemono'
self.font = pygame.font.SysFont(fontselect, 14)
self.font_bold = pygame.font.SysFont(fontselect, 14, True)
# get advance of some random char (all should be same in monospace font)
advance = self.font.metrics('Q')[0][4]
height = self.font.get_height()
self.charsize = Size(advance, height)
self.ascent = self.font.get_ascent()
# choose self.render() implementation
if hasattr(self.font, 'render_glyph'):
self.render_char = self.render_glyph
else:
self.render_char = self.render_noglyph
self.chars = None
self.attrs = None
self.default_attr = None
self.current_attr = None
def set_default_attr(self, fg, bg, flags):
self.default_attr = (fg, bg, flags)
def set_attr(self, fg, bg, flags):
self.current_attr = (fg, bg, flags)
def reset_attr(self):
self.current_attr = self.default_attr
def reset(self, w, h):
self.w, self.h = w, h
numchars = w * h
self.chars = [' '] * numchars
self.attrs = [self.default_attr] * numchars
def clear(self):
numchars = self.w * self.h
for pos in range(numchars):
self.chars[pos] = ' '
self.attrs[pos] = self.default_attr
def putch(self, x, y, c):
pos = y * self.w + x
self.chars[pos] = c
self.attrs[pos] = self.current_attr
def update(self, surface):
pos = 0
for y in range(self.h):
for x in range(self.w):
fgcolor, bgcolor, flags = self.attrs[pos]
c = self.chars[pos]
self.render_char(surface, x, y, c,
fgcolor, bgcolor, flags)
pos += 1
def render_glyph(self, screen, x, y, c, fgcolor, bgcolor, flags):
'''Render using render_glyph and metrics.
This is the correct way, but the output seems same as of render_noglyph
and this implementation requires patching PyGame to work.
This implements render() method. See render_noglyph for other implementation.
'''
# draw background
dest = Coords(x * self.charsize.w, y * self.charsize.h)
if bgcolor != self.default_attr[1]:
screen.fill(bgcolor, pygame.Rect(dest.x, dest.y, self.charsize.w, self.charsize.h))
if c == ' ':
return
# choose font
if flags == 1:
font = self.font_bold
else:
font = self.font
# render character, get metrics
surface = font.render_glyph(c, True, fgcolor, bgcolor)
metrics = font.metrics(c)[0]
minx, maxx, miny, maxy, advance = metrics
height, ascent = self.charsize.h, self.ascent
# clips origin and area of rendered character according to metrics
startx, starty = 0, 0
if minx < 0:
startx = abs(minx)
minx = 0
if maxy > ascent:
starty = maxy - ascent
maxy -= starty
if ascent - miny > height:
miny = ascent - height
if maxx > advance:
maxx = advance
# draw character
dest.x += minx
dest.y += ascent - maxy
area = pygame.Rect(startx, starty, maxx - minx, maxy - miny)
screen.blit(surface, tuple(dest), area)
def render_noglyph(self, screen, x, y, c, fgcolor, bgcolor, attr):
'''Render character using normal text rendering.
This implements render() method. See render_glyph for other implementation.
'''
if attr == 'bold':
font = self.font_bold
else:
font = self.font
# render character, get metrics
surface = font.render(c, True, fgcolor, bgcolor)
metrics = font.metrics(c)[0]
minx = metrics[0]
startx = 0
if minx < 0:
startx = abs(minx)
# draw background
dest = Coords(x * self.charsize.w, y * self.charsize.h)
screen.fill(bgcolor, pygame.Rect(dest.x, dest.y, self.charsize.w, self.charsize.h))
# draw character
area = pygame.Rect(startx, 0, self.charsize.w, self.charsize.h)
screen.blit(surface, tuple(dest), area)
class DriverPygame(Driver):
'''PyGame driver class.'''
keymap = {
pygame.K_ESCAPE : 'escape',
pygame.K_TAB : 'tab',
pygame.K_RETURN : 'enter',
pygame.K_BACKSPACE : 'backspace',
pygame.K_F1 : 'f1',
pygame.K_F2 : 'f2',
pygame.K_F3 : 'f3',
pygame.K_F4 : 'f4',
pygame.K_F5 : 'f5',
pygame.K_F6 : 'f6',
pygame.K_F7 : 'f7',
pygame.K_F8 : 'f8',
pygame.K_F9 : 'f9',
pygame.K_F10 : 'f10',
pygame.K_F11 : 'f11',
pygame.K_F12 : 'f12',
pygame.K_INSERT : 'insert',
pygame.K_DELETE : 'delete',
pygame.K_HOME : 'home',
pygame.K_END : 'end',
pygame.K_PAGEUP : 'pageup',
pygame.K_PAGEDOWN : 'pagedown',
pygame.K_UP : 'up',
pygame.K_DOWN : 'down',
pygame.K_LEFT : 'left',
pygame.K_RIGHT : 'right',
pygame.K_PRINT : 'print',
pygame.K_SCROLLOCK : 'scrollock',
pygame.K_PAUSE : 'pause',
}
colormap = {
'black' : (0,0,0),
'blue' : (23,23,178),
'green' : (23,178,23),
'cyan' : (23,178,178),
'red' : (178,23,23),
'magenta' : (178,23,178),
'yellow' : (178,103,23),
'white' : (178,178,178),
'intenseblack' : (104,104,104),
'intenseblue' : (84,84,255),
'intensegreen' : (84,255,84),
'intensecyan' : (84,255,255),
'intensered' : (255,84,84),
'intensemagenta': (255,84,255),
'intenseyellow' : (255,255,84),
'intensewhite' : (255,255,255),
}
def __init__(self):
'''Initialize instance attributes'''
Driver.__init__(self)
self.log = logging.getLogger('tuikit')
self.screen = None
self.size.w, self.size.h = 120, 40 # screen size in characters
self.term = None
self.charsize = Size(16, 8) # character size in pixels
self.last_keypress = None
self.last_key = None
self.colors = {} # maps names to curses attributes
self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
def start(self, mainfunc):
pygame.init()
self.term = TerminalScreen()
self.charsize = self.term.charsize
self.resize_screen()
self.term.set_default_attr(self.colormap['white'], self.colormap['black'], 0)
self.term.reset_attr()
mainfunc()
def resize_screen(self):
mode = self.size.w * self.charsize.w, self.size.h * self.charsize.h
self.screen = pygame.display.set_mode(mode, pygame.RESIZABLE)
self.term.reset(self.size.w, self.size.h)
## input ##
def getevents(self, timeout=None):
'''Process input, return list of events.'''
events = []
for ev in pygame.event.get():
# mouse
if ev.type == pygame.MOUSEMOTION:
mx = ev.pos[0] // self.charsize.w
my = ev.pos[1] // self.charsize.h
events.append(('mousemove', mx, my))
elif ev.type in (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP):
mx = ev.pos[0] // self.charsize.w
my = ev.pos[1] // self.charsize.h
evname = {pygame.MOUSEBUTTONDOWN: 'mousedown', pygame.MOUSEBUTTONUP: 'mouseup'}
events.append((evname[ev.type], mx, my, ev.button))
# keyboard
elif ev.type == pygame.KEYDOWN:
keypress = self.keypress_from_pygame_event(ev)
if keypress:
events.append(keypress)
self.last_keypress = keypress
self.last_key = ev.key
pygame.time.set_timer(pygame.USEREVENT, 200)
elif ev.type == pygame.USEREVENT: # repeat last key press
events.append(self.last_keypress)
pygame.time.set_timer(pygame.USEREVENT, 50)
elif ev.type == pygame.KEYUP:
if ev.key == self.last_key:
pygame.time.set_timer(pygame.USEREVENT, 0)
# window
elif ev.type == pygame.VIDEORESIZE:
neww, newh = ev.w // self.charsize.w, ev.h // self.charsize.h
if neww != self.size.w or newh != self.size.h:
self.size.w, self.size.h = neww, newh
self.resize_screen()
events.append(('resize',))
elif ev.type == pygame.QUIT:
events.append(('quit',))
else:
self.log.warning('Unknown PyGame event: %r', ev.type)
return events
def keypress_from_pygame_event(self, ev):
if ev.key in self.keymap:
keypress = ('keypress', self.keymap[ev.key], None)
elif ev.unicode:
keypress = ('keypress', None, ev.unicode)
else:
self.log.debug('Unknown key: key=%r unicode=%r' % (ev.key, ev.unicode))
keypress = None
return keypress
## drawing ##
def erase(self):
'''Clear screen.'''
self.term.clear()
self.screen.fill(self.term.default_attr[1])
def putch(self, x, y, c):
if not self.clipstack.test(x, y):
return
self.term.putch(x, y, c)
def commit(self):
'''Commit changes to the screen.'''
self.term.update(self.screen)
pygame.display.flip()
## colors ##
def _parsecolor(self, name, attr=None):
name = name.lower().strip()
if attr == 'bold':
name = 'intense' + name
return self.colormap[name]
def _parseattrs(self, attrs):
res = ''
for a in attrs:
a = a.lower().strip()
if a == 'bold':
res = 1
return res
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')
'''
parts = desc.split(',')
fg, bg = parts[0].split(' on ')
attrs = parts[1:]
attr = self._parseattrs(attrs)
fg = self._parsecolor(fg, attr)
bg = self._parsecolor(bg)
self.colors[name] = (fg, bg, attr)
def pushcolor(self, name):
'''Add color on top of stack and use this color for following output.'''
# add prefix if such color is available
if len(self.colorprefix):
prefixname = self.colorprefix[-1] + name
if prefixname in self.colors:
name = prefixname
col = self.colors[name]
self.current_color = col
self.colorstack.append(col)
def popcolor(self):
'''Remove color from top of stack and use new top color for following output.'''
self.colorstack.pop()
if len(self.colorstack):
col = self.colorstack[-1]
self.term.set_attr(col[0], col[1], col[2])
else:
self.term.reset_attr()
## cursor ##
def showcursor(self, x, y):
'''Set cursor to be shown at x, y coordinates.'''
def hidecursor(self):
'''Hide cursor.'''
driverclass = DriverPygame