If you ever program on Linux, you may be used to writing code like this:
print("\x1b[2;32mhello \x1b[1;31mworld\x1b[0m")
And seeing terminal output like this:
hello world
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.
Enter ctypes
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:
GetStdHandle
to fetch the handle to real STDIN,SetConsoleTextAttribute
to actually change the colours and other flags, andGetConsoleScreenBufferInfo
to 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
Here _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).
Redirecting flow
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
functions: stdin
needs a read(...)
function, and stdout
and stderr
both
need a write(...)
function. Intercepting the characters is simply a case of
replacing 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 StreamWrapper._handle_escape
which
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!
Hello World!
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 print
-ed is
intercepted by the 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!