| Paint the phone in COSMIC's own theme, read from cosmic-config ba4e71b nandi 7d ago | 1 | #!/usr/bin/env python3 |
| 2 | """Read the COSMIC theme out of cosmic-config and write it as ClojureDart. |
| 3 | |
| Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 7d ago | 4 | libcosmic asks cosmic-config for the user's theme at run time, so |
| 5 | `just cosmic run` already paints frq in whatever accent and surfaces are set in COSMIC Settings. |
| Paint the phone in COSMIC's own theme, read from cosmic-config ba4e71b nandi 7d ago | 6 | A phone has no cosmic-config, so the APK cannot ask — the values are read here |
| 7 | instead, on the machine that has them, and compiled in. |
| 8 | |
| 9 | That is a real difference and worth naming: the desktop follows the theme as |
| 10 | it changes, and the APK carries the theme as it was when the APK was built. |
| 11 | `just theme` moves it. |
| 12 | |
| 13 | The config is RON. Not parsed as RON: every value this needs is either a |
| 14 | `key: "#RRGGBBAA"` line or a `key: N` line inside a named block, and a general |
| 15 | RON parser to read colours out of a flat file is a dependency nobody needs. |
| 16 | """ |
| 17 | |
| 18 | import pathlib |
| 19 | import re |
| 20 | import sys |
| 21 | |
| 22 | CONFIG = pathlib.Path.home() / ".config" / "cosmic" |
| 23 | |
| 24 | # What frq asks of a theme, as (token, file, field). `component` fields are one |
| 25 | # block deep — see the `component: (` in background/primary/secondary. |
| 26 | COLOURS = [ |
| 27 | ("accent", "accent", "base"), |
| 28 | ("on-accent", "accent", "on"), |
| 29 | ("bg", "background", "base"), |
| 30 | ("on-bg", "background", "component.on"), |
| 31 | ("component", "background", "component.base"), |
| 32 | ("component-hover", "background", "component.hover"), |
| 33 | ("divider", "background", "component.divider"), |
| 34 | ("card", "primary", "base"), |
| 35 | ("card-component", "primary", "component.base"), |
| 36 | ("on-card", "primary", "component.on"), |
| 37 | ("destructive", "destructive", "base"), |
| 38 | ("on-destructive", "destructive", "on"), |
| 39 | ("success", "success", "base"), |
| 40 | ] |
| 41 | |
| 42 | |
| 43 | def read(mode: str, name: str) -> str: |
| 44 | for version in ("v2", "v1"): |
| 45 | p = CONFIG / f"com.system76.CosmicTheme.{mode}" / version / name |
| 46 | if p.exists(): |
| 47 | return p.read_text() |
| 48 | raise SystemExit(f"cosmic2cljd: no {name} under {CONFIG} — is COSMIC installed?") |
| 49 | |
| 50 | |
| 51 | def field(text: str, path: str) -> str: |
| 52 | """`base`, or `component.base` for the one inside the component block.""" |
| 53 | if "." in path: |
| 54 | outer, inner = path.split(".", 1) |
| 55 | m = re.search(rf"\b{outer}:\s*\(", text) |
| 56 | if not m: |
| 57 | raise SystemExit(f"cosmic2cljd: no {outer} block") |
| 58 | text = text[m.end():] |
| 59 | path = inner |
| 60 | m = re.search(rf'^\s*{path}:\s*"(#[0-9A-Fa-f]{{6,8}})"', text, re.M) |
| 61 | if not m: |
| 62 | raise SystemExit(f"cosmic2cljd: no {path}") |
| 63 | return m.group(1) |
| 64 | |
| 65 | |
| 66 | def argb(hex_rgba: str) -> str: |
| 67 | """#RRGGBBAA (COSMIC) to 0xAARRGGBB (Flutter's Color).""" |
| 68 | h = hex_rgba.lstrip("#") |
| 69 | if len(h) == 6: |
| 70 | h += "FF" |
| 71 | r, g, b, a = h[0:2], h[2:4], h[4:6], h[6:8] |
| 72 | return f"0x{a}{r}{g}{b}".upper().replace("0X", "0x") |
| 73 | |
| 74 | |
| 75 | def number(text: str, key: str, default: float) -> float: |
| 76 | m = re.search(rf"^\s*{key}:\s*\(?\s*([0-9.]+)", text, re.M) |
| 77 | return float(m.group(1)) if m else default |
| 78 | |
| 79 | |
| 80 | def main() -> None: |
| 81 | out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 |
| 82 | else "flutter/src/frq/theme/cosmic.cljd") |
| 83 | dark = (CONFIG / "com.system76.CosmicTheme.Mode" / "v1" / "is_dark") |
| 84 | is_dark = dark.exists() and dark.read_text().strip() == "true" |
| 85 | mode = "Dark" if is_dark else "Light" |
| 86 | |
| 87 | radii = read(mode, "corner_radii") |
| 88 | spacing = read(mode, "spacing") |
| 89 | files = {n: read(mode, n) for n in {f for _, f, _ in COLOURS}} |
| 90 | |
| 91 | lines = [ |
| 92 | "(ns frq.theme.cosmic", |
| 93 | ' "The COSMIC theme, as it was on the machine that built this.', |
| 94 | "", |
| 95 | " GENERATED by tools/cosmic2cljd.py — `just theme`. Do not edit.", |
| 96 | "", |
| Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 7d ago | 97 | " libcosmic asks cosmic-config for these at run time, so `just cosmic run`", |
| 98 | " follows COSMIC Settings as it changes. A phone has no cosmic-config, so", |
| 99 | " the APK carries them instead. That is the one real difference between", |
| 100 | f' the two, and it is why this file is in git."', |
| Paint the phone in COSMIC's own theme, read from cosmic-config ba4e71b nandi 7d ago | 101 | ' (:require ["package:flutter/material.dart" :as m]))', |
| 102 | "", |
| 103 | f";; {mode} theme, from ~/.config/cosmic.", |
| 104 | f"(def dark? {str(is_dark).lower()})", |
| 105 | "", |
| 106 | ] |
| 107 | for token, fname, path in COLOURS: |
| 108 | val = field(files[fname], path) |
| 109 | lines.append(f";; {fname}.{path} = {val}") |
| 110 | lines.append(f"(def {token} (m/Color. {argb(val)}))") |
| 111 | lines.append("") |
| 112 | lines.append(";; corner_radii") |
| 113 | for token, key, default in [("radius-xs", "radius_xs", 2.0), |
| 114 | ("radius-s", "radius_s", 8.0), |
| 115 | ("radius-m", "radius_m", 8.0)]: |
| 116 | lines.append(f"(def {token} {number(radii, key, default)})") |
| 117 | lines.append("") |
| 118 | lines.append(";; spacing") |
| 119 | for token, key, default in [("space-xxxs", "space_xxxs", 4.0), |
| 120 | ("space-xxs", "space_xxs", 8.0), |
| 121 | ("space-xs", "space_xs", 12.0), |
| 122 | ("space-s", "space_s", 16.0), |
| 123 | ("space-m", "space_m", 24.0)]: |
| 124 | lines.append(f"(def {token} {number(spacing, key, default)})") |
| 125 | |
| 126 | out.parent.mkdir(parents=True, exist_ok=True) |
| 127 | out.write_text("\n".join(lines) + "\n") |
| 128 | print(f"wrote {out} ({mode}, accent {field(files['accent'], 'base')})") |
| 129 | |
| 130 | |
| 131 | if __name__ == "__main__": |
| 132 | main() |