xiwal

Generate terminal color schemes
git clone https://git.ce9e.org/xiwal.git

commit
3d8839acaf0c4741a128bf3eb8f8229520a5a53f
parent
5a568136591570a7f6f869a38ac4c1a186106bfc
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2023-11-05 09:48
lint

Diffstat

M xiwal/__main__.py 40 ++++++++++++++++++++--------------------
M xiwal/image.py 26 +++++++++++++-------------
M xiwal/lch.py 148 ++++++++++++++++++++++++++++++------------------------------
M xiwal/scheme.py 86 ++++++++++++++++++++++++++++++------------------------------
M xiwal/term.py 30 +++++++++++++++---------------

5 files changed, 165 insertions, 165 deletions


diff --git a/xiwal/__main__.py b/xiwal/__main__.py

@@ -1,5 +1,5 @@
    1    -1 import sys
    2     1 import argparse
   -1     2 import sys
    3     3 
    4     4 from . import image
    5     5 from . import scheme
@@ -7,32 +7,32 @@ from . import term
    7     7 
    8     8 
    9     9 def parse_args():
   10    -1 	parser = argparse.ArgumentParser()
   11    -1 	parser.add_argument('--apply', '-a', action='store_true')
   12    -1 	parser.add_argument('--image', '-i')
   13    -1 	parser.add_argument('-n', type=int, default=8)
   14    -1 	parser.add_argument('colors', nargs='*')
   15    -1 	return parser.parse_args()
   -1    10     parser = argparse.ArgumentParser()
   -1    11     parser.add_argument('--apply', '-a', action='store_true')
   -1    12     parser.add_argument('--image', '-i')
   -1    13     parser.add_argument('-n', type=int, default=8)
   -1    14     parser.add_argument('colors', nargs='*')
   -1    15     return parser.parse_args()
   16    16 
   17    17 
   18    18 def main():
   19    -1 	args = parse_args()
   -1    19     args = parse_args()
   20    20 
   21    -1 	colors = []
   22    -1 	if args.image:
   23    -1 		colors += list(image.extract_colors(args.image, args.n))
   24    -1 	colors += args.colors
   -1    21     colors = []
   -1    22     if args.image:
   -1    23         colors += list(image.extract_colors(args.image, args.n))
   -1    24     colors += args.colors
   25    25 
   26    -1 	if len(colors) < 6:
   27    -1 		sys.exit('Need at least 6 colors')
   -1    26     if len(colors) < 6:
   -1    27         sys.exit('Need at least 6 colors')
   28    28 
   29    -1 	s = scheme.colors2scheme(colors)
   -1    29     s = scheme.colors2scheme(colors)
   30    30 
   31    -1 	print(';'.join(s))
   32    -1 	term.palette(s)
   33    -1 	if args.apply:
   34    -1 		term.apply(s)
   -1    31     print(';'.join(s))
   -1    32     term.palette(s)
   -1    33     if args.apply:
   -1    34         term.apply(s)
   35    35 
   36    36 
   37    37 if __name__ == '__main__':
   38    -1 	main()
   -1    38     main()

diff --git a/xiwal/image.py b/xiwal/image.py

@@ -3,16 +3,16 @@ import subprocess
    3     3 
    4     4 
    5     5 def extract_colors(path, colors):
    6    -1 	cmd = [
    7    -1 		'convert', path,
    8    -1 		'-resize', '25%',
    9    -1 		'-alpha', 'deactivate',
   10    -1 		'-colors', str(colors),
   11    -1 		'-unique-colors',
   12    -1 		'txt:-'
   13    -1 	]
   14    -1 	output = subprocess.check_output(cmd)
   15    -1 	for line in output.splitlines()[1:]:
   16    -1 		line = line.decode('ascii')
   17    -1 		match = re.search(r'#[0-9A-F]{6}', line)
   18    -1 		yield match.group()
   -1     6     cmd = [
   -1     7         'convert', path,
   -1     8         '-resize', '25%',
   -1     9         '-alpha', 'deactivate',
   -1    10         '-colors', str(colors),
   -1    11         '-unique-colors',
   -1    12         'txt:-'
   -1    13     ]
   -1    14     output = subprocess.check_output(cmd)
   -1    15     for line in output.splitlines()[1:]:
   -1    16         line = line.decode('ascii')
   -1    17         match = re.search(r'#[0-9A-F]{6}', line)
   -1    18         yield match.group()

diff --git a/xiwal/lch.py b/xiwal/lch.py

@@ -7,119 +7,119 @@ import math
    7     7 
    8     8 
    9     9 def _srgb2rgb(c):
   10    -1 	c = c / 255.0
   11    -1 	if c <= 0.04045:
   12    -1 		c = c / 12.92
   13    -1 	else:
   14    -1 		c = ((c + 0.055) / 1.055) ** 2.4
   15    -1 	return c
   -1    10     c = c / 255.0
   -1    11     if c <= 0.04045:
   -1    12         c = c / 12.92
   -1    13     else:
   -1    14         c = ((c + 0.055) / 1.055) ** 2.4
   -1    15     return c
   16    16 
   17    17 
   18    18 def _rgb2srgb(c):
   19    -1 	if c <= 0.0031308:
   20    -1 		c = c * 12.92
   21    -1 	else:
   22    -1 		c = 1.055 * (c ** (1 / 2.4)) - 0.055
   23    -1 	return c * 255
   -1    19     if c <= 0.0031308:
   -1    20         c = c * 12.92
   -1    21     else:
   -1    22         c = 1.055 * (c ** (1 / 2.4)) - 0.055
   -1    23     return c * 255
   24    24 
   25    25 
   26    26 def rgb2lab(rgb):
   27    -1 	r, g, b = map(_srgb2rgb, rgb)
   -1    27     r, g, b = map(_srgb2rgb, rgb)
   28    28 
   29    -1 	l = 0.4121656120 * r + 0.5362752080 * g + 0.0514575653 * b
   30    -1 	m = 0.2118591070 * r + 0.6807189584 * g + 0.1074065790 * b
   31    -1 	s = 0.0883097947 * r + 0.2818474174 * g + 0.6302613616 * b
   -1    29     l = 0.4121656120 * r + 0.5362752080 * g + 0.0514575653 * b
   -1    30     m = 0.2118591070 * r + 0.6807189584 * g + 0.1074065790 * b
   -1    31     s = 0.0883097947 * r + 0.2818474174 * g + 0.6302613616 * b
   32    32 
   33    -1 	l_ = l ** (1 / 3)
   34    -1 	m_ = m ** (1 / 3)
   35    -1 	s_ = s ** (1 / 3)
   -1    33     l_ = l ** (1 / 3)
   -1    34     m_ = m ** (1 / 3)
   -1    35     s_ = s ** (1 / 3)
   36    36 
   37    -1 	L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
   38    -1 	a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
   39    -1 	b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
   -1    37     L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
   -1    38     a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
   -1    39     b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
   40    40 
   41    -1 	return L, a, b
   -1    41     return L, a, b
   42    42 
   43    43 
   44    44 def lab2rgb(lab):
   45    -1 	L, a, b = lab
   -1    45     L, a, b = lab
   46    46 
   47    -1 	l_ = L + 0.3963377774 * a + 0.2158037573 * b
   48    -1 	m_ = L - 0.1055613458 * a - 0.0638541728 * b
   49    -1 	s_ = L - 0.0894841775 * a - 1.2914855480 * b
   -1    47     l_ = L + 0.3963377774 * a + 0.2158037573 * b
   -1    48     m_ = L - 0.1055613458 * a - 0.0638541728 * b
   -1    49     s_ = L - 0.0894841775 * a - 1.2914855480 * b
   50    50 
   51    -1 	l = l_ ** 3
   52    -1 	m = m_ ** 3
   53    -1 	s = s_ ** 3
   -1    51     l = l_ ** 3
   -1    52     m = m_ ** 3
   -1    53     s = s_ ** 3
   54    54 
   55    -1 	r = +4.0767245293 * l - 3.3072168827 * m + 0.2307590544 * s
   56    -1 	g = -1.2681437731 * l + 2.6093323231 * m - 0.3411344290 * s
   57    -1 	b = -0.0041119885 * l - 0.7034763098 * m + 1.7068625689 * s
   -1    55     r = +4.0767245293 * l - 3.3072168827 * m + 0.2307590544 * s
   -1    56     g = -1.2681437731 * l + 2.6093323231 * m - 0.3411344290 * s
   -1    57     b = -0.0041119885 * l - 0.7034763098 * m + 1.7068625689 * s
   58    58 
   59    -1 	r = max(0, min(1, r))
   60    -1 	g = max(0, min(1, g))
   61    -1 	b = max(0, min(1, b))
   -1    59     r = max(0, min(1, r))
   -1    60     g = max(0, min(1, g))
   -1    61     b = max(0, min(1, b))
   62    62 
   63    -1 	return tuple(map(_rgb2srgb, (r, g, b)))
   -1    63     return tuple(map(_rgb2srgb, (r, g, b)))
   64    64 
   65    65 
   66    66 def rgb2lch(rgb):
   67    -1 	l, a, b = rgb2lab(rgb)
   68    -1 	c = (a ** 2 + b ** 2) ** 0.5
   69    -1 	h = 0
   70    -1 	if abs(a) > 0.0001 or abs(b) > 0.0001:
   71    -1 		h = math.atan2(b, a)
   72    -1 		h = (h + 4 * math.pi) % (2 * math.pi)
   73    -1 	return l, c, h
   -1    67     l, a, b = rgb2lab(rgb)
   -1    68     c = (a ** 2 + b ** 2) ** 0.5
   -1    69     h = 0
   -1    70     if abs(a) > 0.0001 or abs(b) > 0.0001:
   -1    71         h = math.atan2(b, a)
   -1    72         h = (h + 4 * math.pi) % (2 * math.pi)
   -1    73     return l, c, h
   74    74 
   75    75 
   76    76 def _lch2rgb(lch):
   77    -1 	l, c, h = lch
   78    -1 	a = math.cos(h) * c
   79    -1 	b = math.sin(h) * c
   80    -1 	return lab2rgb((l, a, b))
   -1    77     l, c, h = lch
   -1    78     a = math.cos(h) * c
   -1    79     b = math.sin(h) * c
   -1    80     return lab2rgb((l, a, b))
   81    81 
   82    82 
   83    83 def lch2rgb(lch):
   84    -1 	rgb = _lch2rgb(lch)
   -1    84     rgb = _lch2rgb(lch)
   85    85 
   86    -1 	if any(x < 0 or x > 255 for x in rgb):
   87    -1 		c_min = 0
   88    -1 		c_max = lch[1]
   -1    86     if any(x < 0 or x > 255 for x in rgb):
   -1    87         c_min = 0
   -1    88         c_max = lch[1]
   89    89 
   90    -1 		while c_max - c_min > 0.01:
   91    -1 			c_tmp = (c_min + c_max) / 2
   92    -1 			rgb = _lch2rgb((lch[0], c_tmp, lch[2]))
   93    -1 			if any(x < 0 or x > 255 for x in rgb):
   94    -1 				c_max = c_tmp
   95    -1 			else:
   96    -1 				c_min = c_tmp
   -1    90         while c_max - c_min > 0.01:
   -1    91             c_tmp = (c_min + c_max) / 2
   -1    92             rgb = _lch2rgb((lch[0], c_tmp, lch[2]))
   -1    93             if any(x < 0 or x > 255 for x in rgb):
   -1    94                 c_max = c_tmp
   -1    95             else:
   -1    96                 c_min = c_tmp
   97    97 
   98    -1 	return rgb
   -1    98     return rgb
   99    99 
  100   100 
  101   101 def format_hex(rgb):
  102    -1 	return '#{:02x}{:02x}{:02x}'.format(*[int(x) for x in rgb])
   -1   102     return '#{:02x}{:02x}{:02x}'.format(*[int(x) for x in rgb])
  103   103 
  104   104 
  105   105 def parse_hex(s):
  106    -1 	s = s.lstrip('#')
  107    -1 	if len(s) == 6:
  108    -1 		r = int(s[0:2], 16)
  109    -1 		g = int(s[2:4], 16)
  110    -1 		b = int(s[4:6], 16)
  111    -1 	elif len(s) == 3:
  112    -1 		r = int(s[0], 16) * 17
  113    -1 		g = int(s[1], 16) * 17
  114    -1 		b = int(s[2], 16) * 17
  115    -1 	else:
  116    -1 		raise ValueError('Invalid hex color: %s' % s)
  117    -1 	return r, g, b
   -1   106     s = s.lstrip('#')
   -1   107     if len(s) == 6:
   -1   108         r = int(s[0:2], 16)
   -1   109         g = int(s[2:4], 16)
   -1   110         b = int(s[4:6], 16)
   -1   111     elif len(s) == 3:
   -1   112         r = int(s[0], 16) * 17
   -1   113         g = int(s[1], 16) * 17
   -1   114         b = int(s[2], 16) * 17
   -1   115     else:
   -1   116         raise ValueError('Invalid hex color: %s' % s)
   -1   117     return r, g, b
  118   118 
  119   119 
  120   120 def from_hex(s):
  121    -1 	return rgb2lch(parse_hex(s))
   -1   121     return rgb2lch(parse_hex(s))
  122   122 
  123   123 
  124   124 def to_hex(lch):
  125    -1 	return format_hex(lch2rgb(lch))
   -1   125     return format_hex(lch2rgb(lch))

diff --git a/xiwal/scheme.py b/xiwal/scheme.py

@@ -1,5 +1,5 @@
    1    -1 import math
    2     1 import functools
   -1     2 import math
    3     3 
    4     4 from . import lch
    5     5 
@@ -26,64 +26,64 @@ ORDER = [0, 2, 1, 4, 5, 3]
   26    26 
   27    27 
   28    28 def permutate(a, n):
   29    -1 	if n == 0:
   30    -1 		yield ()
   31    -1 	else:
   32    -1 		for i in range(len(a)):
   33    -1 			for rest in permutate(a[:i] + a[i + 1:], n - 1):
   34    -1 				yield (a[i], *rest)
   -1    29     if n == 0:
   -1    30         yield ()
   -1    31     else:
   -1    32         for i in range(len(a)):
   -1    33             for rest in permutate(a[:i] + a[i + 1:], n - 1):
   -1    34                 yield (a[i], *rest)
   35    35 
   36    36 
   37    37 @functools.lru_cache(maxsize=32)
   38    38 def distance(color, i):
   39    -1 	hue = math.pi / 3 * ORDER[i] + OFFSET
   40    -1 	d = abs(color[2] - hue)
   41    -1 	if d > math.pi:
   42    -1 		d = 2 * math.pi - d
   -1    39     hue = math.pi / 3 * ORDER[i] + OFFSET
   -1    40     d = abs(color[2] - hue)
   -1    41     if d > math.pi:
   -1    42         d = 2 * math.pi - d
   43    43 
   44    -1 	c = color[1]
   45    -1 	if i in [0, 1]:
   46    -1 		c = max(c, C_RG)
   47    -1 		d += (c - color[1]) / (c + color[1])
   -1    44     c = color[1]
   -1    45     if i in [0, 1]:
   -1    46         c = max(c, C_RG)
   -1    47         d += (c - color[1]) / (c + color[1])
   48    48 
   49    -1 	return d ** 4 * c, c
   -1    49     return d ** 4 * c, c
   50    50 
   51    51 
   52    52 def score(colors):
   53    -1 	sum_score = 0
   54    -1 	sum_chroma = 0
   -1    53     sum_score = 0
   -1    54     sum_chroma = 0
   55    55 
   56    -1 	for i, color in enumerate(colors):
   57    -1 		score, chroma = distance(color, i)
   58    -1 		sum_score += score
   59    -1 		sum_chroma += chroma
   -1    56     for i, color in enumerate(colors):
   -1    57         score, chroma = distance(color, i)
   -1    58         sum_score += score
   -1    59         sum_chroma += chroma
   60    60 
   61    -1 	return sum_score / sum_chroma
   -1    61     return sum_score / sum_chroma
   62    62 
   63    63 
   64    64 def scheme(colors, dominant):
   65    -1 	c_grey = min(dominant[1], C_GREY)
   -1    65     c_grey = min(dominant[1], C_GREY)
   66    66 
   67    -1 	yield L_DARK[0], c_grey, dominant[2]
   68    -1 	for i in range(6):
   69    -1 		c = colors[i][1] * C_FACTOR
   70    -1 		if i in [0, 1]:
   71    -1 			c = max(c, C_RG)
   72    -1 		yield L_DARK[i + 1], c, colors[i][2]
   73    -1 	yield L_DARK[7], c_grey, dominant[2]
   -1    67     yield L_DARK[0], c_grey, dominant[2]
   -1    68     for i in range(6):
   -1    69         c = colors[i][1] * C_FACTOR
   -1    70         if i in [0, 1]:
   -1    71             c = max(c, C_RG)
   -1    72         yield L_DARK[i + 1], c, colors[i][2]
   -1    73     yield L_DARK[7], c_grey, dominant[2]
   74    74 
   75    -1 	yield L_LIGHT[0], c_grey, dominant[2]
   76    -1 	for i in range(6):
   77    -1 		c = colors[i][1] * C_FACTOR
   78    -1 		if i in [0, 1]:
   79    -1 			c = max(c, C_RG)
   80    -1 		yield L_LIGHT[i + 1], c, colors[i][2]
   81    -1 	yield L_LIGHT[7], c_grey, dominant[2]
   -1    75     yield L_LIGHT[0], c_grey, dominant[2]
   -1    76     for i in range(6):
   -1    77         c = colors[i][1] * C_FACTOR
   -1    78         if i in [0, 1]:
   -1    79             c = max(c, C_RG)
   -1    80         yield L_LIGHT[i + 1], c, colors[i][2]
   -1    81     yield L_LIGHT[7], c_grey, dominant[2]
   82    82 
   83    83 
   84    84 def colors2scheme(colors):
   85    -1 	colors = [lch.from_hex(c) for c in colors]
   86    -1 	dominant = colors[0]
   87    -1 	colors = min(permutate(colors, 6), key=score)
   88    -1 	s = scheme(colors, dominant)
   89    -1 	return [lch.to_hex(c) for c in s]
   -1    85     colors = [lch.from_hex(c) for c in colors]
   -1    86     dominant = colors[0]
   -1    87     colors = min(permutate(colors, 6), key=score)
   -1    88     s = scheme(colors, dominant)
   -1    89     return [lch.to_hex(c) for c in s]

diff --git a/xiwal/term.py b/xiwal/term.py

@@ -1,25 +1,25 @@
    1    -1 import os
    2     1 import glob
   -1     2 import os
    3     3 
    4     4 from . import lch
    5     5 
    6     6 
    7     7 def apply(scheme, cachefile='~/.cache/wal/sequences'):
    8    -1 	cachefile = os.path.expanduser(cachefile)
    9    -1 	os.makedirs(os.path.dirname(cachefile), exist_ok=True)
   -1     8     cachefile = os.path.expanduser(cachefile)
   -1     9     os.makedirs(os.path.dirname(cachefile), exist_ok=True)
   10    10 
   11    -1 	for path in [cachefile] + glob.glob('/dev/pts/[0-9]*'):
   12    -1 		with open(path, 'w') as tty:
   13    -1 			for i in range(16):
   14    -1 				tty.write('\033]4;%i;%s\033\\' % (i, scheme[i]))
   15    -1 			tty.write('\033]%i;%s\033\\' % (11, scheme[0]))
   16    -1 			tty.write('\033]%i;%s\033\\' % (10, scheme[15]))
   -1    11     for path in [cachefile, *glob.glob('/dev/pts/[0-9]*')]:
   -1    12         with open(path, 'w') as tty:
   -1    13             for i in range(16):
   -1    14                 tty.write('\033]4;%i;%s\033\\' % (i, scheme[i]))
   -1    15             tty.write('\033]%i;%s\033\\' % (11, scheme[0]))
   -1    16             tty.write('\033]%i;%s\033\\' % (10, scheme[15]))
   17    17 
   18    18 
   19    19 def palette(scheme):
   20    -1 	s = []
   21    -1 	for i in range(16):
   22    -1 		r, g, b = lch.parse_hex(scheme[i])
   23    -1 		s.append('\033[48;2;%i;%i;%im    \033[0m' % (r, g, b))
   24    -1 	print(''.join(s[:8]))
   25    -1 	print(''.join(s[8:]))
   -1    20     s = []
   -1    21     for i in range(16):
   -1    22         r, g, b = lch.parse_hex(scheme[i])
   -1    23         s.append('\033[48;2;%i;%i;%im    \033[0m' % (r, g, b))
   -1    24     print(''.join(s[:8]))
   -1    25     print(''.join(s[8:]))