Source code for mrcrowbar.ansi
import math
from mrcrowbar import colour, statistics
#: Container for ANSI escape sequences for text formatting
ANSI_FORMAT_BASE = '\x1b[{}m'
#: ANSI escape sequence for resetting the colour settings to the default.
ANSI_FORMAT_RESET_CMD = '0'
ANSI_FORMAT_RESET = ANSI_FORMAT_BASE.format( ANSI_FORMAT_RESET_CMD )
#: ANSI escape sequence for setting the foreground colour (24-bit).
ANSI_FORMAT_FOREGROUND_CMD = '38;2;{};{};{}'
#: ANSI escape sequence for setting the background colour (24-bit).
ANSI_FORMAT_BACKGROUND_CMD = '48;2;{};{};{}'
#: ANSI escape sequence for setting the foreground colour (xterm).
ANSI_FORMAT_FOREGROUND_XTERM_CMD = '38;5;{}'
#: ANSI escape sequence for setting the background colour (xterm).
ANSI_FORMAT_BACKGROUND_XTERM_CMD = '48;5;{}'
#: ANSI escape sequence for bold text
ANSI_FORMAT_BOLD_CMD = '1'
#: ANSI escape sequence for faint text
ANSI_FORMAT_FAINT_CMD = '2'
#: ANSI escape sequence for bold text
ANSI_FORMAT_ITALIC_CMD = '3'
#: ANSI escape sequence for bold text
ANSI_FORMAT_UNDERLINE_CMD = '4'
#: ANSI escape sequence for bold text
ANSI_FORMAT_BLINK_CMD = '5'
#: ANSI escape sequence for inverted text
ANSI_FORMAT_INVERTED_CMD = '7'
#: Container for ANSI escape sequence screen erasing
ANSI_ERASE_BASE = '\x1b[{}J'
#: ANSI escape sequence for clearing the visible terminal
ANSI_ERASE_SCREEN = ANSI_ERASE_BASE.format( 2 )
#: ANSI escape sequence for clearing the scrollback of the terminal
ANSI_ERASE_SCROLLBACK = ANSI_ERASE_BASE.format( 3 )
#: ANSI escape sequence to set the cursor position. (1, 1) is the top left.
ANSI_CURSOR_SET_POSITION = '\x1b[{};{}H'
#: ANSI escape sequence to move the cursor up.
ANSI_CURSOR_MOVE_UP = '\x1b[{}A'
#: ANSI escape sequence to move the cursor down.
ANSI_CURSOR_MOVE_DOWN = '\x1b[{}B'
#: ANSI escape sequence to move the cursor forward.
ANSI_CURSOR_MOVE_FORWARD = '\x1b[{}C'
#: ANSI escape sequence to move the cursor backward.
ANSI_CURSOR_MOVE_BACKWARD = '\x1b[{}D'
#: Unicode representation of a vertical bar graph.
BAR_VERT = u' ▁▂▃▄▅▆▇█'
#: Unicode representation of a horizontal bar graph.
BAR_HORIZ = u' ▏▎▍▌▋▊▉█'
BYTE_GLYPH_MAP = """ ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ """
BYTE_COLOUR_MAP = (12,) + (14,)*32 + (11,)*94 + (14,)*128 + (12,)
[docs]
def format_escape( foreground=None, background=None, bold=False, faint=False,
italic=False, underline=False, blink=False, inverted=False ):
"""Returns the ANSI escape sequence to set character formatting.
foreground
Foreground colour to use. Accepted types: None, int (xterm
palette ID), tuple (RGB, RGBA), Colour
background
Background colour to use. Accepted types: None, int (xterm
palette ID), tuple (RGB, RGBA), Colour
bold
Enable bold text (default: False)
faint
Enable faint text (default: False)
italic
Enable italic text (default: False)
underline
Enable underlined text (default: False)
blink
Enable blinky text (default: False)
inverted
Enable inverted text (default: False)
"""
fg_format = None
if isinstance( foreground, int ):
fg_format = ANSI_FORMAT_FOREGROUND_XTERM_CMD.format( foreground )
else:
fg_rgba = colour.normalise_rgba( foreground )
if fg_rgba[3] != 0:
fg_format = ANSI_FORMAT_FOREGROUND_CMD.format( *fg_rgba[:3] )
bg_format = None
if isinstance( background, int ):
bg_format = ANSI_FORMAT_BACKGROUND_XTERM_CMD.format( background )
else:
bg_rgba = colour.normalise_rgba( background )
if bg_rgba[3] != 0:
bg_format = ANSI_FORMAT_BACKGROUND_CMD.format( *bg_rgba[:3] )
colour_format = []
if fg_format is not None:
colour_format.append( fg_format )
if bg_format is not None:
colour_format.append( bg_format )
if bold:
colour_format.append( ANSI_FORMAT_BOLD_CMD )
if faint:
colour_format.append( ANSI_FORMAT_FAINT_CMD )
if italic:
colour_format.append( ANSI_FORMAT_ITALIC_CMD )
if underline:
colour_format.append( ANSI_FORMAT_UNDERLINE_CMD )
if blink:
colour_format.append( ANSI_FORMAT_BLINK_CMD )
if inverted:
colour_format.append( ANSI_FORMAT_INVERTED_CMD )
colour_format = ANSI_FORMAT_BASE.format( ';'.join( colour_format ) )
return colour_format
[docs]
def format_string( string, foreground=None, background=None, reset=True, bold=False,
faint=False, italic=False, underline=False, blink=False, inverted=False ):
"""Returns a Unicode string formatted with an ANSI escape sequence.
string
String to format
foreground
Foreground colour to use. Accepted types: None, int (xterm
palette ID), tuple (RGB, RGBA), Colour
background
Background colour to use. Accepted types: None, int (xterm
palette ID), tuple (RGB, RGBA), Colour
reset
Reset the formatting at the end (default: True)
bold
Enable bold text (default: False)
faint
Enable faint text (default: False)
italic
Enable italic text (default: False)
underline
Enable underlined text (default: False)
blink
Enable blinky text (default: False)
inverted
Enable inverted text (default: False)
"""
colour_format = format_escape( foreground, background, bold, faint,
italic, underline, blink, inverted )
reset_format = '' if not reset else ANSI_FORMAT_RESET
return '{}{}{}'.format( colour_format, string, reset_format )
[docs]
def format_pixels( top, bottom, reset=True, repeat=1 ):
"""Return the ANSI escape sequence to render two vertically-stacked pixels as a
single monospace character.
top
Top colour to use. Accepted types: None, int (xterm
palette ID), tuple (RGB, RGBA), Colour
bottom
Bottom colour to use. Accepted types: None, int (xterm
palette ID), tuple (RGB, RGBA), Colour
reset
Reset the formatting at the end (default: True)
repeat
Number of horizontal pixels to render (default: 1)
"""
top_src = None
if isinstance( top, int ):
top_src = top
else:
top_rgba = colour.normalise_rgba( top )
if top_rgba[3] != 0:
top_src = top_rgba
bottom_src = None
if isinstance( bottom, int ):
bottom_src = bottom
else:
bottom_rgba = colour.normalise_rgba( bottom )
if bottom_rgba[3] != 0:
bottom_src = bottom_rgba
# short circuit for empty pixel
if (top_src is None) and (bottom_src is None):
return ' '*repeat
string = '▀'*repeat;
colour_format = []
if top_src == bottom_src:
string = '█'*repeat
elif (top_src is None) and (bottom_src is not None):
string = '▄'*repeat
if (top_src is None) and (bottom_src is not None):
if isinstance( bottom_src, int ):
colour_format.append( ANSI_FORMAT_FOREGROUND_XTERM_CMD.format( bottom_src ) )
else:
colour_format.append( ANSI_FORMAT_FOREGROUND_CMD.format( *bottom_src[:3] ) )
else:
if isinstance( top_src, int ):
colour_format.append( ANSI_FORMAT_FOREGROUND_XTERM_CMD.format( top_src ) )
else:
colour_format.append( ANSI_FORMAT_FOREGROUND_CMD.format( *top_src[:3] ) )
if top_src is not None and bottom_src is not None and top_src != bottom_src:
if isinstance( top_src, int ):
colour_format.append( ANSI_FORMAT_BACKGROUND_XTERM_CMD.format( bottom_src ) )
else:
colour_format.append( ANSI_FORMAT_BACKGROUND_CMD.format( *bottom_src[:3] ) )
colour_format = ANSI_FORMAT_BASE.format( ';'.join( colour_format ) )
reset_format = '' if not reset else ANSI_FORMAT_RESET
return '{}{}{}'.format( colour_format, string, reset_format )
[docs]
def format_bar_graph_iter( data, width=64, height=12, y_min=None, y_max=None ):
if width <= 0:
raise ValueError( 'Width of the graph must be greater than zero' )
if height % 2:
raise ValueError( 'Height of the graph must be a multiple of 2' )
if y_min is None:
y_min = min( data )
y_min = min( y_min, 0 )
if y_max is None:
y_max = max( data )
y_max = max( y_max, 0 )
# determine top-left of vertical origin
if y_max <= 0:
top_height, bottom_height = 0, height
y_scale = 8*height/y_min
elif y_min >= 0:
top_height, bottom_height = height, 0
y_scale = 8*height/y_max
else:
top_height, bottom_height = height//2, height//2
y_scale = 8*height/(2*max( abs( y_min ), abs( y_max ) ))
# precalculate sizes
sample_count = len( data )
if sample_count == 0:
# empty graph
samples = [(0, 0) for x in range( width )]
else:
if sample_count <= width:
sample_ranges = [(math.floor( i*sample_count/width ), math.floor( i*sample_count/width )+1) for i in range( width )]
else:
sample_ranges = [(round( i*sample_count/width ), round( (i+1)*sample_count/width )) for i in range( width )]
samples = [(round( min( data[x[0]:x[1]] )*y_scale ), round( max( data[x[0]:x[1]] )*y_scale )) for x in sample_ranges]
for y in range( top_height, 0, -1 ):
result = []
for _, value in samples:
if value // 8 >= y:
result.append( BAR_VERT[8] )
elif value // 8 == y-1:
result.append( BAR_VERT[value % 8] )
else:
result.append( BAR_VERT[0] )
yield ''.join( result )
for y in range( 1, bottom_height+1, 1 ):
result = []
for value, _ in samples:
if -value // 8 >= y:
result.append( BAR_VERT[8] )
elif -value // 8 == y-1:
result.append( format_string( BAR_VERT[8-((-value) % 8)], inverted=True ) )
else:
result.append( BAR_VERT[0] )
yield ''.join( result )
[docs]
def format_image_iter( data_fetch, x_start=0, y_start=0, width=32, height=32, frame=0, columns=1, downsample=1 ):
"""Return the ANSI escape sequence to render a bitmap image.
data_fetch
Function that takes three arguments (x position, y position, and frame) and returns
a Colour corresponding to the pixel stored there, or Transparent if the requested
pixel is out of bounds.
x_start
Offset from the left of the image data to render from. Defaults to 0.
y_start
Offset from the top of the image data to render from. Defaults to 0.
width
Width of the image data to render. Defaults to 32.
height
Height of the image data to render. Defaults to 32.
frame
Single frame number/object, or a list to render in sequence. Defaults to frame 0.
columns
Number of frames to render per line (useful for printing tilemaps!). Defaults to 1.
downsample
Shrink larger images by printing every nth pixel only. Defaults to 1.
"""
frames = []
try:
frame_iter = iter( frame )
frames = [f for f in frame_iter]
except TypeError:
frames = [frame]
rows = math.ceil( len( frames )/columns )
for r in range( rows ):
for y in range( 0, height, 2*downsample ):
result = []
for c in range( min( (len( frames )-r*columns), columns ) ):
row = []
for x in range( 0, width, downsample ):
fr = frames[r*columns + c]
c1 = data_fetch( x_start+x, y_start+y, fr )
c2 = data_fetch( x_start+x, y_start+y+downsample, fr )
row.append( (c1, c2) )
prev_pixel = None
pointer = 0
while pointer < len( row ):
start = pointer
pixel = row[pointer]
while pointer < len( row ) and (row[pointer] == pixel):
pointer += 1
result.append( format_pixels( pixel[0], pixel[1], repeat=pointer-start ) )
yield ''.join( result )
return
BYTE_ESCAPE_MAP = [format_escape( x ) for x in BYTE_COLOUR_MAP]
[docs]
def format_hexdump_line( source, offset, end=None, major_len=8, minor_len=4, colour=True,
prefix='', highlight_addr=None, highlight_map=None, address_base_offset=0, show_offsets=True, show_glyphs=True ):
def get_colour( index ):
if colour:
if highlight_map:
if index in highlight_map:
return format_escape( highlight_map[index] )
return BYTE_ESCAPE_MAP[source[index]]
return ''
def get_glyph():
b = source[offset:min( offset+major_len*minor_len, end )]
letters = []
prev_colour = None
for i in range( offset, min( offset+major_len*minor_len, end ) ):
new_colour = get_colour( i )
if prev_colour != new_colour:
letters.append( new_colour )
prev_colour = new_colour
letters.append( BYTE_GLYPH_MAP[source[i]] )
if colour:
letters.append( ANSI_FORMAT_RESET )
return ''.join( letters )
if end is None:
end = len( source )
line = []
if show_offsets:
digits = ('{}{:0'+str( max( 8, math.floor( math.log( end+address_base_offset )/math.log( 16 ) ) ) )+'x}').format( prefix, offset+address_base_offset )
line = [format_string( digits, highlight_addr ), ' │ ']
prev_colour = None
for major in range( major_len ):
for minor in range( minor_len ):
suboffset = offset+major*minor_len+minor
if suboffset >= end:
line.append( ' ' )
continue
new_colour = get_colour( suboffset )
if prev_colour != new_colour:
line.append( new_colour )
prev_colour = new_colour
line.append( '{:02x} '.format( source[suboffset] ) )
line.append( ' ' )
if colour:
line.append( ANSI_FORMAT_RESET )
if show_glyphs:
line.append( '│ {}'.format( get_glyph() ) )
return ''.join( line )
[docs]
def format_histogram_line( buckets, palette=None ):
if palette is None:
palette = colour.TEST_PALETTE
total = sum( buckets )
floor = math.log( 1/(8*len( buckets )) )
buckets_log = [-floor + max( floor, math.log( b/total ) ) if b else None for b in buckets]
limit = max( [b for b in buckets_log if b is not None] )
buckets_norm = [round( 255*(b/limit) ) if b is not None else None for b in buckets_log]
result = []
for b in buckets_norm:
if b is not None:
result.append( format_string( '█', palette[b] ) )
else:
result.append( ' ' )
return ''.join( result )
[docs]
def format_histdump_line( source, offset, length=None, end=None, width=64, address_base_offset=0, palette=None ):
if length is not None:
data = source[offset:offset+length]
else:
data = source[offset:]
end = end if end else len( source )
if palette is None:
palette = colour.TEST_PALETTE
digits = ('{:0'+str( max( 8, math.floor( math.log( end+address_base_offset )/math.log( 16 ) ) ) )+'x}').format( offset+address_base_offset )
stat = statistics.Stats(data)
return ('{} │ {} │ {:.10f}').format( digits, format_histogram_line( stat.histogram( width ), palette ), stat.entropy )