99 lines
2.9 KiB
Python
Executable File
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())
|