If you ever program on Linux, you may be used to writing code like this:

1
print "\x1b[2;32mhello \x1b[1;31mworld\x1b[0m"

And seeing 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:

1
2
3
4
5
6
7
8
9
10
11
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:

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:

1
2
3
4
5
6
7
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 into a module called `winemu` which is contained within the `vtemu` package.  Simple platform detection in the __init__.py file optionally imports the Windows-specific stuff, or does absolutely nothing.

1
2
3
4
5
6
7
8
import sys
 
__all__ = []
 
if sys.platform == 'win32':
    from winemu import StreamWrapper
 
    sys.stdout = StreamWrapper (sys.stdout)

Notice how nothing is explicitly exported!

Hello World!

Simple cross-platform VT100 colour code emulation.The mere act of importing vtemu is all that’s required for the cross-platform picture above.

1
2
3
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 download the Simple VT100 emulator source code here.  I haven’t done a survey fo 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!

Leave a Reply