Files
Classroom_Desktop/scripts/classroom-notify
T
ekklesiaandClaude Opus 5 ba8768cee5 fix: a room that was set up, a card where its button is, and larger cards
Parent Call told rooms they had never been set up. Nothing about the
rooms was wrong. Read from the church broker: …/coordinator and
…/pending are retained and …/mode is absent, and the coordinator's own
source says why — it publishes the retained mode only in answer to an
operator writing to …/mode/set, and nobody ever has. Every classroom
waited a second and a half for a message that will never exist.

A missing mode is now read as the coordinator's own default rather than
as an unconfigured room: the room offers the call and lets the sanctuary
end answer, because that is the end that knows. A mode that really is
off still refuses, in the coordinator's words, and a broker that is
missing or unreachable still says which of those it is. Proved against a
rig broker made to match the church's exactly, with a call landing on
the sanctuary mock.

The card is anchored to the bottom-left corner instead of centred, by
layer shell, which is what the disc player's cover has used under KWin
for months. Anchoring rather than positioning is what makes it right at
1080p and at 4K without a coordinate anywhere. It falls back to the
centred card if the library is missing.

Notifications are sized for a room. Every Plasma measurement is a grid
unit and a grid unit is the height of the general font, so the font is
the lever — 15 point instead of Breeze's 10, which takes the card from
about 350x110 to about 410x145 at 100% and doubles again at 200%. Text,
icon, padding and buttons move together. The chime stays off. No shell
package: that path cost the panel and is not coming back.

And the product name is gone from everything a teacher sees. The
notification card is from Classroom, the disc player's from DVD.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-13 08:34:37 -05:00

217 lines
6.7 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# Puts a notification on the classroom screen, from anywhere.
#
# classroom-notify "Title" "Body text"
# classroom-notify --critical "Parent Call" "Room 3 needs a parent."
# classroom-notify --urgency=low "DVD" "Disc ejected."
# sudo classroom-notify --dry-run
#
# Used by classroom-notify-test to send a sample, and by classroom-mqtt
# to put messages from the church broker on the screen.
#
# The classroom desktop runs as the classroom user, and the notification
# service is part of that user's Plasma session. An administrator over SSH
# sits in a different session where nothing owns
# org.freedesktop.Notifications, so a plain notify-send there asks D-Bus to
# start a service that has no display to open, and times out with NoReply.
# Find the running desktop session and deliver into it instead.
CLASSROOM_USER=classroom
urgency=normal
dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
--critical)
urgency=critical
shift
;;
--low)
urgency=low
shift
;;
--urgency=*)
urgency="${1#--urgency=}"
case "$urgency" in
low|normal|critical) ;;
*) urgency=normal ;;
esac
shift
;;
--dry-run)
dry_run=true
shift
;;
--)
shift
break
;;
-*)
echo "classroom-notify: unknown option $1" >&2
echo "usage: classroom-notify-test [--low|--critical] [--dry-run] [title] [body]" >&2
exit 2
;;
*)
break
;;
esac
done
title="${1:-DVD Ready}"
body="${2:-A disc is ready to play.}"
fail() {
echo "classroom-notify: $1" >&2
exit 1
}
# Another process's environment, or nothing. Reading this needs ptrace
# access, which root has on the endpoint but not, for instance, inside a
# container with CAP_SYS_PTRACE dropped — so every caller has to cope
# with getting nothing back.
read_environ() {
{ tr '\0' '\n' < "/proc/$1/environ"; } 2>/dev/null || true
}
environ_value() {
printf '%s\n' "$1" | sed -n "s/^$2=//p" | head -n 1
}
# The compositor holds its own wayland-N.lock open, so the display name can be read
# back off the process instead of being guessed at.
display_from_lock() {
local link target
for link in "/proc/$1/fd"/*; do
target="$(readlink "$link" 2>/dev/null || true)"
case "$target" in
*/wayland-*.lock)
target="${target%.lock}"
echo "${target##*/}"
return 0
;;
esac
done
return 0
}
classroom_uid="$(id -u "$CLASSROOM_USER" 2>/dev/null || true)"
[[ -n "$classroom_uid" ]] ||
fail "there is no $CLASSROOM_USER user on this machine."
current_uid="$(id -u)"
runner=()
session="this session"
source_note="inherited from this session"
if [[ "$current_uid" == "$classroom_uid" &&
-n "${WAYLAND_DISPLAY:-}" &&
-n "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
# Already inside the classroom desktop session.
session_pid="n/a"
runtime_dir="${XDG_RUNTIME_DIR:-}"
bus="$DBUS_SESSION_BUS_ADDRESS"
wayland_display="$WAYLAND_DISPLAY"
else
session="the $CLASSROOM_USER desktop session"
# The compositor is the session: it owns the Wayland display and the
# environment every other part of the session inherited.
session_pid="$(pgrep -u "$CLASSROOM_USER" -x kwin_wayland | head -n 1 || true)"
[[ -n "$session_pid" ]] ||
fail "no classroom desktop session is running: there is no kwin_wayland process for user $CLASSROOM_USER."
# Checked before anything that needs to look inside the session, so
# an administrator gets told what to do rather than a permission
# error from somewhere further down.
if [[ "$current_uid" != "$classroom_uid" && "$current_uid" != "0" ]]; then
fail "run this as root (sudo classroom-notify) or as the $CLASSROOM_USER user."
fi
environment_block="$(read_environ "$session_pid")"
if [[ -n "$environment_block" ]]; then
source_note="read from the compositor (pid $session_pid)"
else
source_note="the compositor's environment was unreadable; using session defaults"
fi
runtime_dir="$(environ_value "$environment_block" XDG_RUNTIME_DIR)"
runtime_dir="${runtime_dir:-/run/user/$classroom_uid}"
[[ -d "$runtime_dir" ]] ||
fail "the classroom session runtime directory $runtime_dir does not exist."
bus="$(environ_value "$environment_block" DBUS_SESSION_BUS_ADDRESS)"
bus="${bus:-unix:path=$runtime_dir/bus}"
case "$bus" in
unix:path=*)
bus_socket="${bus#unix:path=}"
bus_socket="${bus_socket%%,*}"
[[ -S "$bus_socket" ]] ||
fail "the classroom session bus socket $bus_socket does not exist. Is the desktop session fully up?"
;;
esac
wayland_display="$(environ_value "$environment_block" WAYLAND_DISPLAY)"
if [[ -z "$wayland_display" ]]; then
wayland_display="$(display_from_lock "$session_pid")"
fi
environment=(
"XDG_RUNTIME_DIR=$runtime_dir"
"DBUS_SESSION_BUS_ADDRESS=$bus"
)
# notify-send only needs the bus. The display is passed through for
# the sake of anything else in the session, and its absence is not
# worth refusing to send over.
if [[ -n "$wayland_display" ]]; then
environment+=("WAYLAND_DISPLAY=$wayland_display")
fi
if [[ "$current_uid" == "$classroom_uid" ]]; then
runner=(env "${environment[@]}")
else
runner=(runuser -u "$CLASSROOM_USER" -- env "${environment[@]}")
fi
fi
# Deliver to the notification service that is already running. Sending into
# a session without one would have D-Bus start something with no display to
# draw on, which is exactly the failure this script exists to avoid.
pgrep -u "$CLASSROOM_USER" -x plasmashell >/dev/null 2>&1 ||
fail "the classroom shell is not running, and this will not start it. Check with: pgrep -u $CLASSROOM_USER -a plasmashell"
if [[ "$dry_run" == true ]]; then
echo "Would notify $session:"
echo " compositor pid: $session_pid"
echo " session environment: $source_note"
echo " XDG_RUNTIME_DIR: $runtime_dir"
echo " DBUS_SESSION_BUS_ADDRESS: $bus"
echo " WAYLAND_DISPLAY: ${wayland_display:-(not resolved)}"
echo " urgency: $urgency"
echo " title: $title"
echo " body: $body"
exit 0
fi
"${runner[@]}" notify-send \
--app-name="Classroom" \
--urgency="$urgency" \
-- \
"$title" "$body"
echo "Sent $urgency notification to $session: $title"