tools/palette_probe.py
Rama main · réplica de lectura del repositorio original.
"""How much of a CLI's colour a theme can actually own.
Theming a terminal is not one question, it is four, and which one applies is
decided by the application rather than by us:
default it printed no colour at all — the theme owns it
16 ANSI "the terminal's idea of red" — the theme owns it
greyscale 232-255, "a shade near the background" — relative, may tint
cube 16-231, an exact colour from a fixed grid — absolute, hands off
truecolor 38;2;r;g;b, an exact RGB — absolute, hands off
The ladder is specificity. The more precisely an application named a colour,
the less business anyone has changing it: an app asking for colour 82 wants
*that* green, and one asking for red wants whatever red means here. The
greyscale ramp is the interesting middle — it expresses a relationship to the
background rather than a colour, so honouring it against the theme's own
background is closer to the intent than neutral grey.
This measures which of those a CLI actually uses, so the answer to "will my
theme skin it" is observed rather than assumed. Nothing here is per-vendor
knowledge; it counts escape sequences.
python3 tools/palette_probe.py [cli ...]
"""
from __future__ import annotations
import re
import sys
import time
import uuid
from collections import Counter
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from clique import tmux
from clique.__main__ import config_path
from clique.registry import Registry
SOCKET = "clique-palette"
SGR = re.compile(r"\x1b\[([0-9;]*)m")
def classify(codes: str) -> set[str]:
"""Which colour mechanisms one SGR sequence uses."""
parts = [p for p in codes.split(";") if p != ""]
found: set[str] = set()
i = 0
while i < len(parts):
try:
n = int(parts[i])
except ValueError:
i += 1
continue
if n in (38, 48) and i + 1 < len(parts):
kind = parts[i + 1]
if kind == "2":
found.add("truecolor")
i += 5
continue
if kind == "5" and i + 2 < len(parts):
try:
index = int(parts[i + 2])
except ValueError:
index = -1
if index < 16:
found.add("16 ANSI")
elif index < 232:
found.add("cube")
else:
found.add("greyscale")
i += 3
continue
elif 30 <= n <= 37 or 40 <= n <= 47 or 90 <= n <= 97 or 100 <= n <= 107:
found.add("16 ANSI")
i += 1
return found
def probe(cli_id: str, reg: Registry, seconds: float = 5.0) -> tuple[Counter, int]:
cli = reg.get(cli_id)
where = cli.resolve()
if not where:
raise FileNotFoundError(cli.command)
# A real uuid, not a placeholder: a CLI handed a malformed session id
# exits immediately, and the probe then reports "not probed" for a reason
# that has nothing to do with colour.
argv = reg.launch_argv(cli_id, session_id=str(uuid.uuid4()), name="probe", cwd="/tmp")
mux = f"sm-probe-{cli_id}"
tmux.create(mux, "/tmp", argv, socket=SOCKET, width=100, height=30)
time.sleep(seconds)
text = tmux.capture(mux, SOCKET, lines=200, styled=True)
tmux.kill(mux, SOCKET, force=True)
tally: Counter = Counter()
for codes in SGR.findall(text):
for kind in classify(codes):
tally[kind] += 1
return tally, len(text)
def main() -> int:
reg = Registry(config_path(None))
wanted = sys.argv[1:] or [k for k, v in reg.types().items() if v.resolve()]
tmux._run(["kill-server"], SOCKET, check=False)
tmux.bootstrap(SOCKET, history_limit=2000)
order = ["16 ANSI", "greyscale", "cube", "truecolor"]
print(f"{'cli':10} " + "".join(f"{k:>11}" for k in order) + " theme reaches")
print("-" * 74)
for cli_id in wanted:
try:
tally, _size = probe(cli_id, reg)
except (FileNotFoundError, tmux.TmuxError) as err:
print(f"{cli_id:10} — not probed ({type(err).__name__})")
continue
row = "".join(f"{tally.get(k, 0):>11}" for k in order)
owned = tally.get("16 ANSI", 0) + tally.get("greyscale", 0)
fixed = tally.get("cube", 0) + tally.get("truecolor", 0)
total = owned + fixed
share = "everything" if not fixed else (
f"{round(100 * owned / total)}% of what it paints" if total else "n/a")
if not total:
share = "everything (paints nothing)"
print(f"{cli_id:10} {row} {share}")
tmux._run(["kill-server"], SOCKET, check=False)
return 0
if __name__ == "__main__":
raise SystemExit(main())