nandi/frqpublic Fork 0
23b79998fbb128adef99c3a4909ba4f6057d9b55
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

cosmic2cljd.py · 132 lines · 5.2 KBPython Blame HistoryRaw
Paint the phone in COSMIC's own theme, read from cosmic-config ba4e71b nandi 7d ago1#!/usr/bin/env python3
2"""Read the COSMIC theme out of cosmic-config and write it as ClojureDart.
3
4libcosmic asks cosmic-config for the user's theme at run time, so `just run`
5already paints frq in whatever accent and surfaces are set in COSMIC Settings.
6A phone has no cosmic-config, so the APK cannot ask — the values are read here
7instead, on the machine that has them, and compiled in.
8
9That is a real difference and worth naming: the desktop follows the theme as
10it changes, and the APK carries the theme as it was when the APK was built.
11`just theme` moves it.
12
13The 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
15RON parser to read colours out of a flat file is a dependency nobody needs.
16"""
17
18import pathlib
19import re
20import sys
21
22CONFIG = 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.
26COLOURS = [
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
43def 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
51def 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
66def 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
75def 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
80def 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 "",
97 " libcosmic asks cosmic-config for these at run time, so `just run`",
98 " follows COSMIC Settings as it changes. A phone has no cosmic-config,",
99 " so the APK carries them instead. That is the one real difference",
100 f' between the two, and it is why this file is in git."',
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
131if __name__ == "__main__":
132 main()