cross-platform coloured text in terminal

J

Jonathan Hartley

Hi,

It irks me that I know of no simple cross-platform way to print
colored terminal text from Python.

As I understand it, printing ANSI escape codes (as wrapped nicely by
module termcolor and others) works on Macs and *nix, but only works on
Windows if one has installed the ANSI.SYS device driver, which most
users have not. However, on Windows, there is an alternative method,
which is to make win32 calls via ctypes.

I'd like to try and unite these different implementations under a
single cross-platform API. Has this been done already? I understand
that the detailed capabilities of the two implementations (eg. dim/
bright colors) might not map neatly, but at least for simple colored
text, it should be OK.

I'm playing with ideas of what API to expose. My favourite one is to
simply embed ANSI codes in the stream to be printed. Then this will
work as-is on Mac and *nix. To make it work on Windows, printing could
be done to a file0-like object which wraps stdout:


class ColorStream(object):

def __init__(self, wrapped):
self.wrapped = wrapped

def write(self, text):
# magic goes here
self.wrapped.write(text)

def __getattr__(self, name):
return getattr(self.wrapped, name)

term = ColorTerm(sys.stdout)
print <<term, ANSI.GREEN + "hello"

The idea being that in place of 'magic goes here', there will be code
that, on Windows, searches 'text' for ANSI escape codes, strips them
from the text, and converts them into the appropriate win32 calls.

For extra nasty magic, either the module or the user of the module
could wrap sys.stdout globally:

sys.stdout = ColoredStream(sys.stdout)

Then print statements in the user's code would simply be:

print ANSI.GREEN + "hello"

and this would work on all platforms.

No doubt there are many problems with these ideas. I would love to
hear about them. Many thanks.
 
J

Jonathan Hartley

Hi,

It irks me that I know of no simple cross-platform way to print
colored terminal text from Python.

As I understand it, printing ANSI escape codes (as wrapped nicely by
module termcolor and others) works on Macs and *nix, but only works on
Windows if one has installed the ANSI.SYS device driver, which most
users have not. However, on Windows, there is an alternative method,
which is to make win32 calls via ctypes.

I'd like to try and unite these different implementations under a
single cross-platform API. Has this been done already? I understand
that the detailed capabilities of the two implementations (eg. dim/
bright colors) might not map neatly, but at least for simple colored
text, it should be OK.

I'm playing with ideas of what API to expose. My favourite one is to
simply embed ANSI codes in the stream to be printed. Then this will
work as-is on Mac and *nix. To make it work on Windows, printing could
be done to a file0-like object which wraps stdout:

class ColorStream(object):

    def __init__(self, wrapped):
        self.wrapped = wrapped

    def write(self, text):
        # magic goes here
        self.wrapped.write(text)

    def __getattr__(self, name):
        return getattr(self.wrapped, name)

term = ColorTerm(sys.stdout)
print <<term, ANSI.GREEN + "hello"

The idea being that in place of 'magic goes here', there will be code
that, on Windows, searches 'text' for ANSI escape codes, strips them
from the text, and converts them into the appropriate win32 calls.

For extra nasty magic, either the module or the user of the module
could wrap sys.stdout globally:

sys.stdout = ColoredStream(sys.stdout)

Then print statements in the user's code would simply be:

print ANSI.GREEN + "hello"

and this would work on all platforms.

No doubt there are many problems with these ideas. I would love to
hear about them. Many thanks.


Sorry, I forgot to mention: The reason I like this idea is that, in
theory, all existing libraries like termcolor will then work,
unmodified, on all platforms.
 
L

Lie Ryan

I'm playing with ideas of what API to expose. My favourite one is to
simply embed ANSI codes in the stream to be printed. Then this will
work as-is on Mac and *nix. To make it work on Windows, printing could
be done to a file0-like object which wraps stdout:

The problem with that is you're simply reinventing ANSI.SYS device driver.

An alternative API is you could override .__add__(), like so (completely
untested):

class Color(object):
def __init__(self, color):
self.color = map_the_color(color)
self.string = ""
def __add__(self, string):
self.string += string
return self
def __str__(self):
if terminal_can_do_ansi_color:
return ansicolorescape(self.string, self.color)
elif windows:
syscalltocolor(self.color)
print self.string
syscalltocolor(reset the color)
return ""

GREEN = Color('green')
print GREEN + "Great" + "Good"

you can even go a bit further and allow chained calls (again, completely
untested, but you get the idea):

class Color(object):
def __init__(self, color):
self.color = map_the_color(color)
self.stack = []
def __add__(self, string):
if isinstance(string, Color):
# not a string, chain the calls
self.stack.append((string.color, []]))
else:
# a string,
self.stack[-1][1].append(string)
return self
def __radd__(self, string):
self.stack.append([self.default, string])
return self

def __str__(self):
if ansi_capable:
return colorescape(format, string)
elif windows:
for format, string in self.stack:
syscalltocolor(color)
print string
return ""

GREEN = Color('green')
RED = Color('red')

print "Fairly" + GREEN + "Great" + RED + "Poor"

or something like that, and you will have an API that works
transparently on all platforms. The downside is that you cannot call
str(GREEN + "foo") on windows.
 
J

Jonathan Hartley

I'm playing with ideas of what API to expose. My favourite one is to
simply embed ANSI codes in the stream to be printed. Then this will
work as-is on Mac and *nix. To make it work on Windows, printing could
be done to a file0-like object which wraps stdout:

The problem with that is you're simply reinventing ANSI.SYS device driver..

An alternative API is you could override .__add__(), like so (completely
untested):

class Color(object):
   def __init__(self, color):
       self.color =  map_the_color(color)
       self.string = ""
   def __add__(self, string):
       self.string += string
       return self
   def __str__(self):
       if terminal_can_do_ansi_color:
           return ansicolorescape(self.string, self.color)
       elif windows:
           syscalltocolor(self.color)
           print self.string
           syscalltocolor(reset the color)
           return ""

GREEN = Color('green')
print GREEN + "Great" + "Good"

you can even go a bit further and allow chained calls (again, completely
untested, but you get the idea):

class Color(object):
   def __init__(self, color):
       self.color =  map_the_color(color)
       self.stack = []
   def __add__(self, string):
       if isinstance(string, Color):
           # not a string, chain the calls
           self.stack.append((string.color, []]))
       else:
           # a string,
           self.stack[-1][1].append(string)
       return self
   def __radd__(self, string):
       self.stack.append([self.default, string])
       return self

   def __str__(self):
       if ansi_capable:
           return colorescape(format, string)
       elif windows:
           for format, string in self.stack:
               syscalltocolor(color)
               print string
               return ""

GREEN = Color('green')
RED = Color('red')

print "Fairly" + GREEN + "Great" + RED + "Poor"

or something like that, and you will have an API that works
transparently on all platforms. The downside is that you cannot call
str(GREEN + "foo") on windows.



Hey Lie,

Thanks heaps for the reply!

I don't see that as a problem - in fact I think it's exactly my
goal! :)

The difference is that the ANSI driver requires installation and a
reboot on the end-user's computer, which is a fiddly and intrusive
thing for a Python developer to achieve. Whereas doing the same job in
a Python module is easy to use for the Python developer - they just
import the module, maybe call an 'init()' function, and then the ANSI
functionality works on all platforms.

Your ideas about generating and chaining the ANSI code strings are
great. I worry though, about intermingling the code that generates
ANSI escape sequences with the code which makes them work on Windows.
The problem is that then, only applications which use your ANSI-
generation library will work on Windows. Whereas if these two things
are kept separate, then applications which use any other ANSI-
generation techniques, such as using 'termcolor', or manaully printing
raw ANSI sequences, these can also all work on Windows too, simply by
adding an import and an 'init()' call to the start of the application.

Am I making sense? Many thanks for your thoughts.

Jonathan
 
J

Jonathan Hartley

On 04/16/10 19:28, Jonathan Hartley wrote:
The problem with that is you're simply reinventing ANSI.SYS device driver.
An alternative API is you could override .__add__(), like so (completely
untested):
classColor(object):
   def __init__(self,color):
       self.color=  map_the_color(color)
       self.string = ""
   def __add__(self, string):
       self.string += string
       return self
   def __str__(self):
       if terminal_can_do_ansi_color:
           return ansicolorescape(self.string, self.color)
       elif windows:
           syscalltocolor(self.color)
           print self.string
           syscalltocolor(reset thecolor)
           return ""
GREEN =Color('green')
print GREEN + "Great" + "Good"
you can even go a bit further and allow chained calls (again, completely
untested, but you get the idea):
classColor(object):
   def __init__(self,color):
       self.color=  map_the_color(color)
       self.stack = []
   def __add__(self, string):
       if isinstance(string,Color):
           # not a string, chain the calls
           self.stack.append((string.color, []]))
       else:
           # a string,
           self.stack[-1][1].append(string)
       return self
   def __radd__(self, string):
       self.stack.append([self.default, string])
       return self
   def __str__(self):
       if ansi_capable:
           return colorescape(format, string)
       elif windows:
           for format, string in self.stack:
               syscalltocolor(color)
               print string
               return ""
GREEN =Color('green')
RED =Color('red')
print "Fairly" + GREEN + "Great" + RED + "Poor"
or something like that, and you will have an API that works
transparently on all platforms. The downside is that you cannot call
str(GREEN + "foo") on windows.

Hey Lie,

Thanks heaps for the reply!

I don't see that as a problem - in fact I think it's exactly my
goal! :)

The difference is that the ANSI driver requires installation and a
reboot on the end-user's computer, which is a fiddly and intrusive
thing for a Python developer to achieve. Whereas doing the same job in
a Python module is easy to use for the Python developer - they just
import the module, maybe call an 'init()' function, and then the ANSI
functionality works on all platforms.

Your ideas about generating and chaining the ANSI code strings are
great. I worry though, about intermingling the code that generates
ANSI escape sequences with the code which makes them work on Windows.
The problem is that then, only applications which use your ANSI-
generation library will work on Windows. Whereas if these two things
are kept separate, then applications which use any other ANSI-
generation techniques, such as using 'termcolor', or manaully printing
raw ANSI sequences, these can also all work on Windows too, simply by
adding an import and an 'init()' call to the start of the application.

Am I making sense? Many thanks for your thoughts.

  Jonathan


I have implemented these ideas here. It seems to work.
http://pypi.python.org/pypi/colorama
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,764
Messages
2,569,564
Members
45,041
Latest member
RomeoFarnh

Latest Threads

Top