Added terminal_colors script.
authorIain Patterson <me@iain.cx>
Tue, 25 Nov 2008 15:31:34 +0000 (15:31 +0000)
committerIain Patterson <me@iain.cx>
Tue, 25 Nov 2008 15:31:34 +0000 (15:31 +0000)
git-svn-id: https://svn.cambridge.iain.cx/profile/trunk@154 6be0d1a5-5cfe-0310-89b6-964be062b18b

terminal_colors [new file with mode: 0755]

diff --git a/terminal_colors b/terminal_colors
new file mode 100755 (executable)
index 0000000..75ef3d2
--- /dev/null
@@ -0,0 +1,390 @@
+#!/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()
+