Files

99 lines
2.9 KiB
Python
Executable File

#!/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())