├── .gitignore ├── TermEmulator ├── __init__.py ├── TermEmulatorDemo.py └── TermEmulator.py ├── HISTORY.txt ├── README.rst ├── PKG-INFO ├── setup.py └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /TermEmulator/__init__.py: -------------------------------------------------------------------------------- 1 | from TermEmulator import V102Terminal 2 | -------------------------------------------------------------------------------- /HISTORY.txt: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.0.3 (unreleased) 5 | ------------------ 6 | 7 | - Python 3 support 8 | [ajdavis] 9 | - Handle UTF-8 characters 10 | [ajdavis] 11 | 12 | 13 | 1.0.2 (2011-07-11) 14 | ------------------ 15 | 16 | - Fix broken egg 17 | [gotcha] 18 | 19 | 20 | 1.0.1 (2011-07-09) 21 | ------------------ 22 | 23 | - Eggified 24 | [gotcha] 25 | 26 | 1.0 (2008-04-18) 27 | ---------------- 28 | 29 | - First release (as .tgz) 30 | [sivachandran] 31 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | TermEmulator 2 | ============ 3 | 4 | ``TermEmulator`` is a pure python module for emulating VT100 terminal programs. 5 | 6 | It handles V100 special characters and most important escape sequences. 7 | It also handles graphics rendition which specifies text style (i.e. bold, italics), 8 | foreground color and background color. 9 | 10 | The handled escape sequences are ``CUU``, ``CUD``, ``CUF``, ``CUB``, ``CHA``, 11 | ``CUP``, ``ED``, ``EL``, ``VPA`` and ``SGR``. 12 | 13 | Development 14 | =========== 15 | 16 | ``TermEmulator`` source code and tracker are at https://github.com/sivachandran/TermEmulator. 17 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: TermEmulator 3 | Version: 1.0 4 | Summary: Emulator for V100 terminal programs 5 | Home-page: http://sourceforge.net/projects/termemulator/ 6 | Author: Siva Chandran P 7 | Author-email: siva.chandran.p@gmail.com 8 | License: LGPL 9 | Description: TermEmulator is a pure python module for emulating 10 | VT100 terminal programs. It handles V100 special 11 | characters and most important escape sequences. 12 | It also handles graphics rendition which specifies 13 | text style(i.e. bold, italics), foreground color 14 | and background color. The handled escape sequences 15 | are CUU, CUD, CUF, CUB, CHA, CUP, ED, EL, VPA 16 | and SGR. 17 | Platform: any 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = '1.0.3dev' 4 | 5 | long_description = (open('README.rst').read() + 6 | '\n\n' + open('HISTORY.txt').read()) 7 | 8 | 9 | setup(name='TermEmulator', 10 | version=version, 11 | description="Emulator for V100 terminal programs", 12 | long_description=long_description, 13 | author="Siva Chandran P", 14 | author_email="siva.chandran.p@gmail.com", 15 | url="https://github.com/sivachandran/TermEmulator", 16 | license="LGPL", 17 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 18 | include_package_data=True, 19 | zip_safe=True, 20 | install_requires=[ 21 | 'setuptools', 22 | ], 23 | entry_points=""" 24 | # -*- Entry points: -*- 25 | """, 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- /TermEmulator/TermEmulatorDemo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | 5 | import os 6 | import sys 7 | import pty 8 | import threading 9 | import select 10 | import wx 11 | 12 | import fcntl 13 | import termios 14 | import struct 15 | import tty 16 | 17 | import TermEmulator 18 | 19 | ID_TERMINAL = 1 20 | 21 | def PrintStringAsAscii(s): 22 | import string 23 | for ch in s: 24 | if ch in string.printable: 25 | print(ch, end="") 26 | else: 27 | print(ord(ch), end="") 28 | 29 | class TermEmulatorDemo(wx.Frame): 30 | def __init__(self): 31 | wx.Frame.__init__(self, None, wx.ID_ANY, "TermEmulator Demo", \ 32 | size = (700, 500)) 33 | 34 | self.Bind(wx.EVT_CLOSE, self.OnClose) 35 | 36 | vbox = wx.BoxSizer(wx.VERTICAL) 37 | hbox1 = wx.BoxSizer(wx.HORIZONTAL) 38 | 39 | self.st1 = wx.StaticText(self, wx.ID_ANY, "Program path:") 40 | hbox1.Add(self.st1, 0, wx.ALIGN_CENTER | wx.LEFT, 10) 41 | 42 | self.tc1 = wx.TextCtrl(self, wx.ID_ANY) 43 | self.tc1.SetValue("/bin/bash") 44 | hbox1.Add(self.tc1, 1, wx.ALIGN_CENTER) 45 | 46 | self.st2 = wx.StaticText(self, wx.ID_ANY, "Arguments:") 47 | hbox1.Add(self.st2, 0, wx.ALIGN_CENTER | wx.LEFT, 10) 48 | 49 | self.tc2 = wx.TextCtrl(self, wx.ID_ANY) 50 | hbox1.Add(self.tc2, 1, wx.ALIGN_CENTER) 51 | 52 | self.b1 = wx.Button(self, wx.ID_ANY, "Run") 53 | hbox1.Add(self.b1, 0, wx.LEFT | wx.RIGHT, 10) 54 | self.b1.Bind(wx.EVT_BUTTON, self.OnRun, id = self.b1.GetId()) 55 | 56 | vbox.Add(hbox1, 0, wx.EXPAND | wx.HORIZONTAL | wx.TOP | wx.BOTTOM, 5) 57 | 58 | hbox2 = wx.BoxSizer(wx.HORIZONTAL) 59 | 60 | self.st3 = wx.StaticText(self, wx.ID_ANY, "Terminal Size, Rows:") 61 | hbox2.Add(self.st3, 0, wx.ALIGN_CENTER | wx.LEFT, 10) 62 | 63 | self.tc3 = wx.TextCtrl(self, wx.ID_ANY) 64 | self.tc3.SetValue("24") 65 | hbox2.Add(self.tc3, 1, wx.ALIGN_CENTER) 66 | 67 | self.st4 = wx.StaticText(self, wx.ID_ANY, "Columns:") 68 | hbox2.Add(self.st4, 0, wx.ALIGN_CENTER | wx.LEFT, 10) 69 | 70 | self.tc4 = wx.TextCtrl(self, wx.ID_ANY) 71 | self.tc4.SetValue("80") 72 | hbox2.Add(self.tc4, 1, wx.ALIGN_CENTER) 73 | 74 | self.b2 = wx.Button(self, wx.ID_ANY, "Resize") 75 | hbox2.Add(self.b2, 0, wx.LEFT | wx.RIGHT, 10) 76 | self.b2.Bind(wx.EVT_BUTTON, self.OnResize, id = self.b2.GetId()) 77 | 78 | self.cb1 = wx.CheckBox(self, wx.ID_ANY, "Disable text coloring") 79 | hbox2.Add(self.cb1, 0, wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, 10) 80 | 81 | vbox.Add(hbox2, 0, wx.EXPAND | wx.HORIZONTAL | wx.TOP | wx.BOTTOM, 5) 82 | 83 | self.txtCtrlTerminal = wx.TextCtrl(self, ID_TERMINAL, 84 | style = wx.TE_MULTILINE 85 | | wx.TE_DONTWRAP) 86 | font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, 87 | wx.FONTWEIGHT_NORMAL, False) 88 | self.txtCtrlTerminal.SetFont(font) 89 | 90 | self.txtCtrlTerminal.Bind(wx.EVT_CHAR, self.OnTerminalChar, 91 | id = ID_TERMINAL) 92 | 93 | self.txtCtrlTerminal.Bind(wx.EVT_KEY_DOWN, self.OnTerminalKeyDown, 94 | id = ID_TERMINAL) 95 | 96 | self.txtCtrlTerminal.Bind(wx.EVT_KEY_UP, self.OnTerminalKeyUp, 97 | id = ID_TERMINAL) 98 | 99 | vbox.Add(self.txtCtrlTerminal, 1, wx.EXPAND | wx.ALL) 100 | self.SetSizer(vbox) 101 | 102 | self.termRows = 24 103 | self.termCols = 80 104 | 105 | self.FillScreen() 106 | 107 | self.linesScrolledUp = 0 108 | self.scrolledUpLinesLen = 0 109 | 110 | self.termEmulator = TermEmulator.V102Terminal(self.termRows, 111 | self.termCols) 112 | self.termEmulator.SetCallback(self.termEmulator.CALLBACK_SCROLL_UP_SCREEN, 113 | self.OnTermEmulatorScrollUpScreen) 114 | self.termEmulator.SetCallback(self.termEmulator.CALLBACK_UPDATE_LINES, 115 | self.OnTermEmulatorUpdateLines) 116 | self.termEmulator.SetCallback(self.termEmulator.CALLBACK_UPDATE_CURSOR_POS, 117 | self.OnTermEmulatorUpdateCursorPos) 118 | self.termEmulator.SetCallback(self.termEmulator.CALLBACK_UPDATE_WINDOW_TITLE, 119 | self.OnTermEmulatorUpdateWindowTitle) 120 | self.termEmulator.SetCallback(self.termEmulator.CALLBACK_UNHANDLED_ESC_SEQ, 121 | self.OnTermEmulatorUnhandledEscSeq) 122 | 123 | self.isRunning = False 124 | self.UpdateUI() 125 | 126 | self.Show(True) 127 | 128 | def FillScreen(self): 129 | """ 130 | Fills the screen with blank lines so that we can update terminal 131 | dirty lines quickly. 132 | """ 133 | text = "" 134 | for i in range(self.termRows): 135 | for j in range(self.termCols): 136 | text += ' ' 137 | text += '\n' 138 | 139 | text = text.rstrip('\n') 140 | self.txtCtrlTerminal.SetValue(text) 141 | 142 | def UpdateUI(self): 143 | self.tc1.Enable(not self.isRunning) 144 | self.tc2.Enable(not self.isRunning) 145 | self.b1.Enable(not self.isRunning) 146 | self.b2.Enable(self.isRunning) 147 | self.txtCtrlTerminal.Enable(self.isRunning) 148 | 149 | def OnRun(self, event): 150 | path = self.tc1.GetValue() 151 | basename = os.path.basename(path) 152 | arglist = [ basename ] 153 | 154 | arguments = self.tc2.GetValue() 155 | if arguments != "": 156 | for arg in arguments.split(' '): 157 | arglist.append(arg) 158 | 159 | self.termRows = int(self.tc3.GetValue()) 160 | self.termCols = int(self.tc4.GetValue()) 161 | 162 | rows, cols = self.termEmulator.GetSize() 163 | if rows != self.termRows or cols != self.termCols: 164 | self.termEmulator.Resize (self.termRows, self.termCols) 165 | 166 | processPid, processIO = pty.fork() 167 | if processPid == 0: # child process 168 | os.execl(path, *arglist) 169 | 170 | print("Child process pid {}".format(processPid)) 171 | 172 | # Sets raw mode 173 | #tty.setraw(processIO) 174 | 175 | # Sets the terminal window size 176 | fcntl.ioctl(processIO, termios.TIOCSWINSZ, 177 | struct.pack("hhhh", self.termRows, self.termCols, 0, 0)) 178 | 179 | tcattrib = termios.tcgetattr(processIO) 180 | tcattrib[3] = tcattrib[3] & ~termios.ICANON 181 | termios.tcsetattr(processIO, termios.TCSAFLUSH, tcattrib) 182 | 183 | self.processPid = processPid 184 | self.processIO = processIO 185 | self.processOutputNotifierThread = threading.Thread( 186 | target = self.__ProcessOuputNotifierRun) 187 | self.waitingForOutput = True 188 | self.stopOutputNotifier = False 189 | self.processOutputNotifierThread.start() 190 | self.isRunning = True 191 | self.UpdateUI() 192 | 193 | def OnResize(self, event): 194 | self.termRows = int(self.tc3.GetValue()) 195 | self.termCols = int(self.tc4.GetValue()) 196 | 197 | # Resize emulator 198 | self.termEmulator.Resize(self.termRows, self.termCols) 199 | 200 | # Resize terminal 201 | fcntl.ioctl(self.processIO, termios.TIOCSWINSZ, 202 | struct.pack("hhhh", self.termRows, self.termCols, 0, 0)) 203 | 204 | self.FillScreen() 205 | self.UpdateDirtyLines(range(self.termRows)) 206 | 207 | def __ProcessIsAlive(self): 208 | try: 209 | pid, status = os.waitpid(self.processPid, os.WNOHANG) 210 | if pid == self.processPid and os.WIFEXITED(status): 211 | return False 212 | except: 213 | return False 214 | 215 | return True 216 | 217 | def __ProcessOuputNotifierRun(self): 218 | inpSet = [ self.processIO ] 219 | while (not self.stopOutputNotifier and self.__ProcessIsAlive()): 220 | if self.waitingForOutput: 221 | inpReady, outReady, errReady = select.select(inpSet, [], [], 0) 222 | if self.processIO in inpReady: 223 | self.waitingForOutput = False 224 | wx.CallAfter(self.ReadProcessOutput) 225 | 226 | if not self.__ProcessIsAlive(): 227 | self.isRunning = False 228 | wx.CallAfter(self.ReadProcessOutput) 229 | wx.CallAfter(self.UpdateUI) 230 | print("Process exited") 231 | 232 | print("Notifier thread exited") 233 | 234 | def SetTerminalRenditionStyle(self, style): 235 | fontStyle = wx.FONTSTYLE_NORMAL 236 | fontWeight = wx.FONTWEIGHT_NORMAL 237 | underline = False 238 | 239 | if style & self.termEmulator.RENDITION_STYLE_BOLD: 240 | fontWeight = wx.FONTWEIGHT_BOLD 241 | elif style & self.termEmulator.RENDITION_STYLE_DIM: 242 | fontWeight = wx.FONTWEIGHT_LIGHT 243 | 244 | if style & self.termEmulator.RENDITION_STYLE_ITALIC: 245 | fontStyle = wx.FONTSTYLE_ITALIC 246 | 247 | if style & self.termEmulator.RENDITION_STYLE_UNDERLINE: 248 | underline = True 249 | 250 | font = wx.Font(10, wx.FONTFAMILY_TELETYPE, fontStyle, fontWeight, 251 | underline) 252 | 253 | self.txtCtrlTerminal.SetFont(font) 254 | 255 | def SetTerminalRenditionForeground(self, fgcolor): 256 | if fgcolor != 0: 257 | if fgcolor == 1: 258 | self.txtCtrlTerminal.SetForegroundColour((0, 0, 0)) 259 | elif fgcolor == 2: 260 | self.txtCtrlTerminal.SetForegroundColour((255, 0, 0)) 261 | elif fgcolor == 3: 262 | self.txtCtrlTerminal.SetForegroundColour((0, 255, 0)) 263 | elif fgcolor == 4: 264 | self.txtCtrlTerminal.SetForegroundColour((255, 255, 0)) 265 | elif fgcolor == 5: 266 | self.txtCtrlTerminal.SetForegroundColour((0, 0, 255)) 267 | elif fgcolor == 6: 268 | self.txtCtrlTerminal.SetForegroundColour((255, 0, 255)) 269 | elif fgcolor == 7: 270 | self.txtCtrlTerminal.SetForegroundColour((0, 255, 255)) 271 | elif fgcolor == 8: 272 | self.txtCtrlTerminal.SetForegroundColour((255, 255, 255)) 273 | else: 274 | self.txtCtrlTerminal.SetForegroundColour((0, 0, 0)) 275 | 276 | def SetTerminalRenditionBackground(self, bgcolor): 277 | if bgcolor != 0: 278 | if bgcolor == 1: 279 | self.txtCtrlTerminal.SetBackgroundColour((0, 0, 0)) 280 | elif bgcolor == 2: 281 | self.txtCtrlTerminal.SetBackgroundColour((255, 0, 0)) 282 | elif bgcolor == 3: 283 | self.txtCtrlTerminal.SetBackgroundColour((0, 255, 0)) 284 | elif bgcolor == 4: 285 | self.txtCtrlTerminal.SetBackgroundColour((255, 255, 0)) 286 | elif bgcolor == 5: 287 | self.txtCtrlTerminal.SetBackgroundColour((0, 0, 255)) 288 | elif bgcolor == 6: 289 | self.txtCtrlTerminal.SetBackgroundColour((255, 0, 255)) 290 | elif bgcolor == 7: 291 | self.txtCtrlTerminal.SetBackgroundColour((0, 255, 255)) 292 | elif bgcolor == 8: 293 | self.txtCtrlTerminal.SetBackgroundColour((255, 255, 255)) 294 | else: 295 | self.txtCtrlTerminal.SetBackgroundColour((255, 255, 255)) 296 | 297 | def GetTextCtrlLineStart(self, lineNo): 298 | lineStart = self.scrolledUpLinesLen 299 | lineStart += (self.termCols + 1) * (lineNo - self.linesScrolledUp) 300 | return lineStart 301 | 302 | def UpdateCursorPos(self): 303 | row, col = self.termEmulator.GetCursorPos() 304 | 305 | lineNo = self.linesScrolledUp + row 306 | insertionPoint = self.GetTextCtrlLineStart(lineNo) 307 | insertionPoint += col 308 | self.txtCtrlTerminal.SetInsertionPoint(insertionPoint) 309 | 310 | def UpdateDirtyLines(self, dirtyLines = None): 311 | text = "" 312 | curStyle = 0 313 | curFgColor = 0 314 | curBgColor = 0 315 | 316 | #self.SetTerminalRenditionStyle(curStyle) 317 | self.SetTerminalRenditionForeground(curFgColor) 318 | self.SetTerminalRenditionBackground(curBgColor) 319 | 320 | screen = self.termEmulator.GetRawScreen() 321 | screenRows = self.termEmulator.GetRows() 322 | screenCols = self.termEmulator.GetCols() 323 | if dirtyLines == None: 324 | dirtyLines = self.termEmulator.GetDirtyLines() 325 | 326 | disableTextColoring = self.cb1.IsChecked() 327 | 328 | for row in dirtyLines: 329 | text = "" 330 | 331 | # finds the line starting and ending index 332 | lineNo = self.linesScrolledUp + row 333 | lineStart = self.GetTextCtrlLineStart(lineNo) 334 | #lineText = self.txtCtrlTerminal.GetLineText(lineNo) 335 | #lineEnd = lineStart + len(lineText) 336 | lineEnd = lineStart + self.termCols 337 | 338 | # delete the line content 339 | self.txtCtrlTerminal.Replace(lineStart, lineEnd, "") 340 | self.txtCtrlTerminal.SetInsertionPoint(lineStart) 341 | 342 | for col in range(screenCols): 343 | style, fgcolor, bgcolor = self.termEmulator.GetRendition(row, 344 | col) 345 | 346 | if not disableTextColoring and (curStyle != style 347 | or curFgColor != fgcolor \ 348 | or curBgColor != bgcolor): 349 | 350 | if text != "": 351 | self.txtCtrlTerminal.WriteText(text) 352 | text = "" 353 | 354 | if curStyle != style: 355 | curStyle = style 356 | #print("Setting style {}".format(curStyle)) 357 | if style == 0: 358 | self.txtCtrlTerminal.SetForegroundColour((0, 0, 0)) 359 | self.txtCtrlTerminal.SetBackgroundColour((255, 255, 255)) 360 | elif style & self.termEmulator.RENDITION_STYLE_INVERSE: 361 | self.txtCtrlTerminal.SetForegroundColour((255, 255, 255)) 362 | self.txtCtrlTerminal.SetBackgroundColour((0, 0, 0)) 363 | else: 364 | # skip other styles since TextCtrl doesn't support 365 | # multiple fonts(bold, italic and etc) 366 | pass 367 | 368 | if curFgColor != fgcolor: 369 | curFgColor = fgcolor 370 | #print("Setting foreground {}".format(curFgColor)) 371 | self.SetTerminalRenditionForeground(curFgColor) 372 | 373 | if curBgColor != bgcolor: 374 | curBgColor = bgcolor 375 | #print("Setting background {}".format(curBgColor)) 376 | self.SetTerminalRenditionBackground(curBgColor) 377 | 378 | text += screen[row][col] 379 | 380 | self.txtCtrlTerminal.WriteText(text) 381 | 382 | 383 | def OnTermEmulatorScrollUpScreen(self): 384 | blankLine = "\n" 385 | 386 | for i in range(self.termEmulator.GetCols()): 387 | blankLine += ' ' 388 | 389 | #lineLen = len(self.txtCtrlTerminal.GetLineText(self.linesScrolledUp)) 390 | lineLen = self.termCols 391 | self.txtCtrlTerminal.AppendText(blankLine) 392 | self.linesScrolledUp += 1 393 | self.scrolledUpLinesLen += lineLen + 1 394 | 395 | def OnTermEmulatorUpdateLines(self): 396 | self.UpdateDirtyLines() 397 | wx.YieldIfNeeded() 398 | 399 | def OnTermEmulatorUpdateCursorPos(self): 400 | self.UpdateCursorPos() 401 | 402 | def OnTermEmulatorUpdateWindowTitle(self, title): 403 | self.SetTitle(title) 404 | 405 | def OnTermEmulatorUnhandledEscSeq(self, escSeq): 406 | print("Unhandled escape sequence: [{}".format(escSeq)) 407 | 408 | def ReadProcessOutput(self): 409 | output = bytes("",'utf8') 410 | 411 | try: 412 | while True: 413 | data = os.read(self.processIO, 512) 414 | datalen = len(data) 415 | output += data 416 | 417 | if datalen < 512: 418 | break 419 | except: 420 | output = bytes("",'utf8') 421 | 422 | #print("Received: ", end="") 423 | #PrintStringAsAscii(output) 424 | #print("") 425 | 426 | self.termEmulator.ProcessInput(output.decode()) 427 | 428 | # resets text control's foreground and background 429 | self.txtCtrlTerminal.SetForegroundColour((0, 0, 0)) 430 | self.txtCtrlTerminal.SetBackgroundColour((255, 255, 255)) 431 | 432 | self.waitingForOutput = True 433 | 434 | def OnTerminalKeyDown(self, event): 435 | #print("KeyDown {}".format(event.GetKeyCode())) 436 | event.Skip() 437 | 438 | def OnTerminalKeyUp(self, event): 439 | #print("KeyUp {}".format(event.GetKeyCode())) 440 | event.Skip() 441 | 442 | def OnTerminalChar(self, event): 443 | if not self.isRunning: 444 | return 445 | 446 | ascii = event.GetKeyCode() 447 | #print("ASCII = {}".format(ascii)) 448 | 449 | keystrokes = None 450 | 451 | if ascii < 256: 452 | keystrokes = chr(ascii) 453 | elif ascii == wx.WXK_UP: 454 | keystrokes = "\033[A" 455 | elif ascii == wx.WXK_DOWN: 456 | keystrokes = "\033[B" 457 | elif ascii == wx.WXK_RIGHT: 458 | keystrokes = "\033[C" 459 | elif ascii == wx.WXK_LEFT: 460 | keystrokes = "\033[D" 461 | 462 | if keystrokes != None: 463 | #print("Sending:", end="") 464 | #PrintStringAsAscii(keystrokes) 465 | #print("") 466 | os.write(self.processIO, bytes(keystrokes,'utf-8')) 467 | 468 | def OnClose(self, event): 469 | if self.isRunning: 470 | self.stopOutputNotifier = True 471 | self.processOutputNotifierThread.join(None) 472 | 473 | event.Skip() 474 | 475 | if __name__ == '__main__': 476 | app = wx.App(0); 477 | termEmulatorDemo = TermEmulatorDemo() 478 | 479 | app.SetTopWindow(termEmulatorDemo) 480 | app.MainLoop() 481 | -------------------------------------------------------------------------------- /TermEmulator/TermEmulator.py: -------------------------------------------------------------------------------- 1 | # TermEmulator - Emulator for VT100 terminal programs 2 | # Copyright (C) 2008 Siva Chandran P 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, write to the Free 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | # 18 | # Siva Chandran P 19 | # siva.chandran.p@gmail.com 20 | 21 | """ 22 | Emulator for VT100 terminal programs. 23 | 24 | This module provides terminal emulation for VT100 terminal programs. It handles 25 | V100 special characters and most important escape sequences. It also handles 26 | graphics rendition which specifies text style(i.e. bold, italics), foreground color 27 | and background color. The handled escape sequences are CUU, CUD, CUF, CUB, CHA, 28 | CUP, ED, EL, VPA and SGR. 29 | """ 30 | 31 | from __future__ import print_function 32 | 33 | import sys 34 | import os 35 | import pty 36 | import select 37 | from array import * 38 | 39 | class V102Terminal: 40 | __ASCII_NUL = 0 # Null 41 | __ASCII_BEL = 7 # Bell 42 | __ASCII_BS = 8 # Backspace 43 | __ASCII_HT = 9 # Horizontal Tab 44 | __ASCII_LF = 10 # Line Feed 45 | __ASCII_VT = 11 # Vertical Tab 46 | __ASCII_FF = 12 # Form Feed 47 | __ASCII_CR = 13 # Carriage Return 48 | __ASCII_XON = 17 # Resume Transmission 49 | __ASCII_XOFF = 19 # Stop Transmission or Ignore Characters 50 | __ASCII_ESC = 27 # Escape 51 | __ASCII_SPACE = 32 # Space 52 | __ASCII_CSI = 153 # Control Sequence Introducer 53 | 54 | __ESCSEQ_CUU = 'A' # n A: Moves the cursor up n(default 1) times. 55 | __ESCSEQ_CUD = 'B' # n B: Moves the cursor down n(default 1) times. 56 | __ESCSEQ_CUF = 'C' # n C: Moves the cursor forward n(default 1) times. 57 | __ESCSEQ_CUB = 'D' # n D: Moves the cursor backward n(default 1) times. 58 | 59 | __ESCSEQ_CHA = 'G' # n G: Cursor horizontal absolute position. 'n' denotes 60 | # the column no(1 based index). Should retain the line 61 | # position. 62 | 63 | __ESCSEQ_CUP = 'H' # n ; m H: Moves the cursor to row n, column m. 64 | # The values are 1-based, and default to 1 (top left 65 | # corner). 66 | 67 | __ESCSEQ_ED = 'J' # n J: Clears part of the screen. If n is zero 68 | # (or missing), clear from cursor to end of screen. 69 | # If n is one, clear from cursor to beginning of the 70 | # screen. If n is two, clear entire screen. 71 | 72 | __ESCSEQ_EL = 'K' # n K: Erases part of the line. If n is zero 73 | # (or missing), clear from cursor to the end of the 74 | # line. If n is one, clear from cursor to beginning of 75 | # the line. If n is two, clear entire line. Cursor 76 | # position does not change. 77 | 78 | __ESCSEQ_VPA = 'd' # n d: Cursor vertical absolute position. 'n' denotes 79 | # the line no(1 based index). Should retain the column 80 | # position. 81 | 82 | __ESCSEQ_SGR = 'm' # n [;k] m: Sets SGR (Select Graphic Rendition) 83 | # parameters. After CSI can be zero or more parameters 84 | # separated with ;. With no parameters, CSI m is treated 85 | # as CSI 0 m (reset / normal), which is typical of most 86 | # of the ANSI codes. 87 | 88 | RENDITION_STYLE_BOLD = 1 89 | RENDITION_STYLE_DIM = 2 90 | RENDITION_STYLE_ITALIC = 4 91 | RENDITION_STYLE_UNDERLINE = 8 92 | RENDITION_STYLE_SLOW_BLINK = 16 93 | RENDITION_STYLE_FAST_BLINK = 32 94 | RENDITION_STYLE_INVERSE = 64 95 | RENDITION_STYLE_HIDDEN = 128 96 | 97 | CALLBACK_SCROLL_UP_SCREEN = 1 98 | CALLBACK_UPDATE_LINES = 2 99 | CALLBACK_UPDATE_CURSOR_POS = 3 100 | CALLBACK_UPDATE_WINDOW_TITLE = 4 101 | CALLBACK_UNHANDLED_ESC_SEQ = 5 102 | 103 | def __init__(self, rows, cols): 104 | """ 105 | Initializes the terminal with specified rows and columns. User can 106 | resize the terminal any time using Resize method. By default the screen 107 | is cleared(filled with blank spaces) and cursor positioned in the first 108 | row and first column. 109 | """ 110 | self.cols = cols 111 | self.rows = rows 112 | self.curX = 0 113 | self.curY = 0 114 | self.ignoreChars = False 115 | 116 | # special character handlers 117 | self.charHandlers = { 118 | self.__ASCII_NUL:self.__OnCharIgnore, 119 | self.__ASCII_BEL:self.__OnCharIgnore, 120 | self.__ASCII_BS:self.__OnCharBS, 121 | self.__ASCII_HT:self.__OnCharHT, 122 | self.__ASCII_LF:self.__OnCharLF, 123 | self.__ASCII_VT:self.__OnCharLF, 124 | self.__ASCII_FF:self.__OnCharLF, 125 | self.__ASCII_CR:self.__OnCharCR, 126 | self.__ASCII_XON:self.__OnCharXON, 127 | self.__ASCII_XOFF:self.__OnCharXOFF, 128 | self.__ASCII_ESC:self.__OnCharESC, 129 | self.__ASCII_CSI:self.__OnCharCSI, 130 | } 131 | 132 | # escape sequence handlers 133 | self.escSeqHandlers = { 134 | self.__ESCSEQ_CUU:self.__OnEscSeqCUU, 135 | self.__ESCSEQ_CUD:self.__OnEscSeqCUD, 136 | self.__ESCSEQ_CUF:self.__OnEscSeqCUF, 137 | self.__ESCSEQ_CUB:self.__OnEscSeqCUB, 138 | self.__ESCSEQ_CHA:self.__OnEscSeqCHA, 139 | self.__ESCSEQ_CUP:self.__OnEscSeqCUP, 140 | self.__ESCSEQ_ED:self.__OnEscSeqED, 141 | self.__ESCSEQ_EL:self.__OnEscSeqEL, 142 | self.__ESCSEQ_VPA:self.__OnEscSeqVPA, 143 | self.__ESCSEQ_SGR:self.__OnEscSeqSGR, 144 | } 145 | 146 | # defines the printable characters, only these characters are printed 147 | # on the terminal 148 | self.printableChars = "0123456789" 149 | self.printableChars += "abcdefghijklmnopqrstuvwxyz" 150 | self.printableChars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 151 | self.printableChars += """!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ """ 152 | self.printableChars += "\t" 153 | 154 | # terminal screen, its a list of string in which each string always 155 | # holds self.cols characters. If the screen doesn't contain any 156 | # character then it'll blank space 157 | self.screen = [] 158 | 159 | # terminal screen rendition, its a list of array of long. The first 160 | # 8 bits of the long holds the rendition style/attribute(i.e. bold, 161 | # italics and etc). The next 4 bits specifies the foreground color and 162 | # next 4 bits for background 163 | self.scrRendition = [] 164 | 165 | # current rendition 166 | self.curRendition = 0 167 | 168 | # list of dirty lines since last call to GetDirtyLines 169 | self.isLineDirty = [] 170 | 171 | for i in range(rows): 172 | line = array('u') 173 | rendition = array('L') 174 | 175 | for j in range(cols): 176 | line.append(u' ') 177 | rendition.append(0) 178 | 179 | self.screen.append(line) 180 | self.scrRendition.append(rendition) 181 | self.isLineDirty.append(False) 182 | 183 | # initializes callbacks 184 | self.callbacks = { 185 | self.CALLBACK_SCROLL_UP_SCREEN: None, 186 | self.CALLBACK_UPDATE_LINES: None, 187 | self.CALLBACK_UPDATE_CURSOR_POS: None, 188 | self.CALLBACK_UNHANDLED_ESC_SEQ: None, 189 | self.CALLBACK_UPDATE_WINDOW_TITLE: None, 190 | } 191 | 192 | # unparsed part of last input 193 | self.unparsedInput = None 194 | 195 | def GetRawScreen(self): 196 | """ 197 | Returns the screen as a list of strings. The list will have rows no. of 198 | strings and each string will have columns no. of characters. Blank space 199 | used represents no character. 200 | """ 201 | return self.screen 202 | 203 | def GetRawScreenRendition(self): 204 | """ 205 | Returns the screen as a list of array of long. The list will have rows 206 | no. of array and each array will have columns no. of longs. The first 207 | 8 bits of long represents rendition style like bold, italics and etc. 208 | The next 4 bits represents foreground color and next 4 bits for 209 | background color. 210 | """ 211 | return self.scrRendition 212 | 213 | def GetRows(self): 214 | """ 215 | Returns no. rows in the terminal 216 | """ 217 | return self.rows 218 | 219 | def GetCols(self): 220 | """ 221 | Returns no. cols in the terminal 222 | """ 223 | return self.cols 224 | 225 | def GetSize(self): 226 | """ 227 | Returns terminal rows and cols as tuple 228 | """ 229 | return (self.rows, self.cols) 230 | 231 | def Resize(self, rows, cols): 232 | """ 233 | Resizes the terminal to specified rows and cols. 234 | - If the new no. rows is less than existing no. rows then existing rows 235 | are deleted at top. 236 | - If the new no. rows is greater than existing no. rows then 237 | blank rows are added at bottom. 238 | - If the new no. cols is less than existing no. cols then existing cols 239 | are deleted at right. 240 | - If the new no. cols is greater than existing no. cols then new cols 241 | are added at right. 242 | """ 243 | if rows < self.rows: 244 | # remove rows at top 245 | for i in range(self.rows - rows): 246 | self.isLineDirty.pop(0) 247 | self.screen.pop(0) 248 | self.scrRendition.pop(0) 249 | 250 | elif rows > self.rows: 251 | # add blank rows at bottom 252 | for i in range(rows - self.rows): 253 | line = array('u') 254 | rendition = array('L') 255 | 256 | for j in range(self.cols): 257 | line.append(u' ') 258 | rendition.append(0) 259 | 260 | self.screen.append(line) 261 | self.scrRendition.append(rendition) 262 | self.isLineDirty.append(False) 263 | 264 | self.rows = rows 265 | 266 | if cols < self.cols: 267 | # remove cols at right 268 | for i in range(self.rows): 269 | self.screen[i] = self.screen[i][:cols - self.cols] 270 | for j in range(self.cols - cols): 271 | self.scrRendition[i].pop(len(self.scrRendition[i]) - 1) 272 | elif cols > self.cols: 273 | # add cols at right 274 | for i in range(self.rows): 275 | for j in range(cols - self.cols): 276 | self.screen[i].append(u' ') 277 | self.scrRendition[i].append(0) 278 | 279 | self.cols = cols 280 | 281 | def GetCursorPos(self): 282 | """ 283 | Returns cursor position as tuple 284 | """ 285 | return (self.curY, self.curX) 286 | 287 | def Clear(self): 288 | """ 289 | Clears the entire terminal screen 290 | """ 291 | self.ClearRect(0, 0, self.rows - 1, self.cols - 1) 292 | 293 | def ClearRect(self, startRow, startCol, endRow, endCol): 294 | """ 295 | Clears the terminal screen starting from startRow and startCol to 296 | endRow and EndCol. 297 | """ 298 | if startRow < 0: 299 | startRow = 0 300 | elif startRow >= self.rows: 301 | startRow = self.rows - 1 302 | 303 | if startCol < 0: 304 | startCol = 0 305 | elif startCol >= self.cols: 306 | startCol = self.cols - 1 307 | 308 | if endRow < 0: 309 | endRow = 0 310 | elif endRow >= self.rows: 311 | endRow = self.rows - 1 312 | 313 | if endCol < 0: 314 | endCol = 0 315 | elif endCol >= self.cols: 316 | endCol = self.cols - 1 317 | 318 | if startRow > endRow: 319 | startRow, endRow = endRow, startRow 320 | 321 | if startCol > endCol: 322 | startCol, endCol = endCol, startCol 323 | 324 | for i in range(startRow, endRow + 1): 325 | start = 0 326 | end = self.cols - 1 327 | 328 | if i == startRow: 329 | start = startCol 330 | elif i == endRow: 331 | end = endCol 332 | 333 | for j in range(start, end + 1): 334 | self.screen[i][j] = ' ' 335 | self.scrRendition[i][j] = 0 336 | 337 | if end + 1 > start: 338 | self.isLineDirty[i] = True 339 | 340 | def GetChar(self, row, col): 341 | """ 342 | Returns the character at the location specified by row and col. The 343 | row and col should be in the range 0..rows - 1 and 0..cols - 1." 344 | """ 345 | if row < 0 or row >= self.rows: 346 | return None 347 | 348 | if col < 0 or col >= self.cols: 349 | return None 350 | 351 | return self.screen[row][col] 352 | 353 | def GetRendition(self, row, col): 354 | """ 355 | Returns the screen rendition at the location specified by row and col. 356 | The returned value is a long, the first 8 bits specifies the rendition 357 | style and next 4 bits for foreground and another 4 bits for background 358 | color. 359 | """ 360 | if row < 0 or row >= self.rows: 361 | return None 362 | 363 | if col < 0 or col >= self.cols: 364 | return None 365 | 366 | style = self.scrRendition[row][col] & 0x000000ff 367 | fgcolor = (self.scrRendition[row][col] & 0x00000f00) >> 8 368 | bgcolor = (self.scrRendition[row][col] & 0x0000f000) >> 12 369 | 370 | return (style, fgcolor, bgcolor) 371 | 372 | def GetLine(self, lineno): 373 | """ 374 | Returns the terminal screen line specified by lineno. The line is 375 | returned as string, blank space represents empty character. The lineno 376 | should be in the range 0..rows - 1 377 | """ 378 | if lineno < 0 or lineno >= self.rows: 379 | return None 380 | 381 | return self.screen[lineno].tostring() 382 | 383 | def GetLines(self): 384 | """ 385 | Returns terminal screen lines as a list, same as GetScreen 386 | """ 387 | lines = [] 388 | 389 | for i in range(self.rows): 390 | lines.append(self.screen[i].tostring()) 391 | 392 | return lines 393 | 394 | def GetLinesAsText(self): 395 | """ 396 | Returns the entire terminal screen as a single big string. Each row 397 | is seperated by \\n and blank space represents empty character. 398 | """ 399 | text = "" 400 | 401 | for i in range(self.rows): 402 | text += self.screen[i].tostring() 403 | text += '\n' 404 | 405 | text = text.rstrip("\n") # removes leading new lines 406 | 407 | return text 408 | 409 | def GetDirtyLines(self): 410 | """ 411 | Returns list of dirty lines(line nos) since last call to GetDirtyLines. 412 | The line no will be 0..rows - 1. 413 | """ 414 | dirtyLines = [] 415 | 416 | for i in range(self.rows): 417 | if self.isLineDirty[i]: 418 | dirtyLines.append(i) 419 | self.isLineDirty[i] = False 420 | 421 | return dirtyLines 422 | 423 | def SetCallback(self, event, func): 424 | """ 425 | Sets callback function for the specified event. The event should be 426 | any one of the following. None can be passed as callback function to 427 | reset the callback. 428 | 429 | CALLBACK_SCROLL_UP_SCREEN 430 | Called before scrolling up the terminal screen. 431 | 432 | CALLBACK_UPDATE_LINES 433 | Called when ever some lines need to be updated. Usually called 434 | before leaving ProcessInput and before scrolling up the 435 | terminal screen. 436 | 437 | CALLBACK_UPDATE_CURSOR_POS 438 | Called to update the cursor position. Usually called before leaving 439 | ProcessInput. 440 | 441 | CALLBACK_UPDATE_WINDOW_TITLE 442 | Called when ever a window title escape sequence encountered. The 443 | terminal window title will be passed as a string. 444 | 445 | CALLBACK_UNHANDLED_ESC_SEQ 446 | Called when ever a unsupported escape sequence encountered. The 447 | unhandled escape sequence(escape sequence character and it 448 | parameters) will be passed as a string. 449 | """ 450 | self.callbacks[event] = func 451 | 452 | def ProcessInput(self, text): 453 | """ 454 | Processes the given input text. It detects V100 escape sequences and 455 | handles it. Any partial unparsed escape sequences are stored internally 456 | and processed along with next input text. Before leaving, the function 457 | calls the callbacks CALLBACK_UPDATE_LINE and CALLBACK_UPDATE_CURSOR_POS 458 | to update the changed lines and cursor position respectively. 459 | """ 460 | if text == None: 461 | return 462 | 463 | if self.unparsedInput != None: 464 | text = self.unparsedInput + text 465 | self.unparsedInput = None 466 | 467 | textlen = len(text) 468 | index = 0 469 | while index < textlen: 470 | ch = text[index] 471 | ascii = ord(ch) 472 | 473 | if self.ignoreChars: 474 | index += 1 475 | continue 476 | 477 | if ascii in self.charHandlers.keys(): 478 | index = self.charHandlers[ascii](text, index) 479 | else: 480 | if ch in self.printableChars: 481 | self.__PushChar(ch) 482 | else: 483 | print("WARNING: Unsupported character %s:%d" % (ch, ascii)) 484 | index += 1 485 | 486 | # update the dirty lines 487 | if self.callbacks[self.CALLBACK_UPDATE_LINES] != None: 488 | self.callbacks[self.CALLBACK_UPDATE_LINES]() 489 | 490 | # update cursor position 491 | if self.callbacks[self.CALLBACK_UPDATE_CURSOR_POS] != None: 492 | self.callbacks[self.CALLBACK_UPDATE_CURSOR_POS]() 493 | 494 | def ScrollUp(self): 495 | """ 496 | Scrolls up the terminal screen by one line. The callbacks 497 | CALLBACK_UPDATE_LINES and CALLBACK_SCROLL_UP_SCREEN are called before 498 | scrolling the screen. 499 | """ 500 | # update the dirty lines 501 | if self.callbacks[self.CALLBACK_UPDATE_LINES] != None: 502 | self.callbacks[self.CALLBACK_UPDATE_LINES]() 503 | 504 | # scrolls up the screen 505 | if self.callbacks[self.CALLBACK_SCROLL_UP_SCREEN] != None: 506 | self.callbacks[self.CALLBACK_SCROLL_UP_SCREEN]() 507 | 508 | line = self.screen.pop(0) 509 | for i in range(self.cols): 510 | line[i] = u' ' 511 | self.screen.append(line) 512 | 513 | rendition = self.scrRendition.pop(0) 514 | for i in range(self.cols): 515 | rendition[i] = 0 516 | self.scrRendition.append(rendition) 517 | 518 | def Dump(self, file=sys.stdout): 519 | """ 520 | Dumps the entire terminal screen into the given file/stdout 521 | """ 522 | for i in range(self.rows): 523 | file.write(self.screen[i].tostring()) 524 | file.write("\n") 525 | 526 | def __NewLine(self): 527 | """ 528 | Moves the cursor to the next line, if the cursor is already at the 529 | bottom row then scrolls up the screen. 530 | """ 531 | self.curX = 0 532 | if self.curY + 1 < self.rows: 533 | self.curY += 1 534 | else: 535 | self.ScrollUp() 536 | 537 | def __PushChar(self, ch): 538 | """ 539 | Writes the character(ch) into current cursor position and advances 540 | cursor position. 541 | """ 542 | if self.curX >= self.cols: 543 | self.__NewLine() 544 | 545 | self.screen[self.curY][self.curX] = ch 546 | self.scrRendition[self.curY][self.curX] = self.curRendition 547 | self.curX += 1 548 | 549 | self.isLineDirty[self.curY] = True 550 | 551 | def __ParseEscSeq(self, text, index): 552 | """ 553 | Parses escape sequence from the input and returns the index after escape 554 | sequence, the escape sequence character and parameter for the escape 555 | sequence 556 | """ 557 | textlen = len(text) 558 | interChars = None 559 | while index < textlen: 560 | ch = text[index] 561 | ascii = ord(ch) 562 | 563 | if ascii >= 32 and ascii <= 63: 564 | # intermediate char (32 - 47) 565 | # parameter chars (48 - 63) 566 | if interChars == None: 567 | interChars = ch 568 | else: 569 | interChars += ch 570 | elif ascii >= 64 and ascii <= 125: 571 | # final char 572 | return (index + 1, chr(ascii), interChars) 573 | else: 574 | print("Unexpected characters in escape sequence %s" % ch) 575 | 576 | index += 1 577 | 578 | # the escape sequence is not complete, inform this to caller by giving 579 | # '?' as final char 580 | return (index, '?', interChars) 581 | 582 | def __HandleEscSeq(self, text, index): 583 | """ 584 | Tries to parse escape sequence from input and if its not complete then 585 | puts it in unparsedInput and process it when the ProcessInput called 586 | next time. 587 | """ 588 | if text[index] == '[': 589 | index += 1 590 | index, finalChar, interChars = self.__ParseEscSeq(text, index) 591 | 592 | if finalChar == '?': 593 | self.unparsedInput = "\033[" 594 | if interChars != None: 595 | self.unparsedInput += interChars 596 | elif finalChar in self.escSeqHandlers.keys(): 597 | self.escSeqHandlers[finalChar](interChars) 598 | else: 599 | escSeq = "" 600 | if interChars != None: 601 | escSeq += interChars 602 | 603 | escSeq += finalChar 604 | 605 | if self.callbacks[self.CALLBACK_UNHANDLED_ESC_SEQ] != None: 606 | self.callbacks[self.CALLBACK_UNHANDLED_ESC_SEQ](escSeq) 607 | 608 | elif text[index] == ']': 609 | textlen = len(text) 610 | if index + 2 < textlen: 611 | if text[index + 1] == '0' and text[index + 2] == ';': 612 | # parse title, terminated by bell char(\007) 613 | index += 3 # ignore '0' and ';' 614 | start = index 615 | while index < textlen: 616 | if ord(text[index]) == self.__ASCII_BEL: 617 | break 618 | 619 | index += 1 620 | 621 | self.__OnEscSeqTitle(text[start:index]) 622 | 623 | return index 624 | 625 | def __OnCharBS(self, text, index): 626 | """ 627 | Handler for backspace character 628 | """ 629 | if self.curX > 0: 630 | self.curX -= 1 631 | 632 | return index + 1 633 | 634 | def __OnCharHT(self, text, index): 635 | """ 636 | Handler for horizontal tab character 637 | """ 638 | while True: 639 | self.curX += 1 640 | if self.curX % 8 == 0: 641 | break 642 | return index + 1 643 | 644 | def __OnCharLF(self, text, index): 645 | """ 646 | Handler for line feed character 647 | """ 648 | self.__NewLine() 649 | return index + 1 650 | 651 | def __OnCharCR(self, text, index): 652 | """ 653 | Handler for carriage return character 654 | """ 655 | self.curX = 0 656 | return index + 1 657 | 658 | def __OnCharXON(self, text, index): 659 | """ 660 | Handler for XON character 661 | """ 662 | self.ignoreChars = False 663 | return index + 1 664 | 665 | def __OnCharXOFF(self, text, index): 666 | """ 667 | Handler for XOFF character 668 | """ 669 | self.ignoreChars = True 670 | return index + 1 671 | 672 | def __OnCharESC(self, text, index): 673 | """ 674 | Handler for escape character 675 | """ 676 | index += 1 677 | if index < len(text): 678 | index = self.__HandleEscSeq(text, index) 679 | 680 | return index 681 | 682 | def __OnCharCSI(self, text, index): 683 | """ 684 | Handler for control sequence intruducer(CSI) character 685 | """ 686 | index += 1 687 | index = self.__HandleEscSeq(text, index) 688 | return index 689 | 690 | def __OnCharIgnore(self, text, index): 691 | """ 692 | Dummy handler for unhandler characters 693 | """ 694 | return index + 1 695 | 696 | def __OnEscSeqTitle(self, params): 697 | """ 698 | Handler for window title escape sequence 699 | """ 700 | if self.callbacks[self.CALLBACK_UPDATE_WINDOW_TITLE] != None: 701 | self.callbacks[self.CALLBACK_UPDATE_WINDOW_TITLE](params) 702 | 703 | def __OnEscSeqCUU(self, params): 704 | """ 705 | Handler for escape sequence CUU 706 | """ 707 | n = 1 708 | if params != None: 709 | n = int(params) 710 | 711 | self.curY -= n; 712 | if self.curY < 0: 713 | self.curY = 0 714 | 715 | def __OnEscSeqCUD(self, params): 716 | """ 717 | Handler for escape sequence CUD 718 | """ 719 | n = 1 720 | if params != None: 721 | n = int(params) 722 | 723 | self.curY += n; 724 | if self.curY >= self.rows: 725 | self.curY = self.rows - 1 726 | 727 | def __OnEscSeqCUF(self, params): 728 | """ 729 | Handler for escape sequence CUF 730 | """ 731 | n = 1 732 | if params != None: 733 | n = int(params) 734 | 735 | self.curX += n; 736 | if self.curX >= self.cols: 737 | self.curX = self.cols - 1 738 | 739 | def __OnEscSeqCUB(self, params): 740 | """ 741 | Handler for escape sequence CUB 742 | """ 743 | n = 1 744 | if params != None: 745 | n = int(params) 746 | 747 | self.curX -= n; 748 | if self.curX < 0: 749 | self.curX = 0 750 | 751 | def __OnEscSeqCHA(self, params): 752 | """ 753 | Handler for escape sequence CHA 754 | """ 755 | if params == None: 756 | print("WARNING: CHA without parameter") 757 | return 758 | 759 | col = int(params) 760 | 761 | # convert it to zero based index 762 | col -= 1 763 | if col >= 0 and col < self.cols: 764 | self.curX = col 765 | else: 766 | print("WARNING: CHA column out of boundary") 767 | 768 | def __OnEscSeqCUP(self, params): 769 | """ 770 | Handler for escape sequence CUP 771 | """ 772 | y = 0 773 | x = 0 774 | 775 | if params != None: 776 | values = params.split(';') 777 | if len(values) == 2: 778 | y = int(values[0]) - 1 779 | x = int(values[1]) - 1 780 | else: 781 | print("WARNING: escape sequence CUP has invalid parameters") 782 | return 783 | 784 | if x < 0: 785 | x = 0 786 | elif x >= self.cols: 787 | x = self.cols - 1 788 | 789 | if y < 0: 790 | y = 0 791 | elif y >= self.rows: 792 | y = self.rows - 1 793 | 794 | self.curX = x 795 | self.curY = y 796 | 797 | def __OnEscSeqED(self, params): 798 | """ 799 | Handler for escape sequence ED 800 | """ 801 | n = 0 802 | if params != None: 803 | n = int(params) 804 | 805 | if n == 0: 806 | self.ClearRect(self.curY, self.curX, self.rows - 1, self.cols - 1) 807 | elif n == 1: 808 | self.ClearRect(0, 0, self.curY, self.curX) 809 | elif n == 2: 810 | self.ClearRect(0, 0, self.rows - 1, self.cols - 1) 811 | else: 812 | print("WARNING: escape sequence ED has invalid parameter") 813 | 814 | def __OnEscSeqEL(self, params): 815 | """ 816 | Handler for escape sequence EL 817 | """ 818 | n = 0 819 | if params != None: 820 | n = int(params) 821 | 822 | if n == 0: 823 | self.ClearRect(self.curY, self.curX, self.curY, self.cols - 1) 824 | elif n == 1: 825 | self.ClearRect(self.curY, 0, self.curY, self.curX) 826 | elif n == 2: 827 | self.ClearRect(self.curY, 0, self.curY, self.cols - 1) 828 | else: 829 | print("WARNING: escape sequence EL has invalid parameter") 830 | 831 | def __OnEscSeqVPA(self, params): 832 | """ 833 | Handler for escape sequence VPA 834 | """ 835 | if params == None: 836 | print("WARNING: VPA without parameter") 837 | return 838 | 839 | row = int(params) 840 | 841 | # convert it to zero based index 842 | row -= 1 843 | if row >= 0 and row < self.rows: 844 | self.curY = row 845 | else: 846 | print("WARNING: VPA line no. out of boundary") 847 | 848 | def __OnEscSeqSGR(self, params): 849 | """ 850 | Handler for escape sequence SGR 851 | """ 852 | if params != None: 853 | renditions = params.split(';') 854 | for rendition in renditions: 855 | irendition = int(rendition) 856 | if irendition == 0: 857 | # reset rendition 858 | self.curRendition = 0 859 | elif irendition > 0 and irendition < 9: 860 | # style 861 | self.curRendition |= (1 << (irendition - 1)) 862 | elif irendition >= 30 and irendition <= 37: 863 | # foreground 864 | self.curRendition |= ((irendition - 29) << 8) & 0x00000f00 865 | elif irendition >= 40 and irendition <= 47: 866 | # background 867 | self.curRendition |= ((irendition - 39) << 12) & 0x0000f000 868 | elif irendition == 27: 869 | # reverse video off 870 | self.curRendition &= 0xffffffbf 871 | elif irendition == 39: 872 | # set underscore off, set default foreground color 873 | self.curRendition &= 0xfffff0ff 874 | elif irendition == 49: 875 | # set default background color 876 | self.curRendition &= 0xffff0fff 877 | else: 878 | print("WARNING: Unsupported rendition %s" % irendition) 879 | else: 880 | # reset rendition 881 | self.curRendition = 0 882 | --------------------------------------------------------------------------------