1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4#
5
6"""Module to emulate a VT100 terminal in Tkinter.
7
8Maintainer: Paul Swartz
9"""
10
11import string
12import tkinter as Tkinter
13import tkinter.font as tkFont
14
15from . import ansi
16
17ttyFont = None  # tkFont.Font(family = 'Courier', size = 10)
18fontWidth, fontHeight = (
19    None,
20    None,
21)  # max(map(ttyFont.measure, string.letters+string.digits)), int(ttyFont.metrics()['linespace'])
22
23colorKeys = (
24    "b",
25    "r",
26    "g",
27    "y",
28    "l",
29    "m",
30    "c",
31    "w",
32    "B",
33    "R",
34    "G",
35    "Y",
36    "L",
37    "M",
38    "C",
39    "W",
40)
41
42colorMap = {
43    "b": "#000000",
44    "r": "#c40000",
45    "g": "#00c400",
46    "y": "#c4c400",
47    "l": "#000080",
48    "m": "#c400c4",
49    "c": "#00c4c4",
50    "w": "#c4c4c4",
51    "B": "#626262",
52    "R": "#ff0000",
53    "G": "#00ff00",
54    "Y": "#ffff00",
55    "L": "#0000ff",
56    "M": "#ff00ff",
57    "C": "#00ffff",
58    "W": "#ffffff",
59}
60
61
62class VT100Frame(Tkinter.Frame):
63    def __init__(self, *args, **kw):
64        global ttyFont, fontHeight, fontWidth
65        ttyFont = tkFont.Font(family="Courier", size=10)
66        fontWidth = max(map(ttyFont.measure, string.ascii_letters + string.digits))
67        fontHeight = int(ttyFont.metrics()["linespace"])
68        self.width = kw.get("width", 80)
69        self.height = kw.get("height", 25)
70        self.callback = kw["callback"]
71        del kw["callback"]
72        kw["width"] = w = fontWidth * self.width
73        kw["height"] = h = fontHeight * self.height
74        Tkinter.Frame.__init__(self, *args, **kw)
75        self.canvas = Tkinter.Canvas(bg="#000000", width=w, height=h)
76        self.canvas.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
77        self.canvas.bind("<Key>", self.keyPressed)
78        self.canvas.bind("<1>", lambda x: "break")
79        self.canvas.bind("<Up>", self.upPressed)
80        self.canvas.bind("<Down>", self.downPressed)
81        self.canvas.bind("<Left>", self.leftPressed)
82        self.canvas.bind("<Right>", self.rightPressed)
83        self.canvas.focus()
84
85        self.ansiParser = ansi.AnsiParser(ansi.ColorText.WHITE, ansi.ColorText.BLACK)
86        self.ansiParser.writeString = self.writeString
87        self.ansiParser.parseCursor = self.parseCursor
88        self.ansiParser.parseErase = self.parseErase
89        # for (a, b) in colorMap.items():
90        #    self.canvas.tag_config(a, foreground=b)
91        #    self.canvas.tag_config('b'+a, background=b)
92        # self.canvas.tag_config('underline', underline=1)
93
94        self.x = 0
95        self.y = 0
96        self.cursor = self.canvas.create_rectangle(
97            0, 0, fontWidth - 1, fontHeight - 1, fill="green", outline="green"
98        )
99
100    def _delete(self, sx, sy, ex, ey):
101        csx = sx * fontWidth + 1
102        csy = sy * fontHeight + 1
103        cex = ex * fontWidth + 3
104        cey = ey * fontHeight + 3
105        items = self.canvas.find_overlapping(csx, csy, cex, cey)
106        for item in items:
107            self.canvas.delete(item)
108
109    def _write(self, ch, fg, bg):
110        if self.x == self.width:
111            self.x = 0
112            self.y += 1
113            if self.y == self.height:
114                [self.canvas.move(x, 0, -fontHeight) for x in self.canvas.find_all()]
115                self.y -= 1
116        canvasX = self.x * fontWidth + 1
117        canvasY = self.y * fontHeight + 1
118        items = self.canvas.find_overlapping(canvasX, canvasY, canvasX + 2, canvasY + 2)
119        if items:
120            [self.canvas.delete(item) for item in items]
121        if bg:
122            self.canvas.create_rectangle(
123                canvasX,
124                canvasY,
125                canvasX + fontWidth - 1,
126                canvasY + fontHeight - 1,
127                fill=bg,
128                outline=bg,
129            )
130        self.canvas.create_text(
131            canvasX, canvasY, anchor=Tkinter.NW, font=ttyFont, text=ch, fill=fg
132        )
133        self.x += 1
134
135    def write(self, data):
136        self.ansiParser.parseString(data)
137        self.canvas.delete(self.cursor)
138        canvasX = self.x * fontWidth + 1
139        canvasY = self.y * fontHeight + 1
140        self.cursor = self.canvas.create_rectangle(
141            canvasX,
142            canvasY,
143            canvasX + fontWidth - 1,
144            canvasY + fontHeight - 1,
145            fill="green",
146            outline="green",
147        )
148        self.canvas.lower(self.cursor)
149
150    def writeString(self, i):
151        if not i.display:
152            return
153        fg = colorMap[i.fg]
154        bg = i.bg != "b" and colorMap[i.bg]
155        for ch in i.text:
156            b = ord(ch)
157            if b == 7:  # bell
158                self.bell()
159            elif b == 8:  # BS
160                if self.x:
161                    self.x -= 1
162            elif b == 9:  # TAB
163                [self._write(" ", fg, bg) for index in range(8)]
164            elif b == 10:
165                if self.y == self.height - 1:
166                    self._delete(0, 0, self.width, 0)
167                    [
168                        self.canvas.move(x, 0, -fontHeight)
169                        for x in self.canvas.find_all()
170                    ]
171                else:
172                    self.y += 1
173            elif b == 13:
174                self.x = 0
175            elif 32 <= b < 127:
176                self._write(ch, fg, bg)
177
178    def parseErase(self, erase):
179        if ";" in erase:
180            end = erase[-1]
181            parts = erase[:-1].split(";")
182            [self.parseErase(x + end) for x in parts]
183            return
184        start = 0
185        x, y = self.x, self.y
186        if len(erase) > 1:
187            start = int(erase[:-1])
188        if erase[-1] == "J":
189            if start == 0:
190                self._delete(x, y, self.width, self.height)
191            else:
192                self._delete(0, 0, self.width, self.height)
193                self.x = 0
194                self.y = 0
195        elif erase[-1] == "K":
196            if start == 0:
197                self._delete(x, y, self.width, y)
198            elif start == 1:
199                self._delete(0, y, x, y)
200                self.x = 0
201            else:
202                self._delete(0, y, self.width, y)
203                self.x = 0
204        elif erase[-1] == "P":
205            self._delete(x, y, x + start, y)
206
207    def parseCursor(self, cursor):
208        # if ';' in cursor and cursor[-1]!='H':
209        #    end = cursor[-1]
210        #    parts = cursor[:-1].split(';')
211        #    [self.parseCursor(x+end) for x in parts]
212        #    return
213        start = 1
214        if len(cursor) > 1 and cursor[-1] != "H":
215            start = int(cursor[:-1])
216        if cursor[-1] == "C":
217            self.x += start
218        elif cursor[-1] == "D":
219            self.x -= start
220        elif cursor[-1] == "d":
221            self.y = start - 1
222        elif cursor[-1] == "G":
223            self.x = start - 1
224        elif cursor[-1] == "H":
225            if len(cursor) > 1:
226                y, x = map(int, cursor[:-1].split(";"))
227                y -= 1
228                x -= 1
229            else:
230                x, y = 0, 0
231            self.x = x
232            self.y = y
233
234    def keyPressed(self, event):
235        if self.callback and event.char:
236            self.callback(event.char)
237        return "break"
238
239    def upPressed(self, event):
240        self.callback("\x1bOA")
241
242    def downPressed(self, event):
243        self.callback("\x1bOB")
244
245    def rightPressed(self, event):
246        self.callback("\x1bOC")
247
248    def leftPressed(self, event):
249        self.callback("\x1bOD")
250