evennia/src/utils/ansi.py

252 lines
8.4 KiB
Python

"""
ANSI - Gives colour to text.
Use the codes defined in ANSIPARSER in your text
to apply colour to text according to the ANSI standard.
Examples:
This is %crRed text%cn and this is normal again.
This is {rRed text{n and this is normal again.
Mostly you should not need to call parse_ansi() explicitly;
it is run by Evennia just before returning data to/from the
user.
"""
import re
from src.utils import utils
# ANSI definitions
ANSI_BEEP = "\07"
ANSI_ESCAPE = "\033"
ANSI_NORMAL = "\033[0m"
ANSI_UNDERLINE = "\033[4m"
ANSI_HILITE = "\033[1m"
ANSI_BLINK = "\033[5m"
ANSI_INVERSE = "\033[7m"
ANSI_INV_HILITE = "\033[1;7m"
ANSI_INV_BLINK = "\033[7;5m"
ANSI_BLINK_HILITE = "\033[1;5m"
ANSI_INV_BLINK_HILITE = "\033[1;5;7m"
# Foreground colors
ANSI_BLACK = "\033[30m"
ANSI_RED = "\033[31m"
ANSI_GREEN = "\033[32m"
ANSI_YELLOW = "\033[33m"
ANSI_BLUE = "\033[34m"
ANSI_MAGENTA = "\033[35m"
ANSI_CYAN = "\033[36m"
ANSI_WHITE = "\033[37m"
# Background colors
ANSI_BACK_BLACK = "\033[40m"
ANSI_BACK_RED = "\033[41m"
ANSI_BACK_GREEN = "\033[42m"
ANSI_BACK_YELLOW = "\033[43m"
ANSI_BACK_BLUE = "\033[44m"
ANSI_BACK_MAGENTA = "\033[45m"
ANSI_BACK_CYAN = "\033[46m"
ANSI_BACK_WHITE = "\033[47m"
# Formatting Characters
ANSI_RETURN = "\r\n"
ANSI_TAB = "\t"
ANSI_SPACE = " "
# Escapes
ANSI_ESCAPES = ("{{", "%%", "\\\\")
class ANSIParser(object):
"""
A class that parses ansi markup
to ANSI command sequences
We also allow to escape colour codes
by prepending with a \ for mux-style and xterm256,
an extra { for Merc-style codes
"""
def __init__(self):
"Sets the mappings"
# MUX-style mappings %cr %cn etc
self.mux_ansi_map = [
# commented out by default; they (especially blink) are potentially annoying
(r'%r', ANSI_RETURN),
(r'%t', ANSI_TAB),
(r'%b', ANSI_SPACE),
#(r'%cf', ANSI_BLINK),
#(r'%ci', ANSI_INVERSE),
(r'%cr', ANSI_RED),
(r'%cR', ANSI_BACK_RED),
(r'%cg', ANSI_GREEN),
(r'%cG', ANSI_BACK_GREEN),
(r'%cy', ANSI_YELLOW),
(r'%cY', ANSI_BACK_YELLOW),
(r'%cb', ANSI_BLUE),
(r'%cB', ANSI_BACK_BLUE),
(r'%cm', ANSI_MAGENTA),
(r'%cM', ANSI_BACK_MAGENTA),
(r'%cc', ANSI_CYAN),
(r'%cC', ANSI_BACK_CYAN),
(r'%cw', ANSI_WHITE),
(r'%cW', ANSI_BACK_WHITE),
(r'%cx', ANSI_BLACK),
(r'%cX', ANSI_BACK_BLACK),
(r'%ch', ANSI_HILITE),
(r'%cn', ANSI_NORMAL),
]
# Expanded mapping {r {n etc
hilite = ANSI_HILITE
normal = ANSI_NORMAL
self.ext_ansi_map = [
(r'{r', hilite + ANSI_RED),
(r'{R', normal + ANSI_RED),
(r'{g', hilite + ANSI_GREEN),
(r'{G', normal + ANSI_GREEN),
(r'{y', hilite + ANSI_YELLOW),
(r'{Y', normal + ANSI_YELLOW),
(r'{b', hilite + ANSI_BLUE),
(r'{B', normal + ANSI_BLUE),
(r'{m', hilite + ANSI_MAGENTA),
(r'{M', normal + ANSI_MAGENTA),
(r'{c', hilite + ANSI_CYAN),
(r'{C', normal + ANSI_CYAN),
(r'{w', hilite + ANSI_WHITE), # pure white
(r'{W', normal + ANSI_WHITE), #light grey
(r'{x', hilite + ANSI_BLACK), #dark grey
(r'{X', normal + ANSI_BLACK), #pure black
(r'{n', normal) #reset
]
# xterm256 {123, %c134,
self.xterm256_map = [
(r'%c([0-5]{3})', self.parse_rgb), # %c123 - foreground colour
(r'%c(b[0-5]{3})', self.parse_rgb), # %cb123 - background colour
(r'{([0-5]{3})', self.parse_rgb), # {123 - foreground colour
(r'{(b[0-5]{3})', self.parse_rgb) # {b123 - background colour
]
# obs - order matters here, we want to do the xterms first since
# they collide with some of the other mappings otherwise.
self.ansi_map = self.xterm256_map + self.mux_ansi_map + self.ext_ansi_map
# prepare regex matching
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
for sub in self.ansi_map]
# prepare matching ansi codes overall
self.ansi_regex = re.compile("\033\[[0-9;]+m")
# escapes - these double-chars will be replaced with a single instance of each
self.ansi_escapes = re.compile(r"(%s)" % "|".join(ANSI_ESCAPES), re.DOTALL)
def parse_rgb(self, rgbmatch):
"""
This is a replacer method called by re.sub with the matched
tag. It must return the correct ansi sequence.
It checks self.do_xterm256 to determine if conversion
to standard ansi should be done or not.
"""
if not rgbmatch:
return ""
rgbtag = rgbmatch.groups()[0]
background = rgbtag[0] == 'b'
if background:
red, green, blue = int(rgbtag[1]), int(rgbtag[2]), int(rgbtag[3])
else:
red, green, blue = int(rgbtag[0]), int(rgbtag[1]), int(rgbtag[2])
if self.do_xterm256:
colval = 16 + (red * 36) + (green * 6) + blue
#print "RGB colours:", red, green, blue
return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval/100, (colval%100)/10, colval%10)
else:
#print "ANSI convert:", red, green, blue
# xterm256 not supported, convert the rgb value to ansi instead
if red == green and red == blue and red < 2:
if background: return ANSI_BACK_BLACK
elif red >= 1: return ANSI_HILITE + ANSI_BLACK
else: return ANSI_NORMAL + ANSI_BLACK
elif red == green and red == blue:
if background: return ANSI_BACK_WHITE
elif red >= 4: return ANSI_HILITE + ANSI_WHITE
else: return ANSI_NORMAL + ANSI_WHITE
elif red > green and red > blue:
if background: return ANSI_BACK_RED
elif red >= 3: return ANSI_HILITE + ANSI_RED
else: return ANSI_NORMAL + ANSI_RED
elif red == green and red > blue:
if background: return ANSI_BACK_YELLOW
elif red >= 3: return ANSI_HILITE + ANSI_YELLOW
else: return ANSI_NORMAL + ANSI_YELLOW
elif red == blue and red > green:
if background: return ANSI_BACK_MAGENTA
elif red >= 3: return ANSI_HILITE + ANSI_MAGENTA
else: return ANSI_NORMAL + ANSI_MAGENTA
elif green > blue:
if background: return ANSI_BACK_GREEN
elif green >= 3: return ANSI_HILITE + ANSI_GREEN
else: return ANSI_NORMAL + ANSI_GREEN
elif green == blue:
if background: return ANSI_BACK_CYAN
elif green >= 3: return ANSI_HILITE + ANSI_CYAN
else: return ANSI_NORMAL + ANSI_CYAN
else: # mostly blue
if background: return ANSI_BACK_BLUE
elif blue >= 3: return ANSI_HILITE + ANSI_BLUE
else: return ANSI_NORMAL + ANSI_BLUE
def parse_ansi(self, string, strip_ansi=False, xterm256=False):
"""
Parses a string, subbing color codes according to
the stored mapping.
strip_ansi flag instead removes all ansi markup.
"""
if not string:
return ''
self.do_xterm256 = xterm256
string = utils.to_str(string)
# go through all available mappings and translate them
parts = self.ansi_escapes.split(string) + [" "]
string = ""
for part, sep in zip(parts[::2], parts[1::2]):
for sub in self.ansi_sub:
part = sub[0].sub(sub[1], part)
string += "%s%s" % (part, sep[0].strip())
if strip_ansi:
# remove all ansi codes (including those manually inserted in string)
string = self.ansi_regex.sub("", string)
return string
ANSI_PARSER = ANSIParser()
#
# Access function
#
def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False):
"""
Parses a string, subbing color codes as needed.
"""
return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256)
def raw(string):
"""
Escapes a string into a form which won't be colorized by the ansi parser.
"""
return string.replace('{','{{').replace('%','%%')