From 4f9ee5430f0731c9ed82e118acd6649e772509a3 Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 7 Sep 2026 18:15:29 +0000 Subject: [PATCH] =?UTF-8?q?dunst:=20sway=20ya=20atiende=20las=20notificaci?= =?UTF-8?q?ones,=20y=20hay=20un=20p=C3=ADxel=20que=20lo=20prueba?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Era la única mitad que quedaba abierta del mapa del §6.10: KDE atiende `org.freedesktop.Notifications` con plasma-workspace, GNOME con gnome-shell y COSMIC con cosmic-notifications; sway no tenía a NADIE, así que una página que pedía notificar mandaba el mensaje al bus y se lo comía el silencio. `dunst` 1.12.2 y no `mako` —que es el de la casa wlroots— por dos razones medidas: el nombre `mako` YA ESTÁ OCUPADO en el corpus por el motor de plantillas de mesa, y el cierre del perfil incluye las herramientas de build, así que la imagen terminaría con dos artefactos homónimos peleando por las mismas rutas; y mako habla D-Bus por sd-bus, que en musl es una receta nueva (basu), mientras dunst habla por GDBus, que ya está sellado. Cero dependencias nuevas. DOS GUARDIANES EN LA PROPIA RECETA, porque las dos cosas que importan no se ven en que compile: - `X11=0` es una intención; el hecho se lee en el ELF. dunst compila los dos backends por defecto y su config.mk avisa: sin wayland «forzará xwayland». En esta distro eso sella un binario que arranca, toma el bus y NO PINTA NUNCA. La fase exige `libwayland-client` entre los NEEDED y prohíbe `libX11`. - El `.service` de D-Bus es la mitad que importa: sin systemd, al daemon lo levanta EL BUS. Se comprueba que reclame `org.freedesktop.Notifications` y se le saca la línea `SystemdService=`, que acá no puede significar nada. Y `DUNSTIFY=0`: entró primero en 1 y el binario **segfaultea hasta en `--help`** (rc=139 en las tres pruebas). La causa es el cuadro que este repo ya tiene escrito: nuestro `libnotify.so.4` es compartido y arrastra la cadena glib `.so`, mientras esta cola enlaza la GLib estática ⇒ dos copias de GObject en un proceso. No se pierde nada: el emisor de la casa es `notify-send`, que viene DENTRO del artefacto de libnotify y ya está en las cuatro imágenes. Hay un guardián que frena el día que alguien vuelva a poner DUNSTIFY=1 sin leer. LA PRUEBA, en `scripts/wlr/dunst-headless.sh`: sway headless + bus de sesión + `notify-send`, y NADIE lanza dunst a mano — lo activa el bus por el .service, que es el mismo camino de una página web en atuq. La evidencia es un diff de píxeles antes/después acotado al cuadrante donde dunst dibuja, porque un log en verde es compatible con una pantalla vacía y el color no distingue (swaynag pinta su barra de error en el mismo rojo). positivo 14832 píxeles cambiados · servidor: dunst knopwob 1.12.2 1.2 control 0 píxeles · ServiceUnknown · notify-send rc=1 El control negativo borra el .service dentro del overlay temporal y EXIGE que no se dibuje nada: probado en los dos sentidos. Captura en docs/evidencia/dunst-sway-notificacion-2026-09-07.png. Vigía de sonames: escritorio-sway 203 → 204 nodos, 512 sonames, 0 sin proveedor. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs --- .../dunst-sway-notificacion-2026-09-07.png | Bin 0 -> 16320 bytes docs/state/targets.toml | 10 ++ recipes/incoming-wlr/dunst.toml | 143 ++++++++++++++++++ scripts/wlr/dunst-headless.sh | 120 +++++++++++++++ 4 files changed, 273 insertions(+) create mode 100644 docs/evidencia/dunst-sway-notificacion-2026-09-07.png create mode 100644 recipes/incoming-wlr/dunst.toml create mode 100755 scripts/wlr/dunst-headless.sh diff --git a/docs/evidencia/dunst-sway-notificacion-2026-09-07.png b/docs/evidencia/dunst-sway-notificacion-2026-09-07.png new file mode 100644 index 0000000000000000000000000000000000000000..b3840ec4430822a1741e677a4c1597351d152b50 GIT binary patch literal 16320 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 odrtn|5FjT0j)wn+so-1LZmxx~)CjbBd literal 0 HcmV?d00001 diff --git a/docs/state/targets.toml b/docs/state/targets.toml index 4f590813..0ef51d57 100644 --- a/docs/state/targets.toml +++ b/docs/state/targets.toml @@ -677,6 +677,16 @@ paquetes = [ # ⚠ Resuelve la mitad: libnotify no trae daemon, manda `org.freedesktop.Notifications` por D-Bus, # así que la imagen necesita además a alguien escuchando ese nombre. "libnotify", + # `dunst` es LA OTRA MITAD, y sólo hace falta acá: KDE la atiende con plasma-workspace, GNOME con + # gnome-shell y COSMIC con cosmic-notifications; sway no tenía a NADIE escuchando + # `org.freedesktop.Notifications`, así que una página que pedía notificar mandaba el mensaje al bus + # y se lo comía el silencio. Con el `.service` de D-Bus que trae la receta, el daemon lo levanta el + # propio bus la primera vez que alguien pide ese nombre — sin systemd y sin acordarse de lanzarlo + # en la config de sway. + # ⚠ No es `mako` —que es lo que usa media casa wlroots— porque ese nombre YA ESTÁ OCUPADO en el + # corpus por el motor de plantillas de Python de mesa: dos artefactos homónimos en la misma imagen + # es la colisión que la granja ya nos cobró. El porqué largo está en la receta. + "dunst", # ── VISOR DE IMÁGENES, TAMBIÉN EN LAS CUATRO ───────────────────────────────────────────────── # `swayimg` y no `imv`, que era lo que proponía `docs/plan-apps-usuario-final.md`: imv dibuja con # OpenGL de función fija (`glBegin`/`glOrtho`) y esta distro NO tiene proveedor de GL de diff --git a/recipes/incoming-wlr/dunst.toml b/recipes/incoming-wlr/dunst.toml new file mode 100644 index 00000000..018bf043 --- /dev/null +++ b/recipes/incoming-wlr/dunst.toml @@ -0,0 +1,143 @@ +# dunst 1.12.2 — el daemon de notificaciones que le faltaba a la imagen de sway. +# +# ── POR QUÉ EXISTE ESTA RECETA, Y POR QUÉ ACÁ ────────────────────────────────────────────────── +# El SDD 26 §6.10 midió perfil por perfil quién ATIENDE `org.freedesktop.Notifications`, que es la +# otra mitad de tener `libnotify`: +# +# escritorio-kde plasma-workspace ✓ completa +# escritorio-gnome gnome-shell ✓ completa +# escritorio-cosmic cosmic-notifications ✓ completa +# escritorio-sway — ✗ MEDIA: nadie atiende +# +# Con `libnotify` en la imagen y nadie del otro lado, una página que pide notificar no falla: el +# navegador manda el mensaje al bus y no lo recoge nadie. Media función, cero errores — la forma que +# este proyecto ya se cansó de pagar. +# +# ── POR QUÉ dunst Y NO mako, QUE ES EL DE LA CASA wlroots ────────────────────────────────────── +# Por dos razones medidas, no por gusto: +# +# 1. **El nombre `mako` YA ESTÁ OCUPADO en el corpus** por el motor de plantillas de Python que usa +# Mesa para su codegen. Un `recipes/incoming-wlr/mako.toml` no lo pisaría en el disco —la +# resolución es sibling-first— pero el cierre del perfil incluye las herramientas de build, así +# que la imagen de sway terminaría con DOS artefactos llamados `mako` peleando por las mismas +# rutas al hidratar. Es exactamente la colisión de homónimos que la granja ya nos cobró una vez. +# 2. mako habla D-Bus por `sd-bus`, que en musl significa una receta nueva (`basu`). dunst habla por +# **GDBus**, o sea `gio-2.0`, que ya está sellado. Cero recetas nuevas de dependencia. +# +# ── WAYLAND=1 X11=0, Y EL GUARDIÁN QUE LO EXIGE ─────────────────────────────────────────────── +# dunst compila los dos backends por defecto y el `config.mk` avisa de la trampa con todas las +# letras: «Disable dependency on wayland. This will force dunst to use xwayland». O sea que un build +# descuidado sella un binario que en esta distro —Wayland-only, sin GLX y sin Xwayland nativo— NO +# PINTA NADA y tampoco falla al arrancar. Por eso el `X11=0` no alcanza como intención: la fase +# `install` comprueba en el ELF que `libwayland-client` esté entre los NEEDED y que `libX11` NO +# esté. La intención se declara en el flag; el hecho se lee en el binario. +# +# ── EL FICHERO DE SERVICIO D-BUS ES LA MITAD QUE IMPORTA ────────────────────────────────────── +# Sin systemd (`SYSTEMD=0`, que es la postura de la distro) el daemon lo arranca **el bus**: el +# `.service` declara `Name=org.freedesktop.Notifications` con su `Exec`, y cualquier app que pida ese +# nombre lo levanta sola. Sin ese fichero habría que acordarse de lanzar dunst a mano en la config de +# sway, que es justo el tipo de «lo instalé y no anda» que este proyecto trata de no dejar. Se le +# quita la línea `SystemdService=`, que acá no puede significar nada, y la fase comprueba que el +# nombre bien escrito quedó adentro. +# +# ── `DUNSTIFY=0`: EL EMISOR QUE TRAE dunst SEGFAULTEA, Y NO ES CULPA DE dunst ───────────────── +# Entró primero con `DUNSTIFY=0` —parecía gratis, `libnotify` ya está en el corpus— y el binario +# resultante **muere con SIGSEGV hasta en `dunstify --help`**, o sea antes de tocar el bus. Medido en +# la jaula: rc=139 en las tres pruebas. +# +# La causa es el cuadro que este repo ya tiene escrito: **dos GLib en el mismo proceso**. Nuestro +# `libnotify.so.4` es COMPARTIDO y arrastra `libgobject/libglib/libgio/libgdk_pixbuf` `.so` +# (`readelf -d` lo dice), mientras que dunst y sus clientes de esta cola enlazan la GLib **estática** +# del corpus. Un dunstify que mezcla las dos termina con dos copias del sistema de tipos de GObject y +# se cae al arrancar. El daemon NO tiene el problema: no toca libnotify. +# +# Y no se pierde nada, que es lo que hace que ésta sea la salida correcta y no una rendición: el +# emisor de la casa es **`notify-send`, que viene DENTRO del artefacto de `libnotify`** y ya está en +# las cuatro imágenes, enlazado de forma consistente contra la cadena compartida. Un binario que +# segfaultea shipeado en la imagen es peor que no shipearlo. +# +# ── EL BINARIO PESA 46 MB, Y LA PALANCA NO ESTÁ EN ESTA RECETA ─────────────────────────────── +# Anotado porque el reflejo se equivoca dos veces. `config.mk` hardcodea `-g`, así que uno prueba +# `EXTRA_CFLAGS=-g0` esperando bajar decenas de megas: medido, 46.296.624 → 45.933.472 bytes, o sea +# 0,8%. Y sin embargo la grasa SÍ es depuración — `strip --strip-debug` deja el binario en 7.627.464 +# bytes, los mismos con `-g0` y sin él. Son 38 MB de `.debug_*` que vienen de los `.a` de +# glib/gio/pango/cairo/gdk-pixbuf, no de compilar dunst. +# Por eso acá no se toca: strippear en el enlace sería inventar una política en UNA receta cuando el +# corpus entero tiene la misma forma (swaybg 22 MB, fuzzel 10 MB) y ya hay un documento para eso, el +# SDD 23. Lo que corresponde es medirlo y dejarlo escrito donde se ve. +# +# `VERSION` se pasa a mano: el Makefile la saca de `git describe` y cae a un literal si no hay `.git`. +# Con el commit pineado las dos ramas son deterministas, pero cuál de las dos corre depende de si el +# árbol de fuentes trae `.git` — un detalle del build system que no tiene por qué entrar al binario. +name = "dunst" +version = "1.12.2" +license = "BSD-3-Clause" + +[source] +# Por commit y no por `/archive/.tar.gz`, misma razón que `fuzzel` y `foot`: el tarball lo +# genera la forge al vuelo y su sha256 se muere solo cuando GitHub actualiza su git/gzip. +# ⚠ El tag `v1.12.2` es anotado; éste es el commit desreferenciado (`v1.12.2^{}`). +repo = "https://github.com/dunst-project/dunst.git" +commit = "fc78b9f11aa8b63f4d81c96466206a9b63a22f07" + +[build] +compiler = "zig-cc" +target = "x86_64-linux-musl" +link = "dynamic" +zig_version = "0.13.0" + +[build.phases] +configure = ''' +# ── EL `-lexpat` TIENE QUE IR AL FINAL, Y EL Makefile NO DEJA ──────────────────────────────── +# `libfontconfig.a` referencia XML_* y el expat estático hay que darlo DESPUÉS: el linker resuelve +# de izquierda a derecha. En las recetas meson de esta cola eso se hace con `-Dc_link_args=-lexpat`, +# pero acá manda un Makefile que arma la línea como `${LDFLAGS} = DEFAULT + + LIBS`, +# o sea que cualquier `-lexpat` nuestro cae ANTES de fontconfig y no sirve de nada. Sobreescribir +# `LIBS` significaría reproducir a mano toda la salida de pkg-config. +# La salida es un wrapper de un solo token que agrega la librería al final de CADA invocación —el +# mismo truco que usa `recipes/ffmpeg.toml` para sus `--cc/--ar`—. En las compilaciones `-c` el +# `-lexpat` sobra y clang lo ignora con un aviso. +ZW="$PWD/.zwrap"; mkdir -p "$ZW" +printf '#!/bin/sh\nexec %s "$@" -lexpat\n' "$CC" > "$ZW/cc" +chmod +x "$ZW/cc" +''' +compile = ''' +make -j"$(nproc)" CC="$PWD/.zwrap/cc" WAYLAND=1 X11=0 SYSTEMD=0 DUNSTIFY=0 \ + PREFIX=/usr SYSCONFDIR=/etc/xdg VERSION="1.12.2" +''' +install = ''' +make DESTDIR=/out CC="$PWD/.zwrap/cc" WAYLAND=1 X11=0 SYSTEMD=0 DUNSTIFY=0 \ + PREFIX=/usr SYSCONFDIR=/etc/xdg VERSION="1.12.2" install + +# ── GUARDIÁN 1: ¿es el backend de wayland, o quedó el de X11? ──────────────────────────────── +# «Compilé con X11=0» es una intención; esto es el hecho. Un dunst con backend X11 en una distro +# sin Xwayland arranca, toma el bus y no dibuja nunca. +readelf -d /out/usr/bin/dunst | grep -q 'libwayland-client' || { + echo "guardián: dunst NO enlaza libwayland-client — el backend wayland no entró" >&2; exit 1; } +if readelf -d /out/usr/bin/dunst | grep -q 'libX11'; then + echo "guardián: dunst enlaza libX11 — X11=0 no agarró" >&2; exit 1 +fi + +# ── GUARDIÁN 2: el nombre del bus, escrito en el fichero que lo activa ─────────────────────── +SVC=/out/usr/share/dbus-1/services/org.knopwob.dunst.service +[ -f "$SVC" ] || { echo "guardián: no se instaló el .service de D-Bus — nadie lo activaría" >&2; exit 1; } +grep -q '^Name=org.freedesktop.Notifications$' "$SVC" || { + echo "guardián: el .service no reclama org.freedesktop.Notifications" >&2; exit 1; } +# Sin systemd esa línea no puede significar nada; se va para que el bus no la mire siquiera. +sed -i '/^SystemdService=/d' "$SVC" + +test -x /out/usr/bin/dunstctl || { echo "guardián: falta dunstctl" >&2; exit 1; } +# Y que NO se haya colado el emisor roto: si alguien vuelve a poner DUNSTIFY=1 sin leer el porqué +# de arriba, esto lo frena acá y no en la pantalla de un usuario. +if [ -e /out/usr/bin/dunstify ]; then + echo "guardián: se instaló dunstify — segfaultea por las dos GLib; el emisor es notify-send" >&2 + exit 1 +fi +echo "guardián: dunst wayland-only, y el bus lo puede activar por org.freedesktop.Notifications" +''' + +[deps] +build = ["pkgconf", "perl", "wayland", "wayland-protocols", "glib", "pango", "cairo", + "gdk-pixbuf", "libnotify", "pixman", "fontconfig", "freetype", "harfbuzz", "fribidi", + "libpng", "expat", "libffi", "pcre2", "zlib"] +run = ["wayland", "glib", "pango", "cairo", "gdk-pixbuf"] diff --git a/scripts/wlr/dunst-headless.sh b/scripts/wlr/dunst-headless.sh new file mode 100755 index 00000000..250c4fae --- /dev/null +++ b/scripts/wlr/dunst-headless.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# dunst-headless.sh — ¿la imagen de sway ATIENDE una notificación, y se VE? +# +# scripts/wlr/dunst-headless.sh # sale 0 si la notificación se dibuja +# scripts/wlr/dunst-headless.sh --negative-control # borra el .service y exige que NO se vea +# +# ── QUÉ PRUEBA, QUE NO ES LO MISMO QUE «ESTÁ INSTALADO» ─────────────────────────────────────── +# Tener `libnotify` en la imagen es media función: el que la completa es un daemon escuchando +# `org.freedesktop.Notifications`. KDE lo tiene por plasma-workspace, GNOME por gnome-shell, COSMIC +# por cosmic-notifications; sway no tenía a nadie, y por eso entró `dunst`. Que la receta selle no +# prueba nada de eso: hay que ver el bus ACTIVAR el daemon y el daemon DIBUJAR. +# +# El camino que se ejercita es el de verdad, no un atajo: +# +# notify-send → bus de sesión → activación por el .service → dunst arranca → pinta en sway +# +# **Nadie lanza dunst a mano acá**, a propósito: si el `.service` no estuviera, o el `Exec` apuntara +# a otro lado, o dunst no supiera hablar wayland, este guion no vería nada. Es el mismo camino que +# recorre una página web pidiendo notificar en `atuq`. +# +# ── POR QUÉ notify-send Y NO dunstify ───────────────────────────────────────────────────────── +# Porque `dunstify` no se shipea: mezcla la GLib ESTÁTICA del corpus con la COMPARTIDA que arrastra +# `libnotify.so.4` y se cae con SIGSEGV hasta en `--help` (dos copias de GObject en un proceso — el +# cuadro ya conocido de este repo). El emisor de la casa es `notify-send`, que viene dentro del +# artefacto de `libnotify` y enlaza toda la cadena compartida. El porqué largo está en la receta. +# +# ── LA EVIDENCIA ES UN DIFF DE PÍXELES, NO UN LOG ───────────────────────────────────────────── +# Se captura ANTES y DESPUÉS de mandar la notificación y se cuenta cuántos píxeles cambiaron en el +# cuadrante donde dunst dibuja. Un log lleno de líneas verdes es compatible con una pantalla vacía — +# ya pasó en este mismo andamio— y el color solo tampoco alcanza: swaynag pinta su barra de error en +# el MISMO rojo que dunst usa para `critical`, así que «hay rojo» no distingue nada. Lo que no se +# puede falsear es que aparezca algo donde antes no había nada. +set -eu +ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" +RFS="${RFS:-/mnt/cosecha/escritorios/sway-rootfs}" +ALPINE="$ROOT/.dev-fs/alpine" +CONTROL=0 +[ "${1:-}" = "--negative-control" ] && CONTROL=1 + +[ -d "$RFS" ] || { echo "✗ no está el rootfs en $RFS — corré antes:" >&2 + echo " scripts/hydrate-profile.py escritorio-sway --into $RFS" >&2; exit 1; } +command -v bwrap >/dev/null || { echo "✗ falta bwrap" >&2; exit 1; } + +OUT="$(mktemp -d)" +trap 'rm -rf "$OUT"' EXIT + +cat > "$OUT/dentro.sh" <<'ADENTRO' +#!/bin/sh +export XDG_RUNTIME_DIR=/run/user/0 HOME=/root LANG=C XDG_DATA_DIRS=/usr/share +export WLR_BACKENDS=headless WLR_HEADLESS_OUTPUTS=1 WLR_RENDERER=pixman +mkdir -p "$XDG_RUNTIME_DIR" /root; chmod 700 "$XDG_RUNTIME_DIR" +# El control negativo saca el fichero que hace posible la activación. La jaula corre sobre un +# overlay temporal, así que borrar acá no toca el rootfs de verdad. +[ "${CONTROL:-0}" = "1" ] && rm -f /usr/share/dbus-1/services/org.knopwob.dunst.service +( + i=0 + while [ ! -S /run/user/0/wayland-1 ] && [ $i -lt 40 ]; do sleep 1; i=$((i+1)); done + [ -S /run/user/0/wayland-1 ] || { echo "@@ sin socket wayland"; exit 1; } + export WAYLAND_DISPLAY=wayland-1 + /usr/bin/swaybg -c '#1a4b8c' & sleep 3 + # El bus se levanta CON el entorno wayland puesto: lo que él active lo hereda, igual que en una + # sesión de verdad. Al revés, dunst arrancaría sin display y saldría con status 1. + eval "$(/usr/bin/dbus-daemon --session --fork --print-address=1 | { read a; echo "DBUS_SESSION_BUS_ADDRESS=$a"; })" + export DBUS_SESSION_BUS_ADDRESS + /usr/bin/grim /salida/antes.png + echo "@@ dueño ANTES: $(timeout 10 /usr/bin/dbus-send --session --dest=org.freedesktop.DBus \ + --print-reply=literal /org/freedesktop/DBus org.freedesktop.DBus.NameHasOwner \ + string:org.freedesktop.Notifications 2>&1 | tr -d ' \n')" + timeout 25 /usr/bin/notify-send -u critical "hammer" "dunst lo activó el bus" 2>&1 + echo "@@ notify-send rc=$?" + sleep 5 + echo "@@ servidor: $(timeout 10 /usr/bin/dbus-send --session --dest=org.freedesktop.Notifications \ + --print-reply=literal /org/freedesktop/Notifications \ + org.freedesktop.Notifications.GetServerInformation 2>&1 | tr -s ' ')" + /usr/bin/grim /salida/despues.png && echo "@@ capturas listas" + sleep 1 + /usr/bin/swaymsg exit 2>/dev/null +) & +exec /usr/bin/sway -d 2>&1 +ADENTRO + +# `--unshare-pid` para que un `ps` de adentro no vea la máquina entera, y el lab de alpine DEBAJO +# porque el corpus no produce el intérprete musl (`ld-musl-x86_64.so.1`): sin él ni sway arranca. +timeout 180 bwrap --overlay-src "$ALPINE" --overlay-src "$RFS" --tmp-overlay / \ + --bind "$OUT" /salida --dev /dev --proc /proc --tmpfs /run --tmpfs /tmp --unshare-pid \ + --uid 0 --gid 0 --setenv PATH /usr/bin:/bin:/usr/sbin:/sbin --setenv CONTROL "$CONTROL" \ + /bin/sh /salida/dentro.sh > "$OUT/log.txt" 2>&1 || true + +grep -a '@@' "$OUT/log.txt" || true +[ -f "$OUT/antes.png" ] && [ -f "$OUT/despues.png" ] || { + echo "✗ no hubo capturas — mirá $OUT/log.txt" >&2; cp "$OUT/log.txt" work/dunst-headless.log 2>/dev/null || true; exit 1; } + +# El diff se hace acá afuera: dentro de la imagen no hay python ni con qué mirar un PNG. +CAMBIADOS="$(python3 - "$OUT/antes.png" "$OUT/despues.png" <<'PY' +import sys +from PIL import Image, ImageChops +a, b = (Image.open(p).convert("RGB") for p in sys.argv[1:3]) +# Sólo el cuadrante donde dunst dibuja por defecto (arriba a la derecha). Acotarlo saca del medio la +# barra de swaynag y el reloj de swaybar, que cambian solos y darían un falso positivo. +w, h = a.size +caja = (w // 2, h // 12, w, h // 2) +d = ImageChops.difference(a.crop(caja), b.crop(caja)).convert("L") +# Por histograma y no recorriendo píxeles: el bin 0 son los que NO cambiaron. +print(d.width * d.height - d.histogram()[0]) +PY +)" +echo " píxeles cambiados en el cuadrante de dunst: $CAMBIADOS" + +if [ "$CONTROL" = "1" ]; then + if [ "$CAMBIADOS" -lt 500 ]; then + echo "✓ control negativo: sin el .service no se dibuja nada (el guardián SÍ lo vería)"; exit 0 + fi + echo "✗ control negativo: se dibujó una notificación SIN el .service — este guion no prueba lo que dice" >&2 + exit 1 +fi +[ "$CAMBIADOS" -ge 500 ] || { + echo "✗ la notificación no se dibujó ($CAMBIADOS píxeles) — capturas en $OUT" >&2 + cp "$OUT"/*.png "$OUT/log.txt" work/ 2>/dev/null || true; exit 1; } +cp "$OUT/despues.png" work/dunst-headless.png 2>/dev/null || true +echo "✓ el bus activó dunst y la notificación se dibujó (captura en work/dunst-headless.png)"