#!/usr/bin/env python3 """Report installed cursor and icon themes from the standard XDG roots. This helper is intentionally read-only and argument-free. Theme paths come only from XDG_DATA_HOME and XDG_DATA_DIRS; directory symlinks are not followed. """ from __future__ import annotations import json import os import stat import sys from pathlib import Path def icon_roots() -> list[Path]: home = Path(os.environ.get("HOME") or "/nonexistent") data_home = Path(os.environ.get("XDG_DATA_HOME") or home / ".local/share") data_dirs = os.environ.get("XDG_DATA_DIRS") or "/usr/local/share:/usr/share" roots = [data_home / "icons"] roots.extend(Path(directory) / "icons" for directory in data_dirs.split(":") if directory) # XDG paths are required to be absolute. Ignoring malformed relative # entries also prevents the helper's working directory becoming an # accidental caller-controlled search root. return [root for root in roots if root.is_absolute()] def is_real_directory(path: Path) -> bool: try: return stat.S_ISDIR(path.lstat().st_mode) except OSError: return False def is_real_file(path: Path) -> bool: try: return stat.S_ISREG(path.lstat().st_mode) except OSError: return False def has_icon_directories(index_path: Path) -> bool: try: with index_path.open(encoding="utf-8", errors="replace") as handle: for raw_line in handle: line = raw_line.strip() if line.startswith(("#", ";")) or "=" not in line: continue key, value = line.split("=", 1) if key.strip() == "Directories": return bool(value.strip()) except OSError: return False return False def catalog() -> dict[str, list[str]]: cursor_themes: set[str] = set() icon_themes: set[str] = set() for root in icon_roots(): if not is_real_directory(root): continue try: entries = list(os.scandir(root)) except OSError: continue for entry in entries: if not entry.is_dir(follow_symlinks=False): continue theme = Path(entry.path) if is_real_directory(theme / "cursors"): cursor_themes.add(entry.name) index_path = theme / "index.theme" if is_real_file(index_path) and has_icon_directories(index_path): icon_themes.add(entry.name) return { "cursorThemes": sorted(cursor_themes, key=str.casefold), "iconThemes": sorted(icon_themes, key=str.casefold), } def main() -> int: if len(sys.argv) != 1: print("panama-desktop-style takes no arguments", file=sys.stderr) return 2 print(json.dumps(catalog(), ensure_ascii=False, separators=(",", ":"))) return 0 if __name__ == "__main__": raise SystemExit(main())