#!/usr/bin/env python """ Version 1.0 Displays 256, 88 and 16 color tables depending on what the terminal supports. Also provides for conversion between 256 and 88 color values. Note on coding style. I was playing around with using classes as simple namespaces (sort of like modules); ie. having classes that have all staticmethods and never get instatiated. """ import curses from optparse import OptionParser, make_option from math import ceil, sqrt option_list = [ make_option("-x", "--hex", action="store_true", dest="hex", default=False, help="Include hex color numbers on chart."), make_option("-n", "--numbers", action="store_true", dest="numbers", default=False, help="Include color escape numbers on chart."), make_option("-b", "--block", action="store_true", dest="block", default=True, help="Display as block format (vs cube) [default]."), make_option("-c", "--cube-slice", action="store_true", dest="cube", default=False, help="Display as cube slices (vs block)."), make_option("-v", "--vertical", action="store_true", dest="vertical", default=True, help="Display with vertical orientation [default]."), make_option("-z", "--horizontal", action="store_true", dest="horizontal", default=False, help="Display with horizontal orientation."), make_option("-l", "--rgb", action="store_true", dest="rgb", default=False, help="Long format. RGB values text."), make_option("-r", "--256to88", action="store", dest="reduce", metavar="N", type="int", help="Convert (reduce) 256 color value N to an 88 color value."), make_option("-e", "--88to256", action="store", dest="expand", metavar="N", type="int", help="Convert (expand) 88 color value N to an 256 color value."), ] version = __doc__.split('\n')[1] parser = OptionParser(version=version, option_list=option_list) (options, args) = parser.parse_args() # output constants fg_escape = "\x1b[38;5;%dm" bg_escape = "\x1b[48;5;%dm" clear = "\x1b[0m" class _staticmethods(type): """ Got tired of adding @staticmethod in front of every method. """ def __new__(m, n, b, d): """ turn all methods in to staticmethods. staticmethod() deals correctly with class attributes. """ for (n, f) in d.items(): d[n] = staticmethod(f) return type.__new__(m, n, b, d) class term16(object): """ Basic 16 color terminal. """ __metaclass__ = _staticmethods def _label(): if options.numbers: return " %2d " elif options.hex: return " %2x " return " " def fg(label, n): fg = n < 8 and 15 or 0 try: return fg_escape % fg + label % n + clear except TypeError: return fg_escape % fg + label + clear def _color_table(): label = term16._label() return [ [bg_escape % n + term16.fg(label, n) + clear for n in range(8)], [bg_escape % n + term16.fg(label, n) + clear for n in range(8,16)] ] def display(): """ display 16 color info """ print "System colors:" colors = term16._color_table() for r in colors: print ''.join(i for i in r) class term256(term16): """ eg. xterm-256 """ def _rgb_lookup(): """ color rgb lookup dict """ rgb = "%02x/%02x/%02x" cincr = [0] + [95+40*n for n in range(5)] color_rgb = [rgb % (i, j, k) for i in cincr for j in cincr for k in cincr] color_rgb = dict(zip(range(16, len(color_rgb)+16), color_rgb)) greys = [rgb % (((8+n),)*3) for n in range(0, 240, 10)] greys = dict(zip(range(232, 256), greys)) color_rgb.update(greys) return color_rgb def _rgb_color_table(): """ 256 color info """ label = "% 4d: %s" _rgb = term256._rgb_lookup() return [[fg_escape % n + label % (n, _rgb[n]) + clear for n in [i+j for j in range(6)]] for i in range(16, 256, 6)] def _rgb_display(): """ display colors with rgb hex info """ colors = term256._rgb_color_table() while colors: rows, colors = colors[:6], colors[6:] for r in zip(*rows): print ''.join(i for i in r) print def _label(): if options.numbers: return "%3d " elif options.hex: return " %2x " return " " def fg(label, n): if n < 232: fg = n < 124 and 15 or 0 else: fg = n < 244 and 15 or 0 try: return fg_escape % fg + label % n + clear except TypeError: return fg_escape % fg + label + clear def _color_table(): """ compact 256 color info """ label = term256._label() return [[bg_escape % n + term256.fg(label, n) + clear for n in [i+j for j in range(6)]] for i in range(16, 232, 6)] def _grey_table(): """ compact grey table """ label = " " + term256._label() return [[bg_escape % n + term256.fg(label, n) + clear for n in [i+j for j in range(12)]] for i in range(232, 256, 12)] def _compact_display(): """ display colors in compact format """ colors = term256._color_table() if options.cube: _cube(colors) elif options.block: _block(colors) print print "Greyscale ramp:" greys = term256._grey_table() for r in greys: print ''.join(i for i in r) def display(): """ display 256 color info (+ 16 in compact format) """ if options.rgb: print "Xterm RGB values for 6x6x6 color cube and greyscale." print term256._rgb_display() else: term16.display() print print "6x6x6 color cube:" term256._compact_display() class term88(term16): """ xterm-88 or urxvt """ def _rgb_lookup(): """ color rgb lookup dict """ rgb = "%02x/%02x/%02x" cincr = [0, 0x8b, 0xcd, 0xff] color_rgb = [rgb % (i, j, k) for i in cincr for j in cincr for k in cincr] color_rgb = dict(zip(range(16, len(color_rgb)+16), color_rgb)) greys = [rgb % ((n,)*3) for n in [0x2e, 0x5c, 0x73, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7]] greys = dict(zip(range(80, 88), greys)) color_rgb.update(greys) return color_rgb def _rgb_color_table(): """ 256 color info """ label = "% 4d: %s" _rgb = term88._rgb_lookup() return [[fg_escape % n + label % (n, _rgb[n]) + clear for n in [i+j for j in range(4)]] for i in range(16, 88, 4)] def _rgb_display(): """ display colors with rgb hex info """ colors = term88._rgb_color_table() while colors: rows, colors = colors[:4], colors[4:] for r in zip(*rows): print ''.join(i for i in r) print def _label(): if options.numbers: return " %2d " elif options.hex: return " %2x " return " " def fg(label, n): if n < 80: fg = n < 48 and 15 or 0 else: fg = n < 84 and 15 or 0 try: return fg_escape % fg + label % n + clear except TypeError: return fg_escape % fg + label + clear def _color_table(): """ 88 color info """ label = term88._label() return [[bg_escape % n + term88.fg(label, n) + clear for n in [i+j for j in range(4)]] for i in range(16, 80, 4)] def _grey_table(): """ 88 color grey info """ label = term88._label() return [bg_escape % n + term88.fg(label, n) + clear for n in range(80, 88)] def display(): """ display 16 + 88 color info """ if options.rgb: print "Xterm RGB values for 4x4x4 color cube and greyscale." print term88._rgb_display() else: term16.display() print print "4x4x4 color cube:" colors = term88._color_table() if options.cube: _cube(colors) elif options.block: _block(colors) print print "Greyscale ramp:" greys = term88._grey_table() print ''.join(i for i in greys) def _cube(colors): if options.horizontal: def _horizontal(colors): size = int(sqrt(len(colors))) for n in (n*size for n in range(size)): colors[n:n+size] = zip(*colors[n:n+size]) while colors: rows, colors = colors[:size*2], colors[size*2:] for n in range(size): print ''.join(i for i in rows[n]+tuple(reversed(rows[n+size]))) if colors: print _horizontal(colors) else: #options.vertical - default def _vertical(colors): size = int(sqrt(len(colors))) top = [colors[n:len(colors):size*2] for n in range(size)] bottom = [colors[n+size:len(colors):size*2] for n in reversed(range(size))] for group in [top, bottom]: for rows in group: for r in rows: print ''.join(i for i in r), print _vertical(colors) def _block(colors): size = int(sqrt(len(colors))) if not options.horizontal: for n in (n*size for n in range(size)): colors[n:n+size] = zip(*colors[n:n+size]) while colors: half = size*(size/2) rows, colors = colors[:half], colors[half:] for n in range(size): for r in rows[n:len(rows):size]: print ''.join(i for i in r), print if colors: print def convert88to256(n): """ 88 (4x4x4) color cube to 256 (6x6x6) color cube values """ if n < 16: return n elif n > 79: return 234 + (3 * (n - 80)) else: def m(n): "0->0, 1->1, 2->3, 3->5" return n and n + n-1 or n b = n - 16 x = b % 4 y = (b / 4) % 4 z = b / 16 return 16 + m(x) + (6 * m(y) + 36 * m(z)) def convert256to88(n): """ 256 (6x6x6) color cube to 88 (4x4x4) color cube values """ if n < 16: return n elif n > 231: if n < 234: return 0 return 80 + ((n - 234) / 3) else: def m(n, _ratio=(4./6.)): if n < 2: return int(ceil(_ratio*n)) else: return int(_ratio*n) b = n - 16 x = b % 6 y = (b / 6) % 6 z = b / 36 return 16 + m(x) + (4 * m(y) + 16 * m(z)) def _terminal(): """ detect # of colors supported by terminal and return appropriate terminal class """ curses.setupterm() num_colors = curses.tigetnum('colors') if num_colors > 0: return {16:term16, 88:term88, 256:term256}.get(num_colors, term16) def main(): if options.reduce: v = convert256to88(options.reduce) # reconvert back to display reduction in context print "%s (equivalent to 256 value: %s)" % (v, convert88to256(v)) elif options.expand: print convert88to256(options.expand) else: term = _terminal() if term is None: print "Your terminal reports that it has no color support." else: term.display() if __name__ == "__main__": main()