#!/usr/bin/env python3 """ Simple Helix Theme Tester - Generate Helix theme from image for testing. Usage: ./helix_theme_tester.py test image.jpg ./helix_theme_tester.py test image.jpg --light ./helix_theme_tester.py test image.jpg --output custom_name.toml """ import argparse import subprocess import sys from pathlib import Path import json # Import from existing color_mapper from color_mapper import create_semantic_mapping, apply_semantic_mapping def generate_palette_from_image(image_path: Path, is_light: bool = False) -> bool: """Run pywal on image to generate colors.json""" cmd = ['wal', '-i', str(image_path), '--backend', 'haishoku', '-n'] if is_light: cmd.append('-l') try: subprocess.run(cmd, check=True, timeout=30) print(f"✓ Generated palette from {image_path.name}") return True except subprocess.CalledProcessError as e: print(f"✗ Failed to generate palette: {e}") return False except FileNotFoundError: print("✗ Error: 'wal' command not found. Is pywal16 installed?") return False def generate_helix_theme(output_name: str = "pywal_test") -> bool: """Generate Helix theme using color_mapper logic""" colors_json = Path.home() / ".cache/wal/colors.json" if not colors_json.exists(): print(f"✗ colors.json not found at {colors_json}") return False # Use existing color_mapper to create semantic mapping print("✓ Analyzing colors and creating semantic mapping...") mapping = create_semantic_mapping(colors_json) # Print the mapping for visibility print("\nSemantic color mapping:") for role, color in mapping["semantic_mapping"].items(): props = mapping["color_analysis"][color] print(f" {role:12} -> {color} ({props['category']:8} {props['hex']})") # Check for contrast issues print("\nContrast analysis:") bg_lightness = mapping.get("color_analysis", {}).get("color0", {}).get("lightness", 50) for role, color_key in mapping["semantic_mapping"].items(): color_props = mapping["color_analysis"][color_key] contrast = abs(color_props["lightness"] - bg_lightness) status = "✓" if contrast > 30 else "⚠" print(f" {status} {role:12}: contrast {contrast:.1f}") # Apply to helix template helix_template = Path(__file__).parent / "templates/helix_semantic.toml" helix_output = Path.home() / f".config/helix/themes/{output_name}.toml" helix_output.parent.mkdir(parents=True, exist_ok=True) apply_semantic_mapping(helix_template, helix_output, mapping) print(f"\n✓ Helix theme created: {helix_output}") print(f"\nTest in Helix:") print(f" hx some_file.py") print(f" :theme {output_name}") return True def main(): parser = argparse.ArgumentParser( description='Generate Helix theme from image for testing' ) parser.add_argument('command', choices=['test'], help='Command to run') parser.add_argument('image', type=Path, help='Path to image file') parser.add_argument('--light', action='store_true', help='Generate light theme') parser.add_argument('--output', default='pywal_test', help='Output theme name') args = parser.parse_args() if not args.image.exists(): print(f"✗ Image not found: {args.image}") return 1 print(f"Testing with image: {args.image}") print(f"Mode: {'light' if args.light else 'dark'}\n") # Step 1: Generate palette if not generate_palette_from_image(args.image, args.light): return 1 # Step 2: Generate Helix theme if not generate_helix_theme(args.output): return 1 print("\n✓ Done! Now test the theme in Helix and iterate.") return 0 if __name__ == '__main__': sys.exit(main()) Enhanced color_mapper.py with Tweaking Let me show you what minimal enhancements to add for tweaking: # Add to color_mapper.py after existing imports def adjust_color_lightness(hex_color: str, factor: float) -> str: """ Adjust color lightness by factor. factor > 1.0 = lighter, factor < 1.0 = darker """ rgb = hex_to_rgb(hex_color) h, s, l = rgb_to_hsl(rgb) # Adjust lightness l = max(0, min(100, l * factor)) # Convert back to RGB then hex r, g, b = colorsys.hls_to_rgb(h/360, l/100, s/100) return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" def adjust_color_saturation(hex_color: str, factor: float) -> str: """ Adjust color saturation by factor. factor > 1.0 = more saturated, factor < 1.0 = less saturated """ rgb = hex_to_rgb(hex_color) h, s, l = rgb_to_hsl(rgb) # Adjust saturation s = max(0, min(100, s * factor)) # Convert back r, g, b = colorsys.hls_to_rgb(h/360, l/100, s/100) return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" def calculate_contrast_ratio(color1_hex: str, color2_hex: str) -> float: """Calculate WCAG contrast ratio between two colors""" def relative_luminance(hex_color): rgb = hex_to_rgb(hex_color) r, g, b = [x / 255.0 for x in rgb] # Apply gamma correction r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4 g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4 b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4 return 0.2126 * r + 0.7152 * g + 0.0722 * b l1 = relative_luminance(color1_hex) l2 = relative_luminance(color2_hex) lighter = max(l1, l2) darker = min(l1, l2) return (lighter + 0.05) / (darker + 0.05) def enforce_minimum_contrast(fg_hex: str, bg_hex: str, min_ratio: float = 4.5) -> str: """ Adjust foreground color to meet minimum contrast ratio with background. Preserves hue and saturation, only adjusts lightness. """ current_ratio = calculate_contrast_ratio(fg_hex, bg_hex) if current_ratio >= min_ratio: return fg_hex # Already meets requirement # Determine if we need to lighten or darken bg_rgb = hex_to_rgb(bg_hex) bg_h, bg_s, bg_l = rgb_to_hsl(bg_rgb) fg_rgb = hex_to_rgb(fg_hex) fg_h, fg_s, fg_l = rgb_to_hsl(fg_rgb) # If background is dark, lighten foreground; if light, darken foreground step = 5 if bg_l < 50 else -5 # Iteratively adjust lightness attempts = 0 while attempts < 20: # Prevent infinite loop fg_l += step fg_l = max(0, min(100, fg_l)) # Convert back to hex r, g, b = colorsys.hls_to_rgb(fg_h/360, fg_l/100, fg_s/100) adjusted_hex = f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" if calculate_contrast_ratio(adjusted_hex, bg_hex) >= min_ratio: return adjusted_hex attempts += 1 # If we couldn't meet contrast, return best effort return adjusted_hex # Add parameters to apply_semantic_mapping def apply_semantic_mapping(template_path: Path, output_path: Path, mapping: Dict, adjustments: Dict = None): """ Apply semantic mapping to Helix template. adjustments: Optional dict with tweaking parameters: { 'comment': {'contrast': 4.5, 'saturation': 1.2}, 'error': {'saturation': 1.3, 'force_hue': (0, 30)}, } """ with open(template_path) as f: template = f.read() # Read pywal colors colors_json = Path.home() / ".cache/wal/colors.json" with open(colors_json) as f: colors_data = json.load(f) # Get background for contrast calculations bg_color = colors_data["special"]["background"] # Replace standard pywal colors first output = template for key, value in colors_data["special"].items(): output = output.replace(f"{{{key}}}", value) for key, value in colors_data["colors"].items(): output = output.replace(f"{{{key}}}", value) # Now apply semantic colors with optional adjustments semantic = mapping["semantic_mapping"] for role in ["error", "warning", "comment", "string", "keyword", "number", "info"]: color_key = semantic[role] color_hex = colors_data["colors"][color_key] # Apply adjustments if provided if adjustments and role in adjustments: adj = adjustments[role] # Apply saturation adjustment if 'saturation' in adj: color_hex = adjust_color_saturation(color_hex, adj['saturation']) # Apply lightness adjustment if 'lightness' in adj: color_hex = adjust_color_lightness(color_hex, adj['lightness']) # Enforce minimum contrast if 'contrast' in adj: color_hex = enforce_minimum_contrast(color_hex, bg_color, adj['contrast']) # Replace in template placeholder = f"{{{role.upper()}_COLOR}}" output = output.replace(placeholder, color_hex) with open(output_path, 'w') as f: f.write(output)