import json
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FIELDS_RS = os.path.join(REPO_ROOT, "src", "fields.rs")
CONFIG_DIR = os.path.join(REPO_ROOT, "benches", "fastfetch")
MODES = (("short", "Short"), ("default", "Standard"), ("long", "Long"), ("full", "Full"))
MODE_RANK = {variant: i for i, (_, variant) in enumerate(MODES)}
MAPPING = {
"host": ["title"],
"os": ["os"],
"kernel": ["kernel"],
"domain": "no fastfetch module reports the DNS domain",
"domain-search": "no fastfetch module reports DNS search domains",
"chassis": ["chassis"],
"init": ["initsystem"],
"locale": ["locale"],
"arch": "fastfetch folds the architecture into its OS line",
"uptime": ["uptime"],
"users": ["users"],
"packages": ["packages"],
"cpu": ["cpu"],
"cpu-freq": "fastfetch folds the frequency into its CPU line",
"cpu-cache": ["cpucache"],
"cpu-usage": ["cpuusage"],
"motherboard": ["board"],
"bios": ["bios"],
"bootmgr": ["bootmgr"],
"tpm": ["tpm"],
"gpu": ["gpu"],
"display": ["display"],
"vulkan": ["vulkan"],
"opengl": ["opengl"],
"opencl": ["opencl"],
"brightness": ["brightness"],
"audio": ["sound"],
"camera": ["camera"],
"gamepad": ["gamepad"],
"keyboard": ["keyboard"],
"mouse": ["mouse"],
"wifi": ["wifi"],
"bluetooth": ["bluetooth", "bluetoothradio"],
"battery": ["battery"],
"power-adapter": ["poweradapter"],
"memory": ["memory"],
"phys-mem": ["physicalmemory"],
"swap": ["swap"],
"procs": ["processes"],
"load": ["loadavg"],
"disk": ["disk"],
"phys-disk": ["physicaldisk"],
"disk-io": ["diskio"],
"btrfs": ["btrfs"],
"zpool": ["zpool"],
"temp": "an option on the sensor-owning modules, not a module (see TEMP_MODULES)",
"net": ["localip"],
"net-io": ["netio"],
"public-ip": ["publicip"],
"dns": ["dns"],
"shell": ["shell"],
"editor": ["editor"],
"terminal": ["terminal"],
"terminal-font": ["terminalfont"],
"terminal-size": ["terminalsize"],
"desktop": ["de"],
"wm": ["wm"],
"login-manager": ["lm"],
"player": ["player"],
"media": ["media"],
"wm-theme": ["wmtheme"],
"wallpaper": ["wallpaper"],
"terminal-theme": ["terminaltheme"],
"theme": ["theme"],
"icons": ["icons"],
"cursor": ["cursor"],
"font": ["font"],
"weather": ["weather"],
}
TEMP_MODULES = ("cpu", "gpu", "physicaldisk", "battery")
MODULE_OPTIONS = {
"publicip": {"timeout": 2000},
"weather": {"timeout": 4000},
}
HEADER = """\
// GENERATED by scripts/fastfetch_configs.py from src/fields.rs -- do not edit by hand.
// Regenerate with: just fastfetch-configs
//
// What fastfetch is asked for when benchmarked against `retch{flag}`: the same information,
// field for field. Fields with no fastfetch equivalent are listed here rather than faked:
{unmatched}
"""
def parse_fields(path=FIELDS_RS):
with open(path, encoding="utf-8") as f:
text = f.read()
pairs = re.findall(r'key:\s*"([^"]+)",\s*min_mode:\s*Mode::(\w+)', text)
if not pairs:
raise ValueError(f"no FieldDef entries found in {path}")
return pairs
def fields_for(fields, mode_variant):
rank = MODE_RANK[mode_variant]
return [key for key, variant in fields if MODE_RANK[variant] <= rank]
def problems_with_mapping(fields):
out = []
keys = [k for k, _ in fields]
for key, variant in fields:
if variant not in MODE_RANK:
out.append(f"{key}: unknown mode {variant!r} in src/fields.rs")
for key in keys:
if key not in MAPPING:
out.append(f"{key}: in src/fields.rs but has no entry in MAPPING -- say which "
f"fastfetch module matches it, or why none does")
for key in MAPPING:
if key not in keys:
out.append(f"{key}: in MAPPING but no longer a field in src/fields.rs")
return out
def build_config(fields, mode, variant):
visible = set(fields_for(fields, variant))
want_temp = "temp" in visible
modules, unmatched = [], []
for key, spec in MAPPING.items():
if key not in visible or key == "temp":
continue
if isinstance(spec, str):
unmatched.append((key, spec))
continue
for name in spec:
opts = dict(MODULE_OPTIONS.get(name, {}))
if want_temp and name in TEMP_MODULES:
opts["temp"] = True
modules.append({"type": name, **opts} if opts else name)
if want_temp and not any(
(m if isinstance(m, str) else m["type"]) in TEMP_MODULES for m in modules
):
unmatched.append(("temp", "no sensor-owning module is in this mode"))
config = {"logo": {"type": "none"}, "modules": modules}
return config, unmatched
def render(fields, mode, variant):
config, unmatched = build_config(fields, mode, variant)
flag = "" if mode == "default" else f" --{mode}"
lines = [f"// {k}: {why}" for k, why in unmatched] or ["// (none)"]
return HEADER.format(flag=flag, unmatched="\n".join(lines)) + json.dumps(config, indent=2) + "\n"
def config_path(mode, config_dir=CONFIG_DIR):
return os.path.join(config_dir, f"{mode}.jsonc")
def check(fields, config_dir=CONFIG_DIR):
out = problems_with_mapping(fields)
if out:
return out
for mode, variant in MODES:
path = config_path(mode, config_dir)
want = render(fields, mode, variant)
try:
with open(path, encoding="utf-8", newline="") as f:
have = f.read()
except FileNotFoundError:
out.append(f"{os.path.relpath(path, REPO_ROOT)} is missing")
continue
if have != want:
out.append(f"{os.path.relpath(path, REPO_ROOT)} is stale")
return out
def write(fields, config_dir=CONFIG_DIR):
problems = problems_with_mapping(fields)
if problems:
return problems
os.makedirs(config_dir, exist_ok=True)
for mode, variant in MODES:
with open(config_path(mode, config_dir), "w", encoding="utf-8", newline="\n") as f:
f.write(render(fields, mode, variant))
return []
def _self_test():
import tempfile
failures = []
def expect(name, cond, detail=""):
if not cond:
failures.append(f"{name}: {detail}")
fields = parse_fields()
expect("fields.rs parses to a plausible table", len(fields) > 50, f"{len(fields)} fields")
expect("committed configs are current", check(fields) == [], str(check(fields)))
cfg, unmatched = build_config(fields, "short", "Short")
expect("short modules", cfg["modules"] == [
"title", "os", "kernel", "cpu", "gpu", "memory", "disk", "localip"],
str(cfg["modules"]))
expect("short has nothing unmatched", unmatched == [], str(unmatched))
default_types = {m if isinstance(m, str) else m["type"]
for m in build_config(fields, "default", "Standard")[0]["modules"]}
long_cfg, _ = build_config(fields, "long", "Long")
long_types = {m if isinstance(m, str) else m["type"] for m in long_cfg["modules"]}
expect("default does not sample cpu usage", "cpuusage" not in default_types)
expect("long samples cpu usage", "cpuusage" in long_types)
prev = set()
for mode, variant in MODES:
types = {m if isinstance(m, str) else m["type"]
for m in build_config(fields, mode, variant)[0]["modules"]}
expect(f"{mode} is a superset", prev <= types, str(prev - types))
prev = types
def opts(cfg, name):
return next((m for m in cfg["modules"] if isinstance(m, dict) and m["type"] == name), {})
expect("long asks cpu for its temp", opts(long_cfg, "cpu").get("temp") is True)
expect("default does not ask for temps",
all(isinstance(m, str) for m in build_config(fields, "default", "Standard")[0]["modules"]
if (m if isinstance(m, str) else m["type"]) in TEMP_MODULES))
expect("public ip timeout matches retch", opts(long_cfg, "publicip").get("timeout") == 2000)
unmapped = fields + [("brand-new-field", "Standard")]
expect("an unmapped field is caught",
any("brand-new-field" in p for p in check(unmapped)), "check passed")
expect("a mapping for a deleted field is caught",
any("cpu-usage" in p for p in check([f for f in fields if f[0] != "cpu-usage"])),
"check passed")
moved = [(k, "Standard" if k == "cpu-usage" else v) for k, v in fields]
expect("moving a field between modes makes the configs stale",
any("default.jsonc is stale" in p for p in check(moved)), str(check(moved)))
with tempfile.TemporaryDirectory() as tmp:
write(fields, tmp)
expect("write then check is clean", check(fields, tmp) == [], str(check(fields, tmp)))
with open(config_path("long", tmp), "a", encoding="utf-8") as f:
f.write("// hand edit\n")
expect("a hand edit is caught", any("long.jsonc is stale" in p for p in check(fields, tmp)))
os.remove(config_path("full", tmp))
expect("a missing config is caught", any("full.jsonc is missing" in p for p in check(fields, tmp)))
if failures:
for f in failures:
print(f" FAIL {f}", file=sys.stderr)
print(f"fastfetch_configs.py self-test FAILED ({len(failures)})", file=sys.stderr)
return 1
print("fastfetch_configs.py self-test passed")
return 0
def main(argv):
if argv == ["--self-test"]:
return _self_test()
fields = parse_fields()
if argv == ["--write"]:
problems = write(fields)
verb = "wrote"
elif argv == []:
problems = check(fields)
verb = "checked"
else:
print(__doc__, file=sys.stderr)
return 2
if problems:
for p in problems:
print(f"fastfetch-configs: {p}", file=sys.stderr)
if verb == "checked":
print("fastfetch-configs: run `just fastfetch-configs` to regenerate", file=sys.stderr)
return 1
print(f"fastfetch-configs: {verb} {len(MODES)} configs in benches/fastfetch/")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))