tkvt100.py   [plain text]


# Twisted, the Framework of Your Internet
# Copyright (C) 2001-2002 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

"""Module to emulate a VT100 terminal in Tkinter.

Maintainer: U{Paul Swartz <z3p@twistedmatrix.com>}
"""

import Tkinter, tkFont
import ansi
import string

ttyFont = None#tkFont.Font(family = 'Courier', size = 10)
fontWidth, fontHeight = None,None#max(map(ttyFont.measure, string.letters+string.digits)), int(ttyFont.metrics()['linespace'])

colorKeys = (
    'b', 'r', 'g', 'y', 'l', 'm', 'c', 'w',
    'B', 'R', 'G', 'Y', 'L', 'M', 'C', 'W'
)

colorMap = {
    'b': '#000000', 'r': '#c40000', 'g': '#00c400', 'y': '#c4c400',
    'l': '#000080', 'm': '#c400c4', 'c': '#00c4c4', 'w': '#c4c4c4',
    'B': '#626262', 'R': '#ff0000', 'G': '#00ff00', 'Y': '#ffff00',
    'L': '#0000ff', 'M': '#ff00ff', 'C': '#00ffff', 'W': '#ffffff',
}

class VT100Frame(Tkinter.Frame):
    def __init__(self, *args, **kw):
        global ttyFont, fontHeight, fontWidth
        ttyFont = tkFont.Font(family = 'Courier', size = 10)
        fontWidth, fontHeight = max(map(ttyFont.measure, string.letters+string.digits)), int(ttyFont.metrics()['linespace'])
        self.width = kw.get('width', 80)
        self.height = kw.get('height', 25)
        self.callback = kw['callback']
        del kw['callback']
        kw['width'] = w = fontWidth * self.width
        kw['height'] = h = fontHeight * self.height
        Tkinter.Frame.__init__(self, *args, **kw)
        self.canvas = Tkinter.Canvas(bg='#000000', width=w, height=h)
        self.canvas.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
        self.canvas.bind('<Key>', self.keyPressed)
        self.canvas.bind('<1>', lambda x: 'break')
        self.canvas.bind('<Up>', self.upPressed)
        self.canvas.bind('<Down>', self.downPressed)
        self.canvas.bind('<Left>', self.leftPressed)
        self.canvas.bind('<Right>', self.rightPressed)
        self.canvas.focus()

        self.ansiParser = ansi.AnsiParser(ansi.ColorText.WHITE, ansi.ColorText.BLACK)
        self.ansiParser.writeString = self.writeString
        self.ansiParser.parseCursor = self.parseCursor
        self.ansiParser.parseErase = self.parseErase
        #for (a, b) in colorMap.items():
        #    self.canvas.tag_config(a, foreground=b)
        #    self.canvas.tag_config('b'+a, background=b)
        #self.canvas.tag_config('underline', underline=1)

        self.x = 0 
        self.y = 0
        self.cursor = self.canvas.create_rectangle(0,0,fontWidth-1,fontHeight-1,fill='green',outline='green')

    def _delete(self, sx, sy, ex, ey):
        csx = sx*fontWidth + 1
        csy = sy*fontHeight + 1
        cex = ex*fontWidth + 3
        cey = ey*fontHeight + 3
        items = self.canvas.find_overlapping(csx,csy, cex,cey)
        for item in items:
            self.canvas.delete(item)

    def _write(self, ch, fg, bg):
        if self.x == self.width:
            self.x = 0
            self.y+=1
            if self.y == self.height:
                [self.canvas.move(x,0,-fontHeight) for x in self.canvas.find_all()]
                self.y-=1
        canvasX = self.x*fontWidth + 1
        canvasY = self.y*fontHeight + 1
        items = self.canvas.find_overlapping(canvasX, canvasY, canvasX+2, canvasY+2)
        if items:
            [self.canvas.delete(item) for item in items]
        if bg:
            self.canvas.create_rectangle(canvasX, canvasY, canvasX+fontWidth-1, canvasY+fontHeight-1, fill=bg, outline=bg)
        self.canvas.create_text(canvasX, canvasY, anchor=Tkinter.NW, font=ttyFont, text=ch, fill=fg)
        self.x+=1
            
    def write(self, data):
        #print self.x,self.y,repr(data)
        #if len(data)>5: raw_input()
        self.ansiParser.parseString(data)
        self.canvas.delete(self.cursor)
        canvasX = self.x*fontWidth + 1
        canvasY = self.y*fontHeight + 1
        self.cursor = self.canvas.create_rectangle(canvasX,canvasY,canvasX+fontWidth-1,canvasY+fontHeight-1, fill='green', outline='green')
        self.canvas.lower(self.cursor)

    def writeString(self, i):
        if not i.display:
            return
        fg = colorMap[i.fg]
        bg = i.bg != 'b' and colorMap[i.bg]
        for ch in i.text:
            b = ord(ch)
            if b == 7: # bell
                self.bell() 
            elif b == 8: # BS
                if self.x:
                    self.x-=1
            elif b == 9: # TAB
                [self._write(' ',fg,bg) for i in range(8)]
            elif b == 10:
                if self.y == self.height-1:
                    self._delete(0,0,self.width,0)
                    [self.canvas.move(x,0,-fontHeight) for x in self.canvas.find_all()]
                else:   
                    self.y+=1
            elif b == 13:
                self.x = 0
            elif 32 <= b < 127:
                self._write(ch, fg, bg)

    def parseErase(self, erase):
        if ';' in erase:
            end = erase[-1]
            parts = erase[:-1].split(';')
            [self.parseErase(x+end) for x in parts]
            return
        start = 0
        x,y = self.x, self.y
        if len(erase) > 1:
            start = int(erase[:-1])
        if erase[-1] == 'J':
            if start == 0: 
                self._delete(x,y,self.width,self.height)
            else:
                self._delete(0,0,self.width,self.height)
                self.x = 0
                self.y = 0 
        elif erase[-1] == 'K':
            if start == 0:
                self._delete(x,y,self.width,y)
            elif start == 1:
                self._delete(0,y,x,y)
                self.x = 0
            else:
                self._delete(0,y,self.width,y)
                self.x = 0
        elif erase[-1] == 'P':
            self._delete(x,y,x+start,y)

    def parseCursor(self, cursor):
        #if ';' in cursor and cursor[-1]!='H':
        #    end = cursor[-1]
        #    parts = cursor[:-1].split(';')
        #    [self.parseCursor(x+end) for x in parts]
        #    return
        start = 1
        if len(cursor) > 1 and cursor[-1]!='H':
            start = int(cursor[:-1])
        if cursor[-1] == 'C':
            self.x+=start
        elif cursor[-1] == 'D':
            self.x-=start
        elif cursor[-1]=='d':
            self.y=start-1
        elif cursor[-1]=='G':
            self.x=start-1
        elif cursor[-1]=='H':
            if len(cursor)>1:
                y,x = map(int, cursor[:-1].split(';'))
                y-=1
                x-=1
            else:
                x,y=0,0
            self.x = x
            self.y = y

    def keyPressed(self, event):
        if self.callback and event.char:
            self.callback(event.char)
        return 'break'

    def upPressed(self, event):
        self.callback('\x1bOA')

    def downPressed(self, event):
        self.callback('\x1bOB')

    def rightPressed(self, event):
        self.callback('\x1bOC')

    def leftPressed(self, event):
        self.callback('\x1bOD')