#!/usr/bin/env python3 """Indexa las aplicaciones instaladas en ESTA maquina: que hay y para que sirve. ./instalado.py di cuantas entradas saldrian ./instalado.py --volcado imprime unas cuantas de muestra Dos fuentes, ambas locales: 1. Los .desktop (/usr/share/applications, ~/.local, flatpak): las apps que el usuario ve y lanza, con su nombre, para que sirven (Comment) y el comando. 2. Los paquetes que el usuario instalo a mano (apt-mark showmanual), con su descripcion de una linea. No las dependencias: eso es ruido. Es privado por definicion —dice que software tiene esta maquina—, asi que indexa.py lo mete SOLO en el indice personal, nunca en el publico ni en gitea. """ import glob import os import re import subprocess import sys DIRS_DESKTOP = [ "/usr/share/applications", os.path.expanduser("~/.local/share/applications"), "/var/lib/flatpak/exports/share/applications", os.path.expanduser("~/.local/share/flatpak/exports/share/applications"), "/var/lib/snapd/desktop/applications", ] def _campo(texto, clave): m = re.search(r"^%s=(.+)$" % re.escape(clave), texto, re.M) return m.group(1).strip() if m else "" def _apps_desktop(): vistos = set() for d in DIRS_DESKTOP: for ruta in sorted(glob.glob(os.path.join(d, "*.desktop"))): base = os.path.basename(ruta) if base in vistos: continue vistos.add(base) try: with open(ruta, encoding="utf-8", errors="ignore") as f: txt = f.read() except OSError: continue if _campo(txt, "NoDisplay").lower() == "true": continue nombre = _campo(txt, "Name") if not nombre: continue comentario = _campo(txt, "Comment") or _campo(txt, "GenericName") categorias = _campo(txt, "Categories").replace(";", ", ").strip(", ") exe = re.sub(r"%[a-zA-Z]", "", _campo(txt, "Exec")).strip() cmd = exe.split()[0] if exe else "" partes = [f"{nombre}"] if comentario: partes.append(f"— {comentario}") if cmd: partes.append(f"(se lanza con: {cmd})") if categorias: partes.append(f"[{categorias}]") yield { "tipo": "instalado", "herramienta": nombre, "perfil": "SISTEMA", "publico": False, "tema": categorias.lower(), "fichero": f"app instalada: {base}", "texto": "Aplicacion instalada: " + " ".join(partes), } def _paquetes_manuales(): """Los paquetes apt que instalo el usuario (no dependencias), con su resumen.""" try: man = subprocess.run(["apt-mark", "showmanual"], capture_output=True, text=True, timeout=30).stdout.split() except (OSError, subprocess.SubprocessError): return if not man: return # resumen de cada uno en un solo dpkg-query (rapido) try: salida = subprocess.run( ["dpkg-query", "-W", "-f=${Package}\t${binary:Summary}\n", *man], capture_output=True, text=True, timeout=60).stdout except (OSError, subprocess.SubprocessError): return lineas = [] for l in salida.splitlines(): if "\t" not in l: continue pkg, resumen = l.split("\t", 1) if resumen.strip(): lineas.append(f"{pkg}: {resumen.strip()}") # se agrupan en fragmentos (no una entrada por paquete: son cientos) buffer = "Paquetes que el usuario instalo a mano en esta maquina:\n" for ln in lineas: if len(buffer) + len(ln) > 1100 and buffer: yield _pkg_entry(buffer) buffer = "Paquetes instalados (cont.):\n" buffer += "- " + ln + "\n" if buffer.strip(): yield _pkg_entry(buffer) def _pkg_entry(texto): return {"tipo": "instalado", "herramienta": "paquetes apt", "perfil": "SISTEMA", "publico": False, "tema": "paquetes", "fichero": "apt-mark showmanual", "texto": texto.strip()} def entradas(): yield from _apps_desktop() yield from _paquetes_manuales() def main(): import argparse p = argparse.ArgumentParser() p.add_argument("--volcado", action="store_true") a = p.parse_args() apps = pkgs = 0 muestras = [] for e in entradas(): if e["herramienta"] == "paquetes apt": pkgs += 1 else: apps += 1 if len(muestras) < 6: muestras.append(e) print(f" {apps} apps (.desktop) + {pkgs} fragmentos de paquetes apt") if a.volcado: for e in muestras: print(" -", e["texto"][:160].replace("\n", " ")) return 0 if __name__ == "__main__": sys.exit(main())