feat: give the classroom a desktop instead of a shell

The whole appliance UI is now configuration for a desktop Debian already
ships: a Plasma 6.3 session with a colour scheme, a look and feel package
that builds the bar, window rules, and KDE's own Kiosk restrictions.

Two things were measured rather than assumed, and both changed a decision.

Plasma restarts a crashed panel three times a minute and then gives up,
which is the failure a classroom has already had. Four lines of systemd
drop-in make it unconditional; the panel then survived eight SIGKILLs a
second apart, back each time in 2.4s.

Plasma draws a minimised task and a background task identically, so the
five states the room needs cannot all be shown. Rather than write a task
manager, the classroom removes minimise: clicking a launcher always means
show me this.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-10 08:35:12 -05:00
co-authored by Claude Opus 5
parent bfc7caf28f
commit 87b55761c6
51 changed files with 1352 additions and 36 deletions
@@ -0,0 +1,31 @@
# Everything the classroom's appearance is made of, in one place.
#
# Plasma reads this when the look and feel is applied, so the panel, the
# window decorations, the notifications and every Qt dialog in the session
# are described here rather than in a stylesheet per surface.
[kdeglobals][KDE]
widgetStyle=Breeze
[kdeglobals][General]
ColorScheme=EkklesiaDark
AccentColor=79,179,168
[kdeglobals][Icons]
Theme=breeze-dark
[plasmarc][Theme]
name=default
[kcminputrc][Mouse]
cursorTheme=breeze_cursors
[kwinrc][org.kde.kdecoration2]
library=org.kde.breeze
theme=Breeze
[Wallpaper]
Image=/usr/share/backgrounds/ekklesia/classroom-calm.png
[KSplash]
Theme=None
@@ -0,0 +1,124 @@
// The Ekklesia Classroom desktop.
//
// Plasma runs this once, when a session has no panel yet, and again on
// demand — `plasma-apply-lookandfeel -a org.ekklesia.classroom.desktop
// --resetLayout` rebuilds the whole desktop from this file. That is the
// classroom's recovery path for a damaged shell, and it is Plasma's own
// mechanism rather than something written here.
//
// One full-width bar along the bottom. Left is auxiliary, the middle is
// applications, the right is the small amount of status a teacher needs.
// Nothing else is on the screen.
var WALLPAPER = "/usr/share/backgrounds/ekklesia/classroom-calm.png";
// Matches the 76px strip the classroom has always used, so muscle memory
// and the room's viewing distance both carry over.
var PANEL_HEIGHT = 76;
// The idle desktop: the wallpaper, and nothing on it.
var screens = desktops();
for (var i = 0; i < screens.length; i++) {
var desktop = screens[i];
desktop.wallpaperPlugin = "org.kde.image";
desktop.currentConfigGroup = ["Wallpaper", "org.kde.image", "General"];
desktop.writeConfig("Image", "file://" + WALLPAPER);
desktop.writeConfig("SlidePaths", "");
// 2 is "scaled and cropped": the landscape fills the display at any
// aspect ratio without being stretched out of shape.
desktop.writeConfig("FillMode", 2);
desktop.reloadConfig();
}
var panel = new Panel();
panel.location = "bottom";
panel.height = PANEL_HEIGHT;
panel.hiding = "none";
panel.floating = false;
panel.lengthMode = "fill";
panel.alignment = "center";
panel.opacity = "adaptive";
// LEFT — auxiliary. Parent Call is not one of the room's applications and
// is deliberately not in the row with them.
var parentCall = panel.addWidget("org.kde.plasma.icon");
parentCall.currentConfigGroup = ["General"];
parentCall.writeConfig(
"url",
"file:///usr/share/applications/ekklesia-parent-call.desktop"
);
var leftGap = panel.addWidget("org.kde.plasma.panelspacer");
leftGap.currentConfigGroup = ["General"];
leftGap.writeConfig("expanding", true);
// CENTRE — the applications, and the whole of the running/minimised/active
// story. This applet is the reason the classroom stops writing a dock.
var tasks = panel.addWidget("org.kde.plasma.icontasks");
tasks.currentConfigGroup = ["General"];
tasks.writeConfig("launchers", [
"applications:ekklesia-wonderink.desktop",
"applications:google-chrome.desktop"
].join(","));
// Every window gets its own button. Teachers should never have to open a
// group to find out that a second window exists.
tasks.writeConfig("groupingStrategy", 0);
tasks.writeConfig("showOnlyCurrentDesktop", false);
tasks.writeConfig("showOnlyCurrentActivity", false);
tasks.writeConfig("showOnlyCurrentScreen", false);
// Clicking a button always means "show me this", and never "hide it".
//
// This was measured before it was decided. Plasma 6.3 draws a minimised
// task and a task that is merely behind another window identically: no
// indicator for closed, a grey indicator for running, a teal one for
// active — three marks for four states. Rather than write a task manager
// that draws a fourth, the classroom removes the state. There is nothing
// on the idle desktop for a teacher to want to see, so minimising an
// application in this room has no purpose and only ever loses it.
//
// A window minimised some other way is still shown as running and comes
// back with one click, so it can never be lost even then.
tasks.writeConfig("minimizeActiveTaskOnClick", false);
tasks.writeConfig("fill", false);
tasks.writeConfig("maxStripes", 1);
tasks.writeConfig("indicateAudioStreams", false);
tasks.writeConfig("wheelEnabled", false);
tasks.writeConfig("middleClickAction", "NewInstance");
tasks.writeConfig("showToolTips", true);
tasks.writeConfig("iconSpacing", 2);
var rightGap = panel.addWidget("org.kde.plasma.panelspacer");
rightGap.currentConfigGroup = ["General"];
rightGap.writeConfig("expanding", true);
// RIGHT — status.
//
// Four ordinary applets straight on the bar, and no system tray around
// them. A tray is a container that discovers whatever is installed and
// then hides most of it behind an arrow; measured in the lab it collected
// battery, brightness, clipboard, keyboard layout, camera, vaults and
// screen management on a machine that has none of those to say anything
// about. Naming the four the classroom actually uses is both smaller and
// deterministic, and it leaves nothing for a teacher to expand.
panel.addWidget("org.kde.plasma.networkmanagement");
panel.addWidget("org.kde.plasma.volume");
// Present only while there is something in a drive. Plasma's own Solid
// device notifier is what makes the DVD appear with the disc and leave
// with it, so nothing in the classroom polls for media.
panel.addWidget("org.kde.plasma.devicenotifier");
panel.addWidget("org.kde.plasma.notifications");
var clock = panel.addWidget("org.kde.plasma.digitalclock");
clock.currentConfigGroup = ["Appearance"];
clock.writeConfig("showDate", true);
clock.writeConfig("dateFormat", "custom");
clock.writeConfig("customDateFormat", "ddd d MMM");
clock.writeConfig("showSeconds", 0);
clock.writeConfig("dateDisplayFormat", "BelowTime");
// Left to itself the clock grows to the height of the bar and becomes the
// largest text in the room. It is status, not a headline.
clock.writeConfig("autoFontAndSize", false);
clock.writeConfig("fontSize", 15);
clock.writeConfig("fontWeight", 500);