diff --git a/.gitignore b/.gitignore index c749d68..5f9d397 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,11 @@ *.ipynb *.o *.so +*.py +*.sh +*.txt .vscode/ build/ -dist/ \ No newline at end of file +dist/ +draft/ +perception-default/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 62c9f9d..4666830 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,7 @@ project(perception) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wconversion -Wno-unused-private-field -Wno-unused-variable -Wno-unused-parameter -fno-omit-frame-pointer -fsanitize=address") set(CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_STATIC_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wconversion -Wno-unused-private-field -Wno-unused-variable -Wno-unused-parameter") +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wconversion -Wno-unused-private-field -Wno-unused-variable -Wno-unused-parameter") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(LIB_SOURCES src/colorspace.cpp src/CIEDE2000.cpp src/fitness.cpp) diff --git a/python/cli.py b/python/cli.py new file mode 100644 index 0000000..1282a3c --- /dev/null +++ b/python/cli.py @@ -0,0 +1,111 @@ +from argparse import ArgumentParser, RawDescriptionHelpFormatter +from getpass import getuser +from textwrap import dedent +from .profile import parse +from .theme import update_context +from .version import __version__ + + +def argparser(): + ps = ArgumentParser( + prog='perception', + description=dedent(""" + Generate optimized discernible color palette and themes settings + + Example: + perception --temperature -1 --profile demo + perception --background 97 --profile vscode + """), + formatter_class=RawDescriptionHelpFormatter) + + ps.add_argument( + '-n', + '--name', + default='default', + type=str, + help='theme name, default: \'default\'') + ps.add_argument( + '-f', + '--foreground', + '--Lf', + default=-1, + dest='Lf', + metavar='Lf', + type=float, + help='foreground luminocity between 0 < Lf < 100') + ps.add_argument( + '-b', + '--background', + '--Lb', + default=-1, + dest='Lb', + metavar='Lb', + type=float, + help='background luminocity 0 < Lb < 100') + ps.add_argument( + '-L', + '--palette-luminocity', + '--L3', + default=-1, + dest='L3', + metavar='L3', + type=float, + help='main palette luminocity 0 < L3 < 100') + ps.add_argument( + '--maxC', + default=-1, + metavar='maxC', + type=float, + help='maximal chroma > 0') + ps.add_argument( + '-T', + '--temperature', + default=0, + dest='dK', + metavar='dK', + type=float, + help='temperature hint, -1(warm) < dK < 1(cold)') + ps.add_argument( + '-p', + '--profile', + action='append', + default=[], + dest='profiles', + metavar='PROFILE', + help='profiles to be parsed') + ps.add_argument( + '--L2', + default=-1, + dest='L2', + metavar='L2', + type=float, + help='(advanced) 0 < L2 < 100') + ps.add_argument( + '--L1', + default=-1, + dest='L1', + metavar='L1', + type=float, + help='(advanced) 0 < L1 < 100') + ps.add_argument( + '--L0', + default=-1, + dest='L0', + metavar='L0', + type=float, + help='(advanced) 0 < L0 < 100') + ps.add_argument('-v', '--verbose', action='store_true', default=False) + + return ps + + +def main(): + args = argparser().parse_args() + ctx = { + 'name': args.name, + 'version': __version__, + 'user': getuser(), + } + + update_context(args, ctx) + parse(args, ctx) diff --git a/python/data/demo/context.json.mustache b/python/data/demo/context.json.mustache new file mode 100644 index 0000000..fcc8924 --- /dev/null +++ b/python/data/demo/context.json.mustache @@ -0,0 +1,46 @@ +{ + "name": "{{name}}", + "version": "{{version}}", + "user": "{{user}}", + "ui-theme": "{{ui-theme}}", + + // Foreground + "fg-hex": "{{fg-hex}}", + + // Foreground Palette + "main-0-hex": "{{main-0-hex}}", + "main-1-hex": "{{main-1-hex}}", + "main-2-hex": "{{main-2-hex}}", + "main-3-hex": "{{main-3-hex}}", + "main-4-hex": "{{main-4-hex}}", + "main-5-hex": "{{main-5-hex}}", + "main-6-hex": "{{main-6-hex}}", + + // Lines + "line-2-hex": "{{line-2-hex}}", + "line-1-hex": "{{line-1-hex}}", + "line-0-hex": "{{line-0-hex}}", + + // Semantic Foregrounds + "red-2-hex": "{{red-2-hex}}", + "yellow-2-hex": "{{yellow-2-hex}}", + "green-2-hex": "{{green-2-hex}}", + "blue-2-hex": "{{blue-2-hex}}", + + // Semantic Inversed Backgrounds + "red-1-hex": "{{red-1-hex}}", + "yellow-1-hex": "{{yellow-1-hex}}", + "green-1-hex": "{{green-1-hex}}", + "blue-1-hex": "{{blue-1-hex}}", + + // Semantic Backgrounds/Inversed Foregrounds + "red-0-hex": "{{red-0-hex}}", + "yellow-0-hex": "{{yellow-0-hex}}", + "green-0-hex": "{{green-0-hex}}", + "blue-0-hex": "{{blue-0-hex}}", + + // Backgrounds + "bg-2-hex": "{{bg-2-hex}}", + "bg-1-hex": "{{bg-1-hex}}", + "bg-0-hex": "{{bg-0-hex}}" +} \ No newline at end of file diff --git a/python/data/demo/perception-{{name}}-demo.html.mustache b/python/data/demo/perception-{{name}}-demo.html.mustache new file mode 100644 index 0000000..53e462d --- /dev/null +++ b/python/data/demo/perception-{{name}}-demo.html.mustache @@ -0,0 +1,130 @@ + + + + + Perception Theme - {{name}} | Demo + + + + +
+ +
+
+Foreground:
+fg-hex {{fg-hex}}
+
+Lines:
+line-2-hex {{line-2-hex}}
+line-1-hex {{line-1-hex}}
+line-0-hex {{line-0-hex}}
+
+Foreground Palette:
+main-0-hex {{main-0-hex}}
+main-1-hex {{main-1-hex}}
+main-2-hex {{main-2-hex}}
+main-3-hex {{main-3-hex}}
+main-4-hex {{main-4-hex}}
+main-5-hex {{main-5-hex}}
+main-6-hex {{main-6-hex}}
+
+Semantic Foregrounds:
+red-2-hex {{red-2-hex}}
+yellow-2-hex {{yellow-2-hex}}
+green-2-hex {{green-2-hex}}
+blue-2-hex {{blue-2-hex}}
+        
+
+Semantic Inversed Backgrounds:
+red-1-hex {{red-1-hex}}
+yellow-1-hex {{yellow-1-hex}}
+green-1-hex {{green-1-hex}}
+blue-1-hex {{blue-1-hex}}
+
+Semantic Backgrounds/Inversed Foregrounds:
+red-0-hex {{red-0-hex}}
+yellow-0-hex {{yellow-0-hex}}
+green-0-hex {{green-0-hex}}
+blue-0-hex {{blue-0-hex}}
+
+Backgrounds:
+bg-2-hex {{bg-2-hex}}
+bg-1-hex {{bg-1-hex}}
+bg-0-hex {{bg-0-hex}}
+        
+
+ + \ No newline at end of file diff --git a/python/data/demo/perception-{{name}}-thumb.svg.mustache b/python/data/demo/perception-{{name}}-thumb.svg.mustache new file mode 100644 index 0000000..225a381 --- /dev/null +++ b/python/data/demo/perception-{{name}}-thumb.svg.mustache @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 def Fib(n): 2 if n == 0: return 0 3 elif n == 1: return 1 4 else: return Fib(n-1)+Fib(n-2) 5 6 end = int(input("Print Fibonacci number up to: ")) 7 8 for i in range(end): 9 print(f'{i}: {Fib(i)}') + + + + + + + diff --git a/python/data/nvim/colors/perception-{{name}}.vim.mustache b/python/data/vim/colors/perception-{{name}}.vim.mustache similarity index 100% rename from python/data/nvim/colors/perception-{{name}}.vim.mustache rename to python/data/vim/colors/perception-{{name}}.vim.mustache diff --git a/python/data/vscode/perception-theme-{{name}}/themes/perception-theme.json.mustache b/python/data/vscode/perception-theme-{{name}}/themes/perception-theme.json.mustache index bb13f42..f97068f 100644 --- a/python/data/vscode/perception-theme-{{name}}/themes/perception-theme.json.mustache +++ b/python/data/vscode/perception-theme-{{name}}/themes/perception-theme.json.mustache @@ -213,9 +213,9 @@ // Notification dialog colors "notification.background": "#{{bg-1-hex}}", "notification.foreground": "#{{line-2-hex}}", - "notification.buttonBackground": "#{{blue-1-hex}}", - "notification.buttonHoverBackground": "#{{blue-1-hex}}cc", - "notification.buttonForeground": "#{{fg-hex}}", + "notification.buttonBackground": "#{{blue-2-hex}}", + "notification.buttonHoverBackground": "#{{blue-2-hex}}cc", + "notification.buttonForeground": "#{{bg-0-hex}}", "notification.infoBackground": "#{{blue-2-hex}}", "notification.infoForeground": "#{{blue-0-hex}}", "notification.warningBackground": "#{{yellow-2-hex}}", @@ -239,10 +239,10 @@ "terminal.ansiBrightBlack": "#{{fg-hex}}", "terminal.ansiRed": "#{{main-0-hex}}", "terminal.ansiBrightRed": "#{{main-0-hex}}", - "terminal.ansiYellow": "#{{main-1-hex}}", - "terminal.ansiBrightYellow": "#{{main-1-hex}}", - "terminal.ansiGreen": "#{{main-2-hex}}", - "terminal.ansiBrightGreen": "#{{main-2-hex}}", + "terminal.ansiYellow": "#{{main-2-hex}}", + "terminal.ansiBrightYellow": "#{{main-2-hex}}", + "terminal.ansiGreen": "#{{main-3-hex}}", + "terminal.ansiBrightGreen": "#{{main-3-hex}}", "terminal.ansiCyan": "#{{main-4-hex}}", "terminal.ansiBrightCyan": "#{{main-4-hex}}", "terminal.ansiBlue": "#{{main-5-hex}}", diff --git a/python/pystache_tree.py b/python/profile.py similarity index 57% rename from python/pystache_tree.py rename to python/profile.py index 54194da..fc8b24c 100644 --- a/python/pystache_tree.py +++ b/python/profile.py @@ -32,3 +32,30 @@ def recursive_render(src_path, shutil.copyfile(src_path, dest_path) if debug: print(f'Copy file {dest_path}') + + +# @param args.profiles +# @param args.verbose +def parse(args, ctx): + srcs = ['.', os.path.join(os.path.dirname(__file__), 'data')] + dest = f'./perception-{args.name}' + if not args.profiles: + args.profiles = ['demo'] + print('Available builtin profiles:') + for d in srcs[1:]: + if not os.path.isdir(d): + continue + for n in os.listdir(d): + print(f' {n}') + for profile in args.profiles: + profile_srcs = [os.path.join(d, profile) for d in srcs] + found = False + for profile_src in profile_srcs: + if os.path.exists(profile_src): + found = True + break + if not found: + raise OSError(f'Cannot find profile \'{profile}\'') + profile_dest = os.path.join(dest, profile) + print(f'Parse profile \'{profile}\' into {profile_dest}') + recursive_render(profile_src, profile_dest, ctx, debug=args.verbose) diff --git a/python/theme.py b/python/theme.py index 7dcf909..d2c4319 100644 --- a/python/theme.py +++ b/python/theme.py @@ -1,10 +1,5 @@ -from argparse import ArgumentParser -from getpass import getuser from itertools import product -import os from .native import LABtoRGB, LCHtoLAB, maxChroma, perception -from .version import __version__ -from .pystache_tree import recursive_render D65_A = -1.65 / 100 # a component of D65 in LAB D65_B = -19.33 / 100 # b component of D65 in LAB @@ -31,82 +26,13 @@ def tempered_gray(L, dK): return (L, L * D65_A * dK, L * D65_B * dK) -def argparser(): - ps = ArgumentParser() - - ps.add_argument( - '-n', - '--name', - default='default', - type=str, - help='theme name, default: \'default\'') - ps.add_argument( - '-f', - '--foreground', - default=-1, - dest='Lf', - metavar='Lf', - type=float, - help='foreground luminocity between 0 < Lf < 100') - ps.add_argument( - '-b', - '--background', - default=-1, - dest='Lb', - metavar='Lb', - type=float, - help='background luminocity 0 < Lb < 100') - ps.add_argument( - '-L', - default=-1, - type=float, - help='main palette luminocity 0 < L < 100') - ps.add_argument( - '--maxC', - default=-1, - dest='maxC', - type=float, - help='main palette maximal chroma > 0') - ps.add_argument( - '--temp', - default=0, - dest='dK', - metavar='dK', - type=float, - help='temperature hint, -1(warm) < dK < 1(cold)') - ps.add_argument( - '-i', - '--in-place', - action='store_true', - default=False, - dest='inplace', - help='update themes in place') - ps.add_argument('-v', '--verbose', action='store_true', default=False) - - return ps - - -def parse(args, ctx): - data_dir = os.path.join(os.path.dirname(__file__), 'data') - if args.inplace: - templates = [ - ('vscode', os.path.expanduser('~/.vscode/extensions')), - ('nvim', os.path.expanduser('~/.vim')), - ('nvim', os.path.expanduser('~/.config/nvim')), - ] - for profile, dest in templates: - print(f'Apply profile \'{profile}\'') - recursive_render( - os.path.join(data_dir, profile), dest, ctx, debug=args.verbose) - else: - dest = os.path.abspath(f'./perception-themes-{args.name}') - print(f'Generate themes in {dest}') - recursive_render(data_dir, dest, ctx, debug=args.verbose) - - -def main(): - args = argparser().parse_args() - +# @param args.Lf +# @param args.Lb +# @param args.L +# @param args.maxC +# @param args.dK +# @param args.verbose +def update_context(args, ctx): if args.Lb < 0: if args.Lf < 0: args.Lf = 93 @@ -114,43 +40,45 @@ def main(): elif args.Lf < 0: args.Lf = 100 - args.Lb + ctx['fg-hex'] = rgb_hex(LABtoRGB((tempered_gray(args.Lf, args.dK)))) + ctx['ui-theme'] = 'vs-dark' if args.Lb < 50 else 'vs' + # L3: text color, very close to Lf - L3 = (interpolate2D(0, 100, 45, 100, 0, 80, 60, 60, 60)(args.Lf, args.Lb) - if args.L < 0 else args.L) + if args.L3 < 0: + args.L3 = interpolate2D(0, 100, 45, 100, 0, 80, 60, 60, 60)(args.Lf, + args.Lb) # L2: UI line/background, large inter-distance, close to Lf - L2 = interpolate2D(0, 100, 50, 100, 0, 70, 60, 60, 60)(args.Lf, args.Lb) + if args.L2 < 0: + args.L2 = interpolate2D(0, 100, 50, 100, 0, 70, 60, 60, 60)(args.Lf, + args.Lb) # L1: indicator color, large inter-distance, close to Lb - L1 = interpolate2D(0, 100, 70, 100, 0, 55, 60, 60, 60)(args.Lf, args.Lb) + if args.L1 < 0: + args.L1 = interpolate2D(0, 100, 70, 100, 0, 55, 60, 60, 60)(args.Lf, + args.Lb) # L0: background color, very close to Lb - L0 = interpolate2D(0, 100, 90, 100, 0, 15, 60, 60, 60)(args.Lf, args.Lb) + if args.L0 < 0: + args.L0 = interpolate2D(0, 100, 90, 100, 0, 15, 60, 60, 60)(args.Lf, + args.Lb) if (args.verbose): - print(f'Init parameters: Lf={args.Lf} L={L3} L2={L2} L1={L1} ' + - f'L0={L0} Lb={args.Lb} dK={args.dK}') - else: print( - f'Init parameters: Lf={args.Lf} L={L3} Lb={args.Lb} dK={args.dK}') + f'Init parameters: Lf={args.Lf} L3={args.L3} args.L2={args.L2} args.L1={args.L1} ' + + f'args.L0={args.L0} Lb={args.Lb} dK={args.dK}') sgn = 1 if args.Lf > args.Lb else -1 palette3 = perception( 7, - L=L3, + L=args.L3, maxC=args.maxC, - fixed=[tempered_gray(L3, args.dK)], + fixed=[tempered_gray(args.L3, args.dK)], quiet=not args.verbose) - ctx = { - 'name': args.name, - 'version': __version__, - 'user': getuser(), - 'ui-theme': 'vs-dark' if args.Lb < 50 else 'vs', - 'fg-hex': rgb_hex(LABtoRGB((tempered_gray(args.Lf, args.dK)))) - } for i, rgb in enumerate(palette3['rgb']): ctx[f'main-{i}-hex'] = rgb_hex(rgb) - for (name, hue), (level, L) in product(SEMANTIC_HUE.items(), - enumerate([L0, L1, L2])): + for (name, hue), (level, + L) in product(SEMANTIC_HUE.items(), + enumerate([args.L0, args.L1, args.L2])): ctx[f'{name}-{level}-hex'] = rgb_hex( LABtoRGB(LCHtoLAB(maxChroma([L, 0, hue], maxC=args.maxC)))) @@ -161,8 +89,3 @@ def main(): for i, delta in enumerate([0, 5, 12]): ctx[f'bg-{i}-hex'] = rgb_hex( LABtoRGB(tempered_gray(args.Lb + sgn * delta, args.dK))) - - print('Generated palette: ' + ' '.join( - [f'#{rgb_hex(rgb)}' for rgb in palette3["rgb"]])) - - parse(args, ctx) diff --git a/setup.py b/setup.py index 1b2b064..6f284ba 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from distutils.version import LooseVersion +from setuptools import setup, Extension +from setuptools.command.build_ext import build_ext import glob import multiprocessing import os +import platform +import re import subprocess import sys -from setuptools import setup, Extension -from setuptools.command.build_ext import build_ext class CMakeExtension(Extension): @@ -17,6 +20,23 @@ def __init__(self, name, sourcedir=''): class CMakeBuild(build_ext): + def run(self): + try: + out = subprocess.check_output(['cmake', '--version']) + except OSError: + raise RuntimeError( + "CMake must be installed to build the following extensions: " + + ", ".join(e.name for e in self.extensions)) + + if platform.system() == "Windows": + cmake_version = LooseVersion( + re.search(r'version\s*([\d.]+)', out.decode()).group(1)) + if cmake_version < '3.1.0': + raise RuntimeError("CMake >= 3.1.0 is required on Windows") + + for ext in self.extensions: + self.build_extension(ext) + def build_extension(self, ext): extdir = os.path.abspath( os.path.dirname(self.get_ext_fullpath(ext.name))) @@ -28,8 +48,17 @@ def build_extension(self, ext): cfg = 'Debug' if self.debug else 'Release' build_args = ['--config', cfg] - cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] - build_args += ['--', '-j{0}'.format(multiprocessing.cpu_count())] + if platform.system() == "Windows": + cmake_args += [ + '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format( + cfg.upper(), extdir) + ] + if sys.maxsize > 2**32: + cmake_args += ['-A', 'x64'] + build_args += ['--', '/m'] + else: + cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] + build_args += ['--', '-j{0}'.format(multiprocessing.cpu_count())] if not os.path.exists(self.build_temp): os.makedirs(self.build_temp) @@ -59,9 +88,7 @@ def build_extension(self, ext): for p in glob.glob('python/data/**', recursive=True) ] }, - entry_points={ - 'console_scripts': [ - 'perception-theme = perception.theme:main', - ] - }, + entry_points={'console_scripts': [ + 'perception = perception.cli:main', + ]}, cmdclass=dict(build_ext=CMakeBuild))