#!/usr/bin/env python3 """Read the COSMIC theme out of cosmic-config and write it as ClojureDart. libcosmic asks cosmic-config for the user's theme at run time, so `just run` already paints frq in whatever accent and surfaces are set in COSMIC Settings. A phone has no cosmic-config, so the APK cannot ask — the values are read here instead, on the machine that has them, and compiled in. That is a real difference and worth naming: the desktop follows the theme as it changes, and the APK carries the theme as it was when the APK was built. `just theme` moves it. The config is RON. Not parsed as RON: every value this needs is either a `key: "#RRGGBBAA"` line or a `key: N` line inside a named block, and a general RON parser to read colours out of a flat file is a dependency nobody needs. """ import pathlib import re import sys CONFIG = pathlib.Path.home() / ".config" / "cosmic" # What frq asks of a theme, as (token, file, field). `component` fields are one # block deep — see the `component: (` in background/primary/secondary. COLOURS = [ ("accent", "accent", "base"), ("on-accent", "accent", "on"), ("bg", "background", "base"), ("on-bg", "background", "component.on"), ("component", "background", "component.base"), ("component-hover", "background", "component.hover"), ("divider", "background", "component.divider"), ("card", "primary", "base"), ("card-component", "primary", "component.base"), ("on-card", "primary", "component.on"), ("destructive", "destructive", "base"), ("on-destructive", "destructive", "on"), ("success", "success", "base"), ] def read(mode: str, name: str) -> str: for version in ("v2", "v1"): p = CONFIG / f"com.system76.CosmicTheme.{mode}" / version / name if p.exists(): return p.read_text() raise SystemExit(f"cosmic2cljd: no {name} under {CONFIG} — is COSMIC installed?") def field(text: str, path: str) -> str: """`base`, or `component.base` for the one inside the component block.""" if "." in path: outer, inner = path.split(".", 1) m = re.search(rf"\b{outer}:\s*\(", text) if not m: raise SystemExit(f"cosmic2cljd: no {outer} block") text = text[m.end():] path = inner m = re.search(rf'^\s*{path}:\s*"(#[0-9A-Fa-f]{{6,8}})"', text, re.M) if not m: raise SystemExit(f"cosmic2cljd: no {path}") return m.group(1) def argb(hex_rgba: str) -> str: """#RRGGBBAA (COSMIC) to 0xAARRGGBB (Flutter's Color).""" h = hex_rgba.lstrip("#") if len(h) == 6: h += "FF" r, g, b, a = h[0:2], h[2:4], h[4:6], h[6:8] return f"0x{a}{r}{g}{b}".upper().replace("0X", "0x") def number(text: str, key: str, default: float) -> float: m = re.search(rf"^\s*{key}:\s*\(?\s*([0-9.]+)", text, re.M) return float(m.group(1)) if m else default def main() -> None: out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "flutter/src/frq/theme/cosmic.cljd") dark = (CONFIG / "com.system76.CosmicTheme.Mode" / "v1" / "is_dark") is_dark = dark.exists() and dark.read_text().strip() == "true" mode = "Dark" if is_dark else "Light" radii = read(mode, "corner_radii") spacing = read(mode, "spacing") files = {n: read(mode, n) for n in {f for _, f, _ in COLOURS}} lines = [ "(ns frq.theme.cosmic", ' "The COSMIC theme, as it was on the machine that built this.', "", " GENERATED by tools/cosmic2cljd.py — `just theme`. Do not edit.", "", " libcosmic asks cosmic-config for these at run time, so `just run`", " follows COSMIC Settings as it changes. A phone has no cosmic-config,", " so the APK carries them instead. That is the one real difference", f' between the two, and it is why this file is in git."', ' (:require ["package:flutter/material.dart" :as m]))', "", f";; {mode} theme, from ~/.config/cosmic.", f"(def dark? {str(is_dark).lower()})", "", ] for token, fname, path in COLOURS: val = field(files[fname], path) lines.append(f";; {fname}.{path} = {val}") lines.append(f"(def {token} (m/Color. {argb(val)}))") lines.append("") lines.append(";; corner_radii") for token, key, default in [("radius-xs", "radius_xs", 2.0), ("radius-s", "radius_s", 8.0), ("radius-m", "radius_m", 8.0)]: lines.append(f"(def {token} {number(radii, key, default)})") lines.append("") lines.append(";; spacing") for token, key, default in [("space-xxxs", "space_xxxs", 4.0), ("space-xxs", "space_xxs", 8.0), ("space-xs", "space_xs", 12.0), ("space-s", "space_s", 16.0), ("space-m", "space_m", 24.0)]: lines.append(f"(def {token} {number(spacing, key, default)})") out.parent.mkdir(parents=True, exist_ok=True) out.write_text("\n".join(lines) + "\n") print(f"wrote {out} ({mode}, accent {field(files['accent'], 'base')})") if __name__ == "__main__": main()