Files
odysseus/src/theme_palette.py
T
pewdiepie-archdaemon 2e8413a54a Preserve preview harness, editor, email and task improvements
Snapshot current maintainer-preview application changes and regression fixtures for integration into lab. Excludes local runtime data, evaluation outputs and source backups. Focused Python regression selection: 140 passed; full suite not certified.
2026-10-01 01:34:26 +00:00

65 lines
3.2 KiB
Python

"""Normalize the small theme palette without discarding explicit choices."""
import re
THEME_PRESETS = ('dark', 'light', 'midnight', 'cyberpunk', 'retrowave', 'forest',
'ocean', 'ume', 'terminal', 'organs', 'gpt', 'claude', 'cute',
'eclipse', 'porcelain', 'arcade', 'blueprint', 'monolith', 'yoyo')
BACKGROUND_PATTERNS = ('none', 'dots', 'synapse', 'rain', 'constellations',
'perlin-flow', 'petals', 'sparkles', 'embers',
'starfield-depth', 'ascii-fireflies')
def normalize_theme_background(background, accent):
if background is None:
background = {'pattern': 'none'}
if not isinstance(background, dict):
raise ValueError('background must be an object with a pattern name.')
pattern = background.get('pattern', 'none')
if pattern == 'random':
import random
pattern = random.choice(BACKGROUND_PATTERNS[1:])
if pattern not in BACKGROUND_PATTERNS:
raise ValueError('Unknown background.pattern. Choose: ' + ', '.join(BACKGROUND_PATTERNS) + ', random.')
result = {'bgPattern': pattern, 'bgEffectColor': accent}
for key, low, high in (('intensity', 0, 1), ('size', .2, 3), ('speed', .05, 2.5)):
value = background.get(key, 1)
if isinstance(value, bool) or not isinstance(value, (float, int)) or not low <= value <= high:
raise ValueError(f'background.{key} must be a number between {low} and {high}.')
result['bgEffect' + key.title()] = value
return result
def normalize_theme_colors(colors):
if not isinstance(colors, dict):
raise ValueError('colors must be an object with bg and accent hex colors.')
result = {}
for key, value in colors.items():
if not isinstance(value, str):
raise ValueError(f'colors.{key} must be a hex color, for example #d93025.')
value = value.strip()
if re.fullmatch(r'#?[0-9a-fA-F]{3}|#?[0-9a-fA-F]{6}', value) is None:
raise ValueError(f'colors.{key}={value!r} is invalid. Use #RGB or #RRGGBB.')
value = value.lstrip('#')
if len(value) == 3:
value = ''.join(c * 2 for c in value)
result[key] = '#' + value.lower()
if 'accent' not in result and 'red' in result:
result['accent'] = result.pop('red')
missing = {'bg', 'accent'} - result.keys()
if missing:
raise ValueError('Missing colors: ' + ', '.join(sorted(missing)) + '. Other colors are optional.')
rgb = [int(result['bg'][i:i + 2], 16) / 255 for i in (1, 3, 5)]
linear = [c / 12.92 if c <= .04045 else ((c + .055) / 1.055) ** 2.4 for c in rgb]
luminance = sum(c * w for c, w in zip(linear, (.2126, .7152, .0722)))
light_text = (1.05 / (luminance + .05)) >= ((luminance + .05) / .05)
result.setdefault('fg', '#ffffff' if light_text else '#000000')
# Subtle surfaces move toward the contrasting pole, independent of an
# explicitly chosen foreground that might itself be low contrast.
target = 255 if light_text else 0
def surface(amount):
return '#' + ''.join(f'{round(c * 255 * (1 - amount) + target * amount):02x}' for c in rgb)
result.setdefault('panel', surface(.06))
result.setdefault('border', surface(.20))
return result