mirror of
https://github.com/evennia/evennia.git
synced 2026-03-26 17:56:32 +01:00
252 lines
8.5 KiB
Python
252 lines
8.5 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 = " "
|
|
|
|
class ANSIParser(object):
|
|
"""
|
|
A class that parses ansi markup
|
|
to ANSI command sequences
|
|
|
|
We also allow to escape colour codes
|
|
by prepending with a \.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"Sets the mappings"
|
|
|
|
# MUX-style mappings %cr %cn etc
|
|
|
|
self.mux_ansi_map = [
|
|
(r'(?<!\\)%r', ANSI_RETURN),
|
|
(r'(?<!\\)%t', ANSI_TAB),
|
|
(r'(?<!\\)%b', ANSI_SPACE),
|
|
(r'(?<!\\)%cf', ANSI_BLINK),
|
|
(r'(?<!\\)%ci', ANSI_INVERSE),
|
|
(r'(?<!\\)%ch', ANSI_HILITE),
|
|
(r'(?<!\\)%cn', ANSI_NORMAL),
|
|
(r'(?<!\\)%cx', ANSI_BLACK),
|
|
(r'(?<!\\)%cX', ANSI_BACK_BLACK),
|
|
(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),
|
|
]
|
|
|
|
# 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
|
|
]
|
|
|
|
# matching for cleaning out escaped colour codes (used with sub)
|
|
|
|
self.clean_escapes = [
|
|
(r"\\{", "{"),
|
|
(r"\\%r", "%r"),
|
|
(r"\\%b", "%b"),
|
|
(r"\\%c", "%c")
|
|
]
|
|
|
|
# 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]
|
|
|
|
self.escape_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
|
|
for sub in self.clean_escapes]
|
|
|
|
# prepare matching ansi codes overall
|
|
self.ansi_regex = re.compile("\033\[[0-9;]+m")
|
|
|
|
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 ''
|
|
string = utils.to_str(string)
|
|
|
|
self.do_xterm256 = xterm256
|
|
# handle all subs
|
|
for sub in self.ansi_sub:
|
|
# go through all available mappings and translate them
|
|
string = sub[0].sub(sub[1], string)
|
|
if strip_ansi:
|
|
# remove all ANSI escape codes
|
|
string = self.ansi_regex.sub("", string)
|
|
for sub in self.escape_sub:
|
|
# strip the \ in front of escaped colour codes, like \{r.
|
|
string = sub[0].sub(sub[1], 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)
|
|
|
|
|