nandi/frqpublic Fork 0
3ee0b5b70ed2e58332972e6dcb111aeb87dd036d
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.

Paint the phone in COSMIC's own theme, read from cosmic-config ba4e71b · on 3ee0b5b70ed2e58332972e6dcb111aeb87dd036d · nandi · 7d ago
cosmic2cljd.py · 132 lines · 5.2 KBPython Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/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()