Skip to content

Instantly share code, notes, and snippets.

@TerrorBite
Last active September 14, 2015 12:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TerrorBite/e738e25881d4aecf9043 to your computer and use it in GitHub Desktop.
Save TerrorBite/e738e25881d4aecf9043 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
from sys import argv, stdin, stdout, stderr
from traceback import print_exc
from os import path, isatty
from textwrap import dedent
from collections import Counter
from math import sqrt
import inspect
# Luminance threshold values
# Higher values will combat higher perceived brightness
LUM_R, LUM_G, LUM_B = 0.55, 0.8, 0.3
# Cube constants
BLACK, WHITE = 16, 16+215
# Default color levels for the color cube
cubelevels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
# Generate list of midpoints of the above list
snaps = [(x+y)/2 for x, y in zip(cubelevels, [0]+cubelevels)[1:]]
# Color LookUp Table
CLUT = [(0,0,0)]*256
################################################################################
# Utility Functions
################################################################################
def die(msg):
print dedent(msg),
raise SystemExit(1)
def islight(rgb):
return lightness(rgb) > 0.5
def lightness(rgb):
r, g, b = rgb
return sqrt(sum(map(lambda x:x*x, (LUM_R*r, LUM_G*g, LUM_B*b)))/65536)
#return (LUM_R*r + LUM_G*g + LUM_B*b)/384.0
def hsv(rgb):
"""
Hue: [0, 360)
Sat: [0, 1]
Val: [0, 1]
"""
r, g, b = tuple(x/256.0 for x in rgb)
# Calculate value
v = max(r,g,b)
delta = float(v-min(r,g,b))
# Calculate saturation
if v==0: return(0, 0, 0) # all black, avoid div/0
s = delta/v
# Calculate hue
if r == v: h = ( g - b ) / delta # between yellow & magenta
elif g == v: h = 2 + ( b - r ) / delta # between cyan & yellow
else: h = 4 + ( r - g ) / delta # between magenta & cyan
h = int(60*h) # degrees
h %= 360
return (h, s, v)
################################################################################
# String Conversion Functions
################################################################################
def rgb2str(rgb):
fmt = "\033[48;2;{2};{3};{4}m\033[38;5;{1}m{0}\033[0m"
return fmt.format(rgb2hex(rgb), BLACK if islight(rgb) else WHITE,*rgb)
def cube2str(num, width=4):
return index2str(num+BLACK, width)
def index2str(n, width=5):
fmt = "\033[38;5;{{1}}m\033[48;5;{{0}}m{{0:^{}}}".format(width)
return fmt.format(n, BLACK if islight(CLUT[n]) else WHITE )
def hex2str(n):
rgb = CLUT[n]
fmt = "\033[38;5;{2}m\033[48;5;{0}m{1:^9}"
return fmt.format(n, rgb2hex(rgb), BLACK if islight(rgb) else WHITE )
def index2str_ansi(n, bank, width=5):
fmt = "\033[{};{}{}m{{:^{}}}".format(30 if n%8 else 37,
10 if bank else 4, n, width)
return fmt.format(n+bank)
################################################################################
# Terminal Handling
################################################################################
def readuntil(*end):
"Reads up to and including the specified character."
ch, c, data = None, 0, []
while ch not in end:
ch = stdin.read(1)
data.append(ch)
c+=1
return ''.join(data)
_readcolor_warning = False
def _readcolor():
global _readcolor_warning
readuntil("\033")
rgb = (0, 0, 0)
d = stdin.read(2)
if d[0] != ']': # Some (broken?) terminals seem to return values other than ]
if d != '[0': print "Got {!r}, expecting ']4'\r".format(d)
# Not the response we were expecting, so abort
return (None, None)
if d[1] != '4' and not _readcolor_warning:
print "\rWarning: While reading colormap: Terminal uses nonstandard reply (expected ]4, got {})\r\n".format(d),
_readcolor_warning = True
stdin.read(1)
n = int(readuntil(';').strip(';'))
data = readuntil("\033", "\007").split(':')
if data[0] == 'rgb':
# we know how to read this
rgb = tuple(int(x[:2], 16) for x in data[1].split('/'))
#print 'Queried color (d={}): {} = {}'.format(d, n, rgb)
#print '{0:>4d}\r'.format(n),
return (n, rgb)
def _writecolor(n, rgb):
stderr.write("\033]4;{};rgb:{:02x}/{:02x}/{:02x}\007".format(n, *rgb))
def _writeCLUT():
for i, rgb in zip(xrange(256), CLUT):
_writecolor(i, rgb)
def reset_defaults():
# Write straight into the CLUT since it's going
# right back to the terminal in a second anyway
for x in xrange(8): # Dark CGA colors (with special-case dark yellow)
CLUT[x] = (0xaa if x&1 else 0, (0x55 if x==3 else 0xaa) if x&2 else 0, 0xaa if x&4 else 0)
for x in xrange(8, 16): # Bright CGA colors
CLUT[x] = (0xff if x&1 else 0x55, 0xff if x&2 else 0x55, 0xff if x&4 else 0x55)
for x in xrange(16, 232): # Cube: lucky we have this handy cube2rgb function
CLUT[x] = cube2rgb(x-BLACK)
for x in xrange(232, 256): # Greyscale ramp, starts at 8, increases by 10
CLUT[x] = (10*(x-232) + 8,)*3
_writeCLUT()
def querycolor(n):
stderr.write("\033]4;{};?\007".format(n))
return _readcolor(n)[1]
def queryall():
end=256
stderr.write(("\033]4;{};?\007"*end).format(*range(end)))
stderr.write("\033[5n") # request Device Status Report at the end
last=0
while True:
n, c = _readcolor()
if n is None: break
yield n, c
def read_colormap():
print "Reading color map from terminal, please wait... ",
import tty, termios
fd = stdin.fileno()
old_settings = termios.tcgetattr(fd)
zero = 0
try:
tty.setraw(fd)
for n, c in queryall():
CLUT[n] = c
stderr.write('\r\033[K')
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
if not any(map(sum,CLUT)):
print "Warning: Unable to read colormap from terminal. (Terminal returned all zeros or gave no reply)"
################################################################################
# Theme Guessing
################################################################################
def guesstheme():
ours = [rgb2cube(x)+BLACK for x in CLUT[:16]]
#print ours
themes = {
"Solarized": [23, 166, 100, 136, 32, 168, 36, 145, 59, 166, 60, 66, 102, 62, 109, 231],
"Tango": [16, 160, 64, 178, 61, 96, 30, 188, 59, 196, 113, 221, 74, 139, 80, 231],
"XTerm": [16, 160, 40, 184, 20, 164, 44, 188, 102, 196, 46, 226, 63, 201, 51, 231],
"CGA colors": [16, 124, 34, 130, 19, 127, 37, 145, 59, 203, 83, 227, 63, 207, 87, 231],
"Pastel": [59, 59, 72, 180, 110, 175, 116, 188, 66, 181, 79, 223, 111, 212, 116, 231],
}
scores = Counter()
for name, theme in themes.iteritems():
scores[name] = sum([1 for a, b in zip(theme, ours) if a==b])
#print scores.most_common()
winner = scores.most_common()[0]
return (winner[0], winner[1]/16.0) if winner[1] > 8 else None
################################################################################
# Color Conversion - From RGB
################################################################################
def rgb2hex(rgb):
return "#{0:02x}{1:02x}{2:02x}".format(*rgb);
def rgb2cube(rgb):
r, g, b = rgb
# Snap to closest equivalent color
r, g, b = map(lambda x: len(tuple(s for s in snaps if s<x)), (r, g, b))
return (r*36 + g*6 + b)
################################################################################
# Color Conversion - To RGB
################################################################################
def hex2rgb(hexstr):
s = hexstr.strip('#')
return tuple(int(x, 16) for x in (s[0:2], s[2:4], s[4:6]))
def cube2rgb(num):
"""
Returns a default value for the color cube.
Does not take into account the actual color displayed
by the terminal (use the CLUT for that).
"""
return tuple(cubelevels[x] for x in (num/36, (num/6)%6, num%6))
################################################################################
# Print Functions
################################################################################
def space(pad, nl=0):
stdout.write("\033[0m" + ' '*pad + '\n'*nl)
def nl(count=1, pad=0):
stdout.write("\033[0m" + '\n'*count + ' '*pad)
def boxnl(begin=False, end=False):
stdout.write( '\n '.join(
("\033)0 \016\033[38;5;68;48;5;145ml{0:q^116}k\033[0m".format(
"\017\033[38;5;231;48;5;68;1m XTerm-256 Colormap \033[0;38;5;68;48;5;145m\016")
if begin else "\033[38;5;68;48;5;145m\016x\033[0m",
"\033[38;5;68;48;5;145mm{0:q<72}j\017\033[0m\033)B\n\n".format('')
if end else "\033[38;5;68;48;5;145mx\017\033[0m")
))
def print_ansi():
print "Colors printed using standard ANSI codes"
for a in (0, 8):
for x in xrange(8):
stdout.write( index2str_ansi(x, a) )
nl()
def print_hexcolors():
print "\n\033[0m{0:^80}\033[0m".format("-:| Your terminal's color scheme |:-")
space(4)
for x in xrange(16):
stdout.write( hex2str(x) )
if x==7:
nl(pad=4)
nl()
guess = guesstheme()
print "\033[0m{0:^80}\033[0m".format(
"My best guess for this theme is: {0} ({1:.0%} confidence)\n".format(*guess)
if guess else "I don't recognise the color theme you're using.\n"
)
def print_theme():
print "\033[0m{0:^80}\033[0m".format("Matching your color scheme (top row) to the color cube (bottom row):")
for x in xrange(16): stdout.write( index2str(x, 5) )
nl()
for x in xrange(16): stdout.write( index2str(BLACK+rgb2cube(CLUT[x]), 5) )
nl(2)
def print_colormap():
### Print Xterm-256 colormap
boxnl(begin=True)
# Base 16 colors
for x in xrange(16):
stdout.write( index2str(x, 9) )
if x==7: boxnl()
boxnl()
# 6x6x6 color cube
for row in xrange(12):
for group in (0, 72, 144):
for col in xrange(6):
index = row*6 + col + group
stdout.write( cube2str(index) )
boxnl()
# Greyscale ramp
for x in xrange(232, 256):
stdout.write( index2str(x, 6) )
if x==243: boxnl()
boxnl(end=True)
def _sort(cmpfilter):
CLUT.sort(lambda x,y: cmp(cmpfilter(x),cmpfilter(y)))
print "\033[0m{0:^80}\033[0m".format("Let's sort this shit.")
_writeCLUT()
print_colormap()
################################################################################
# Main
################################################################################
def main():
read_colormap()
if len(argv) > 1:
if argv[1].startswith('--'):
cmd = argv[1][2:]
if cmd == 'reset':
reset_defaults()
print "\033[0m{0:^80}\033[0m".format("Colormap reset to CGA/XTerm-256 defaults.")
print_colormap()
return 0
elif cmd == 'scramble':
from random import randint
def rnd(): return randint(0, 255);
CLUT[:] = [(rnd(), rnd(), rnd()) for x in xrange(256)]
_writeCLUT()
print "\033[0m{0:^80}\033[0m".format("Where will you be when the acid kicks in?")
print_colormap()
return 0
elif cmd == 'sort': return _sort(lightness)
elif cmd == 'hsort': return _sort(lambda x: hsv(x)[0])
elif cmd == 'ssort': return _sort(lambda x: hsv(x)[1])
elif cmd == 'vsort': return _sort(lambda x: hsv(x)[2])
else: usage()
for hexstr in argv[1:]:
rgb = hex2rgb(argv[1])
stdout.write( "The best match for the color \033[1m{0}\033[21m is {1}\033[0m\n"\
.format(rgb2hex(rgb), cube2str(rgb2cube(rgb),5)) )
elif len(argv) == 1:
print_hexcolors()
print_colormap()
print_theme()
else: usage()
def usage():
"""
Prints usage info. Duh.
"""
die("""\
Usage:
{0}
Prints information about the current Xterm-256 colormap.
{0} <hexcolor>
Finds the closest Xterm-256 color to your provided color.
Or run with one of the following options:
--reset --scramble --sort --hsort --ssort --vsort
Color values range from 0 (none) to 5 (fullbright).
""".format(path.basename(argv[0])))
try:
if __name__ == '__main__': main()
except Exception as e:
print_exc()
trace = inspect.trace()
trace.reverse()
for frame in trace:
func_locals = frame[0].f_locals
print 'Locals for {0}: {1}'.format(frame[3], repr(func_locals))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment