summaryrefslogtreecommitdiff
path: root/scripts/terminal-colors
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/terminal-colors')
-rwxr-xr-xscripts/terminal-colors529
1 files changed, 529 insertions, 0 deletions
diff --git a/scripts/terminal-colors b/scripts/terminal-colors
new file mode 100755
index 0000000..985cefb
--- /dev/null
+++ b/scripts/terminal-colors
@@ -0,0 +1,529 @@
+#!/usr/bin/env python
+
+"""2.2 John Eikenberry <jae@zhar.net> GPL-3+ http://zhar.net/projects/
+
+Copyright
+ Copyright (C) 2010-2013 John Eikenberry <jae@zhar.net>
+
+License
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Description
+ My goal in writing this script was to provide all the functionality of all
+ the various perl/sh scripts found around the web in one place with some
+ additional bells and whistles.
+
+ It automatically detects 8, 16, 88, 256 color capabilities (via ncurses)
+ and displays the appropriate color charts. It can display the colors as
+ blocks or (2d) cubes optionally with color values overlaid in int or hex
+ values. It can show the full rgb text string as well. It can also show the
+ display with a vertical (default) or horizontal orientation. It has the
+ option of additional padding and supports -h --help as well.
+
+ It also works as a utility for converting between 256 and 88 color values.
+
+Development
+ A note on coding style. I was playing around with using classes as simple
+ module-esque namespaces; i.e. having classes that have all staticmethods
+ and never get instatiated. As a side effect of this it makes calls at the
+ module level thus this script is not really importable, thus limiting
+ reuse.
+
+Contributors
+ Isaac Cammann <icammann@gmail.com> - cube display bugfix
+ Jan Larres <jan@majutsushi.net> - submitted patches for..
+ - compact rgb display
+ - use of color intensity to determine foreground contrast color
+ - 16 color SGR ANSI chart
+ - 88 color rgb display bugfix
+
+"""
+from __future__ import print_function
+
+__version__ = __doc__.split('\n')[0]
+
+import sys
+import curses
+from optparse import OptionParser, OptionGroup, make_option
+from math import ceil, sqrt
+from functools import wraps
+
+# output constants
+fg_escape = "\x1b[38;5;%dm"
+bg_escape = "\x1b[48;5;%dm"
+clear = "\x1b[0m"
+
+def _get_options(args):
+ """ Setup and parse options.
+ """
+ option_list = [
+ 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("-f", "--foreground", action="store_true",
+ dest="foreground", default=False,
+ help="Use color for foreground text."),
+ make_option("-l", "--rgb", action="store_true", dest="rgb",
+ default=False, help="Long format. RGB values as text."),
+ make_option("-n", "--numbers", action="store_true", dest="numbers",
+ default=False, help="Include color escape numbers on chart."),
+ make_option("-o", "--basiccodes", action="store_true",
+ dest="basiccodes", default=False,
+ help="Display 16 color chart with SGR ANSI escape codes."),
+ make_option("-p", "--padding", action="store_true", dest="padding",
+ default=False, help="Add extra padding (helps discern colors)."),
+ make_option("-v", "--vertical", action="store_true", dest="vertical",
+ default=True, help="Display with vertical orientation [default]."),
+ make_option("-x", "--hex", action="store_true", dest="hex",
+ default=False, help="Include hex color numbers on chart."),
+ make_option("-z", "--horizontal", action="store_true",
+ dest="horizontal", default=False,
+ help="Display with horizontal orientation."),
+ ]
+
+ parser = OptionParser(version=__version__, option_list=option_list)
+
+ convert_option_list = [
+ 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."),
+ ]
+ group = OptionGroup(parser, "Conversion options")
+ group.add_options(option_list=convert_option_list)
+ parser.add_option_group(group)
+
+ (options, args) = parser.parse_args(args)
+ return options
+
+# instantiate global options based on command arguments
+options = _get_options(sys.argv[1:])
+# don't allow -f by itself
+options.foreground = options.foreground and (
+ options.numbers or options.hex or options.rgb )
+
+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():
+ if callable(f):
+ d[n] = staticmethod(f)
+ return type.__new__(m, n, b, d)
+
+def _cached(f):
+ """ Memoize function w/ no params
+ """
+ _cache = {}
+ def cache():
+ if None not in _cache:
+ _cache[None] = f()
+ return _cache[None]
+ return cache
+
+
+class term16(object):
+ """ Basic 16 color terminal.
+ """
+ __metaclass__ = _staticmethods
+
+ def label(n, esc):
+ """ color label for 256 color values
+ """
+ if options.numbers:
+ return esc(n) + "%3d " % n
+ elif options.hex:
+ return esc(n) + " %2x " % n
+ return esc(n) + " "
+
+ def _render():
+ """ 16 color info
+ """
+ if options.foreground:
+ esc = lambda n: fg_escape % n
+ else:
+ esc = lambda n: bg_escape % n + fg_escape % (15 if n < 9 else 0)
+ return [[term16.label(n, esc) + clear for n in range(8)],
+ [term16.label(n, esc) + clear for n in range(8,16)]]
+
+ def basiccodes():
+ text = " gYw "
+ esc_code = "\x1b[%sm"
+ print(' ' * 17 + ' '.join([str(40 + n) + "m" for n in range(8)]))
+ fg_codes = ['0', '1']
+ for i in range(30, 38):
+ fg_codes.extend([str(i), "1;%d" % i])
+ for fg in fg_codes:
+ print("%5sm " % fg + esc_code % fg + text + clear + ' ', end='')
+ print(' '.join([esc_code % fg + esc_code % bg + text + clear
+ for bg in range(40, 48)]))
+
+ def display():
+ """ display 16 color info
+ """
+ print("System colors:")
+ colors = term16._render()
+ padding=' ' if options.padding else ''
+ for r in colors:
+ print(padding.join(i for i in r))
+ if options.padding: print()
+
+
+class term256(term16):
+ """ eg. xterm-256
+ """
+
+ @_cached
+ def _rgb_lookup():
+ """ color rgb lookup dict
+ """
+ # color increment is based on xterm/rxvt palette
+ 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(*((n,)*3)) for n in range(8, 248, 10)]
+ greys = dict(zip(range(232, 256), greys))
+ color_rgb.update(greys)
+ return color_rgb
+
+ def _to_rgb(n):
+ """ Convert color value to rgb tuple.
+ """
+ return term256._rgb_lookup()[n]
+
+ def _rgb_color_table():
+ """ 256 color info
+ """
+ rgbl = term256._rgb_lookup()
+ label_num = "% 4d: "
+ label_val = "%s"
+ if options.foreground:
+ render = lambda n: fg_escape % n + label_num % n + \
+ label_val % str(rgbl[n]) + clear
+ else:
+ render = lambda n: fg_escape % n + label_num % n + \
+ fg_escape % (16 if rgbl[n].is_light() else 255) \
+ + bg_escape % n + label_val % str(rgbl[n]) + clear
+ return [[render(n) 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()
+ padding=' ' if options.padding else ''
+ while colors:
+ rows, colors = colors[:6], colors[6:]
+ if not options.horizontal:
+ rows = zip(*rows)
+ for r in rows:
+ print(padding.join(i for i in r))
+ if options.padding: print()
+ print()
+
+ def _colors():
+ """ 256 color numbers
+ """
+ return [[i+j for j in range(6)] for i in range(16, 232, 6)]
+
+ def _greyscale():
+ """ 256 greyscale numbers
+ """
+ return [[i+j for j in range(12)] for i in range(232, 256, 12)]
+
+ def _render(palette):
+ """ compact 256 color info
+ """
+ if options.foreground:
+ esc = lambda n: fg_escape % n
+ render = lambda n: term256.label(n, esc) + clear
+ else:
+ esc = lambda n: fg_escape % \
+ (16 if term256._to_rgb(n).is_light() else 255)
+ render = lambda n: bg_escape % n + term256.label(n, esc) + clear
+ return [[render(n) for n in i] for i in palette]
+
+ def _compact_display():
+ """ display colors in compact format
+ """
+ colors = term256._render(term256._colors())
+ if options.cube:
+ _cube_display(colors)
+ elif options.block:
+ _block_display(colors)
+ print()
+ greys = term256._render(term256._greyscale())
+ padding=' ' if options.padding else ''
+ for r in greys:
+ print(padding.join(i for i in r))
+ if options.padding: print()
+
+ 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 and greyscale:")
+ term256._compact_display()
+
+
+class term88(term16):
+ """ xterm-88 or urxvt
+ """
+
+ @_cached
+ def _rgb_lookup():
+ """ color rgb lookup dict
+ """
+ # color increment is based on rxvt palette
+ 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 _to_rgb(n):
+ """ Convert color value to rgb tuple.
+ """
+ return term88._rgb_lookup()[n]
+
+ def _rgb_color_table():
+ """ 88 color info
+ """
+ rgbl = term88._rgb_lookup()
+ label_num = "% 4d: "
+ label_val = "%s"
+ if options.foreground:
+ render = lambda n: fg_escape % n + label_num % n + \
+ label_val % str(rgbl[n]) + clear
+ else:
+ render = lambda n: fg_escape % n + label_num % n + \
+ fg_escape % (16 if rgbl[n].is_light() else 87) \
+ + bg_escape % n + label_val % str(rgbl[n]) + clear
+ return [[render(n) 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 _render(palette):
+ """ 88 color info
+ """
+ if options.foreground:
+ esc = lambda n: fg_escape % n
+ render = lambda n: term88.label(n, esc) + clear
+ else:
+ esc = lambda n: fg_escape % \
+ (16 if term88._to_rgb(n).is_light() else 87)
+ render = lambda n: bg_escape % n + term88.label(n, esc) + clear
+ return [[render(n) for n in i] for i in palette]
+ #for n in [i+j for j in range(4)]]
+ #for i in range(16, 80, 4)]
+
+ def _colors():
+ """ 88 color numbers
+ """
+ return [[i+j for j in range(4)] for i in range(16, 80, 4)]
+
+ def _greyscale():
+ """ 88 greyscale numbers
+ """
+ return [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:
+ padding = ' ' if options.padding else ''
+ term16.display()
+ print()
+ print("4x4x4 color cube and greyscale:")
+ colors = term88._render(term88._colors())
+ if options.cube:
+ _cube_display(colors)
+ elif options.block:
+ _block_display(colors)
+ print()
+ greys = term88._render(term88._greyscale())
+ for r in greys:
+ print(padding.join(i for i in r))
+
+class rgb(tuple):
+ """ An RGB, (red, green, blue) tuple. Takes integers.
+ """
+ def __new__(cls, r, g, b):
+ """ We want 3 colors.
+ """
+ return super(rgb, cls).__new__(cls, (r,g,b))
+
+ def __str__(self):
+ """ Display in compact rgb format.
+ """
+ return "#%02x%02x%02x" % self
+
+ def is_light(self):
+ """ Is this color light (or dark).
+ """
+ red, green, blue = self[0], self[1], self[2]
+ intensity = red*0.2126 + green*0.7152 + blue*0.0722
+ return intensity > 127
+
+
+def _cube_display(colors):
+ """ Display color cube as color aligned flatten cube sides.
+ """
+ padding = ' ' if options.padding else ''
+ 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(padding.join(i
+ for i in rows[n]+tuple(reversed(rows[n+size]))))
+ if options.padding: print(padding)
+ 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(padding.join(i for i in r), end=' ')
+ if options.padding: print(padding, end=' ')
+ if options.padding: print()
+ print()
+ _vertical(colors)
+
+def _block_display(colors):
+ """ Display color cube as cube sides organized by color #s (default).
+ """
+ padding = ' ' if options.padding else ''
+ 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(padding.join(i for i in r), end=' ')
+ if options.padding: print(padding, end=' ')
+ if options.padding: print()
+ 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:
+ if options.basiccodes:
+ print("Basic 16 color chart with SGR ANSI escape codes.\n")
+ term16.basiccodes()
+ else:
+ term.display()
+
+if __name__ == "__main__":
+ main()
+