If you ever program on Linux, you may be used to writing code like this:
And seeing terminal output like this:
Linux terminal programs are usually full of colour, and it’s all thanks to VT100 escape sequences that date back to the early dumb-terminals. Microsoft used to support these back on Windows 98 (and I think even as late as Windows 2000) using something they called the ANSI text mode driver. Unfortunately this isn’t supported any more, so we Windows users are usually presented with a very dull command prompt whenever we use it.
I’ve spent the past few days working on my Uploader tool which I wrote last year to physically transfer programs onto SpiNNaker, and I suddenly noticed that the drastic lack of colour makes it very difficult to realise what matters on the screen in a text-mode program. So I started wondering how I could solve this issue in a nice cross-platform way.
ctypes is awesome. It’s a foreign function interface (FFI) library for Python that allows any Python code to invoke functions in a C-compiled dynamic library. We can write things like:
INVALID_HANDLE_VALUE = ctypes.c_ulong(-1) class SMALL_RECT(ctypes.Structure): _fields_ = [('Left', ctypes.c_int16), ('Top', ctypes.c_int16), ('Right', ctypes.c_int16), ('Bottom', ctypes.c_int16)] GetStdHandle = ctypes.windll.kernel32.GetStdHandle GetStdHandle.restype = ctypes.c_ulong GetStdHandle.argtypes = [ctypes.c_uint32]
To give us direct (almost) access to the Windows API, wherein lurk our precious console manipulation functions. We don’t need to write any C code to make this work! The Windows API functions we care about are:
GetStdHandleto fetch the handle to real STDIN,
SetConsoleTextAttributeto actually change the colours and other flags, and
GetConsoleScreenBufferInfoto determine the initial attributes to reset to.
I won’t bother pasting all the declarative code here because it’s not that
interesting, however one point is worthy of note. The
CONSOLE_SCREEN_BUFFER_INFO struct that will be
used to receive the attributes from
GetConsoleScreenBufferInfo needs to be
16bit aligned. ctypes provides a mechanism for this:
class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): _fields_ = [('dwSize', COORD), ('dwCursorPosition', COORD), ('wAttributes', ctypes.c_short), ('srWindow', SMALL_RECT), ('dwMaximumWindowSize', COORD)] _pack_ = 2
_pack_ behaves identically to
#pragma pack in the
Microsoft C compiler, hence this structure will be aligned on a two byte
boundary (i.e. 16bits).
So ctypes has given us access to the console API functions we need, but we still
need to intercept the text that’s on its way to the console. Thankfully this is
simple in Python. The three standard streams only need to support two
stdin needs a
read(...) function, and
write(...) function. Intercepting the characters is simply a case of
sys.stdout with our own:
class StreamWrapper(object): def __init__(self, stream): self._stream = stream def write(self, line): parts = re.split(r'(\x1b\[[^m]+m)', line) for part in parts: if part.startswith('\x1b['): self._handle_escape(part) else: self._stream.write(part) def _handle_escape(self, sequence): pass sys.stdout = StreamWrapper(sys.stdout)
Escape sequences will now be passed off to
will call the appropriate console API functions imported with ctypes, and normal
text will be written to the stream as normal. Only one piece of the puzzle
remains now: making it cross-platform.
Everything imported by ctypes is, obviously, Windows specific. VT100 codes are supported by all Linux terminals that I know about, so there’s no real Linux- specific code that needs to be written. I put all of the Windows-specific code behind a platform check:
import sys # Library should be a no-op on non-Windows platforms. if sys.platform == 'win32': import ctypes import re # ctypes stuff for Windows API... # proxy class definition... # hijack the stream...
Notice how nothing is explicitly exported!
The mere act of importing vtemu is all that’s required for the cross-platform picture above.
import vtemu print("\x1b[2;32mhello \x1b[1;31mworld\x1b[0m")
I opened a PuTTY SSH session to Linux machine to try it out. As expected,
vtemu does nothing at all. But on a Windows machine, everything
StreamWrapper object to process the VT100 codes! Only the
tiniest subset is supported by this class, but it definitely does the trick. Not
bad for an evening’s coding!
You can look at the Simple VT100 emulator source code here. I haven’t done a survey of PyPI to see if I’ve just (poorly) reinvented the wheel yet. It’s been an interesting problem to play with regardless. If it does seem useful, I’ll happily robustify the code and turn it into a proper Python package!