#!/usr/bin/env python3 """Reads the active theme's colors.json and generates all themed config files.""" import json, os scheme = os.path.expanduser('~/.active_theme/colors.json') cache = os.path.expanduser('~/.cache/wal') os.makedirs(cache, exist_ok=True) with open(scheme) as f: c = json.load(f) bg = c['special']['background'] fg = c['special']['foreground'] pink = c['colors']['color1'] green = c['colors']['color2'] warn = c['colors']['color3'] muted = c['colors']['color8'] dark = c['colors']['color0'] def rgba(h, a): h = h.lstrip('#') r, g, b = int(h[0:2],16), int(h[2:4],16), int(h[4:6],16) return f"rgba({r},{g},{b},{a})" # ── Waybar ──────────────────────────────────────────────────────────────────── waybar = f"""/* Generated — edit ~/.active_theme/colors.json */ * {{ font-family: "MesloLGS Nerd Font Mono", "Font Awesome 7 Free", monospace; font-size: 11px; font-weight: normal; min-height: 0; border: none; border-radius: 0; }} window#waybar {{ background-color: {rgba(bg, 0.95)}; border-bottom: 1px solid {rgba(pink, 0.4)}; color: {green}; }} window#waybar.bottom {{ border-bottom: none; border-top: 1px solid {rgba(pink, 0.4)}; font-size: 11px; }} #custom-notification {{ padding: 1px 8px; color: {green}; font-style: italic; }} #custom-notification.critical {{ color: {pink}; font-style: italic; }} #disk, #network.speed {{ padding: 1px 8px; margin: 3px 2px; color: {green}; }} #workspaces {{ margin: 2px 4px; padding: 0 2px; }} #workspaces button {{ padding: 2px 10px; background: transparent; color: {muted}; border-radius: 10px; margin: 2px; transition: all 0.2s ease; }} #workspaces button:hover {{ color: {pink}; }} #workspaces button.active {{ color: {pink}; font-weight: bold; border-bottom: 1px solid {pink}; }} #workspaces button.urgent {{ color: {pink}; animation: blink 0.6s steps(1) infinite; }} #workspaces button .windows-icon {{ font-size: 9px; color: {muted}; margin-left: 2px; }} #window {{ color: {muted}; padding: 0 10px; font-style: italic; }} #clock {{ color: {pink}; font-weight: bold; padding: 1px 12px; }} #cpu, #memory, #temperature, #battery, #network, #pulseaudio, #tray {{ padding: 1px 8px; margin: 2px 1px; color: {green}; transition: all 0.2s ease; }} #battery.warning {{ color: {warn}; }} #battery.critical {{ color: {pink}; animation: blink 1s steps(1) infinite; }} #cpu:hover, #memory:hover, #network:hover, #pulseaudio:hover {{ color: {pink}; }} #temperature.critical {{ color: {pink}; }} #network.disconnected {{ color: {pink}; }} #pulseaudio.muted {{ color: {muted}; }} #tray > .passive {{ -gtk-icon-effect: dim; }} #tray > .needs-attention {{ -gtk-icon-effect: highlight; }} @keyframes blink {{ to {{ color: {muted}; }} }} """ # ── Wofi ────────────────────────────────────────────────────────────────────── wofi = f"""/* Generated — edit ~/.active_theme/colors.json */ window {{ background-color: {bg}; border: 1px solid {rgba(pink, 0.5)}; border-radius: 12px; font-family: "MesloLGS Nerd Font Mono", monospace; font-size: 13px; color: {green}; }} #input {{ background-color: {dark}; color: {green}; border: 1px solid {rgba(pink, 0.4)}; border-radius: 8px; padding: 8px 12px; margin: 8px; outline: none; caret-color: {pink}; }} #input:focus {{ border-color: {pink}; }} #inner-box {{ background-color: transparent; margin: 0 8px 8px 8px; }} #outer-box {{ background-color: transparent; padding: 4px; }} #scroll {{ background-color: transparent; }} #text {{ color: {green}; padding: 2px 8px; }} #entry {{ background-color: transparent; border-radius: 8px; padding: 6px 8px; margin: 2px 0; }} #entry:selected {{ background-color: {rgba(pink, 0.2)}; border: 1px solid {rgba(pink, 0.5)}; }} #entry:selected #text {{ color: {pink}; }} """ # ── Hyprland colors ─────────────────────────────────────────────────────────── def strip(h): return h.lstrip('#') hypr = f"""# Generated — edit ~/.active_theme/colors.json $wal_pink = rgba({strip(pink)}ee) $wal_pink_b = rgba({strip(pink)}66) $wal_green = rgba({strip(green)}ee) $wal_muted = rgba({strip(muted)}aa) $wal_shadow = rgba({strip(bg)}ee) """ # ── Hyprland keyword apply (no reload needed) ───────────────────────────────── def a(h, s): return f"rgba({strip(h)}{s})" hypr_apply = f"""#!/bin/bash hyprctl keyword general:col.active_border "{a(pink,'ee')} {a(green,'ee')} 45deg" hyprctl keyword general:col.inactive_border "{a(muted,'aa')}" hyprctl keyword decoration:shadow:color "{a(bg,'ee')}" """ files = { 'waybar-style.css': waybar, 'wofi-style.css': wofi, 'colors-hypr.conf': hypr, 'hypr-apply.sh': hypr_apply, } for name, content in files.items(): with open(os.path.join(cache, name), 'w') as f: f.write(content) # Write wallpaper path for reload.sh (expand ~ so reload.sh gets an absolute path) wallpaper = os.path.expanduser(c.get('wallpaper', 'None')) with open(os.path.join(cache, 'wallpaper'), 'w') as f: f.write(wallpaper) # Update hyprpaper.conf to preload all theme wallpapers for the active profile active_profile = os.path.expanduser('~/.active_profile') themes_dir = os.path.join(active_profile, 'themes') all_wallpapers = set() if os.path.isdir(themes_dir): for theme_name in sorted(os.listdir(themes_dir)): theme_json = os.path.join(themes_dir, theme_name, 'colors.json') if not os.path.isfile(theme_json): continue try: with open(theme_json) as f: j = json.load(f) wp = os.path.expanduser(j.get('wallpaper', 'None')) if wp and wp != 'None' and os.path.isfile(wp): all_wallpapers.add(wp) except Exception: pass if all_wallpapers: initial = wallpaper if wallpaper in all_wallpapers else next(iter(sorted(all_wallpapers))) hyprpaper_conf = ''.join(f'preload = {wp}\n' for wp in sorted(all_wallpapers)) hyprpaper_conf += f'\nwallpaper = ,{initial}\n' with open(os.path.expanduser('~/.config/hypr/hyprpaper.conf'), 'w') as f: f.write(hyprpaper_conf) print("Generated: " + ", ".join(files.keys()))