refactor(synapse): backend updates, add icons module, relocate playbooks to data/playbooks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""Resolve desktop app icon paths and scan installed applications."""
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import configparser
|
||||
|
||||
_HOME = Path.home()
|
||||
|
||||
ICON_SEARCH_DIRS = [
|
||||
_HOME / ".local/share/icons",
|
||||
Path("/usr/share/icons"),
|
||||
Path("/usr/share/pixmaps"),
|
||||
Path("/usr/local/share/icons"),
|
||||
]
|
||||
|
||||
_THEME_ORDER = ["hicolor", "Papirus-Dark", "Papirus", "gnome", "Adwaita", "breeze"]
|
||||
_ICON_EXTS = [".png", ".svg", ".xpm"]
|
||||
|
||||
DESKTOP_DIRS = [
|
||||
_HOME / ".local/share/applications",
|
||||
Path("/usr/local/share/applications"),
|
||||
Path("/usr/share/applications"),
|
||||
]
|
||||
|
||||
_NEXUS_ICONS = _HOME / ".icons" / "NexusOS"
|
||||
|
||||
|
||||
def _icon_stem(icon_name: str) -> str:
|
||||
"""Return the icon's name for theme lookup.
|
||||
|
||||
Only strips a real image extension (.png/.svg/.xpm). Reverse-DNS icon
|
||||
names like ``com.visualstudio.code`` or ``org.gnome.Files`` must be kept
|
||||
intact — Path.stem would wrongly treat the final dotted segment as an
|
||||
extension and truncate it.
|
||||
"""
|
||||
p = Path(icon_name)
|
||||
if p.suffix.lower() in _ICON_EXTS:
|
||||
return p.stem
|
||||
return p.name
|
||||
|
||||
|
||||
def resolve_icon(icon_name: str, size: int = 128) -> Optional[str]:
|
||||
"""Return absolute path to an icon file, or None if not found."""
|
||||
if not icon_name:
|
||||
return None
|
||||
|
||||
# Absolute path — use directly if it exists
|
||||
p = Path(icon_name)
|
||||
if p.is_absolute() and p.exists():
|
||||
return str(p)
|
||||
|
||||
# Strip extension for name-based search
|
||||
stem = _icon_stem(icon_name)
|
||||
|
||||
size_dirs = [f"{size}x{size}", f"{size}x{size}@2x", "scalable"]
|
||||
|
||||
for search_root in ICON_SEARCH_DIRS:
|
||||
if not search_root.is_dir():
|
||||
continue
|
||||
for theme in _THEME_ORDER:
|
||||
theme_dir = search_root / theme
|
||||
if not theme_dir.is_dir():
|
||||
continue
|
||||
for size_dir in size_dirs:
|
||||
for ctx in ("apps", "categories", "places", "status", "actions"):
|
||||
ctx_dir = theme_dir / size_dir / ctx
|
||||
if not ctx_dir.is_dir():
|
||||
continue
|
||||
for ext in _ICON_EXTS:
|
||||
candidate = ctx_dir / f"{stem}{ext}"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
# pixmaps fallback
|
||||
for search_root in ICON_SEARCH_DIRS:
|
||||
if not search_root.is_dir():
|
||||
continue
|
||||
if search_root.name == "pixmaps":
|
||||
for ext in _ICON_EXTS:
|
||||
candidate = search_root / f"{stem}{ext}"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
for ext in _ICON_EXTS:
|
||||
candidate = Path("/usr/share/pixmaps") / f"{stem}{ext}"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _is_branded(icon_name: str) -> bool:
|
||||
"""Return True if a NexusOS-branded PNG already exists for this icon name."""
|
||||
stem = _icon_stem(icon_name)
|
||||
return (_NEXUS_ICONS / "128x128" / "apps" / f"{stem}.png").exists()
|
||||
|
||||
|
||||
def scan_apps() -> list[dict]:
|
||||
"""Return list of installed apps with resolved icon paths."""
|
||||
seen: dict[str, dict] = {}
|
||||
|
||||
for desktop_dir in DESKTOP_DIRS:
|
||||
if not desktop_dir.is_dir():
|
||||
continue
|
||||
for desktop_file in sorted(desktop_dir.glob("*.desktop")):
|
||||
cfg = configparser.ConfigParser(interpolation=None, strict=False)
|
||||
try:
|
||||
cfg.read(str(desktop_file), encoding="utf-8")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not cfg.has_section("Desktop Entry"):
|
||||
continue
|
||||
de = cfg["Desktop Entry"]
|
||||
|
||||
if de.get("NoDisplay", "false").lower() == "true":
|
||||
continue
|
||||
if de.get("Hidden", "false").lower() == "true":
|
||||
continue
|
||||
if de.get("Type", "") != "Application":
|
||||
continue
|
||||
|
||||
name = de.get("Name", desktop_file.stem)
|
||||
icon_name = de.get("Icon", "")
|
||||
if not icon_name:
|
||||
continue
|
||||
|
||||
icon_path = resolve_icon(icon_name)
|
||||
already_branded = _is_branded(icon_name)
|
||||
|
||||
key = name.lower()
|
||||
if key not in seen:
|
||||
seen[key] = {
|
||||
"name": name,
|
||||
"icon_name": _icon_stem(icon_name),
|
||||
"icon_path": icon_path,
|
||||
"already_branded": already_branded,
|
||||
"desktop_file": str(desktop_file),
|
||||
}
|
||||
|
||||
return sorted(seen.values(), key=lambda x: x["name"].lower())
|
||||
Reference in New Issue
Block a user