Taking the launcher off the bar is half of a feature switch. The command has to refuse as well, or a device that does not offer an application can still be talked into running it by a stale desktop entry, a leftover window rule or a technician's muscle memory. Wonder Ink already did this. Parent Call now does the same, through the same helper and with the same default: absent means yes, so every existing room is unaffected. Co-Authored-By: Claude Opus 5 <[email protected]>
231 lines
8.6 KiB
Bash
Executable File
231 lines
8.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# Which applications a device offers, and the bar that comes out of it.
|
|
#
|
|
# Two halves. The first is the decision — one setting per application, and
|
|
# never the shape of the machine. The second actually RUNS the layout
|
|
# script the package ships, in node, with Plasma's API stubbed, so the
|
|
# assertions are about the real file rather than about a copy of its text
|
|
# that could drift away from it.
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from importlib.machinery import SourceFileLoader
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1])
|
|
loader = SourceFileLoader("layout", str(root / "scripts/classroom-desktop-layout"))
|
|
spec = importlib.util.spec_from_loader("layout", loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(module)
|
|
|
|
LAYOUT = root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js"
|
|
|
|
passed = failed = 0
|
|
|
|
|
|
def check(label, condition, detail=""):
|
|
global passed, failed
|
|
if condition:
|
|
print(f" ok {label}")
|
|
passed += 1
|
|
else:
|
|
print(f" FAIL {label}")
|
|
if detail:
|
|
print(f" {detail}")
|
|
failed += 1
|
|
|
|
|
|
BLOCK = """/* CLASSROOM-FEATURES-BEGIN */
|
|
var WONDERINK = true;
|
|
var DVD = true;
|
|
var PARENT_CALL = true;
|
|
/* CLASSROOM-FEATURES-END */"""
|
|
|
|
|
|
print("1. one line per application, rewritten in place")
|
|
check("a feature can be switched off",
|
|
"var WONDERINK = false;" in module.rewrite(BLOCK, "WONDERINK", False))
|
|
check("and back on again",
|
|
"var WONDERINK = true;"
|
|
in module.rewrite(module.rewrite(BLOCK, "WONDERINK", False),
|
|
"WONDERINK", True))
|
|
check("switching one leaves the others alone",
|
|
"var DVD = true;" in module.rewrite(BLOCK, "WONDERINK", False)
|
|
and "var PARENT_CALL = true;" in module.rewrite(BLOCK, "WONDERINK", False))
|
|
check("rewriting twice changes nothing the second time",
|
|
module.rewrite(module.rewrite(BLOCK, "DVD", False), "DVD", False)
|
|
== module.rewrite(BLOCK, "DVD", False))
|
|
|
|
|
|
print("2. the shape of the machine never decides what it is for")
|
|
# This is the architecture rule, and it is the one worth a test: a laptop
|
|
# must be able to run children's church and a tower must be able to be a
|
|
# Bible study.
|
|
module.has_optical_drive = lambda: True
|
|
|
|
laptop_with_ink = module.wanted({"DEVICE_TYPE": "laptop"})
|
|
check("a laptop still offers Wonder Ink unless told otherwise",
|
|
laptop_with_ink["WONDERINK"] is True, str(laptop_with_ink))
|
|
check("a laptop still offers Parent Call unless told otherwise",
|
|
laptop_with_ink["PARENT_CALL"] is True)
|
|
|
|
desktop_no_ink = module.wanted(
|
|
{"DEVICE_TYPE": "desktop", "WONDERINK_ENABLED": "false"})
|
|
check("a desktop can have Wonder Ink taken away",
|
|
desktop_no_ink["WONDERINK"] is False)
|
|
check("and keeps everything else",
|
|
desktop_no_ink["DVD"] is True and desktop_no_ink["PARENT_CALL"] is True)
|
|
|
|
check("an existing room that has never heard of these keeps them all",
|
|
all(module.wanted({}).values()), str(module.wanted({})))
|
|
|
|
|
|
print("3. a drive can only ever take the player away")
|
|
module.has_optical_drive = lambda: False
|
|
check("no drive means no player, whatever the setting says",
|
|
module.wanted({"DVD_ENABLED": "true"})["DVD"] is False)
|
|
module.has_optical_drive = lambda: True
|
|
check("a drive plus DVD_ENABLED=false is still no player",
|
|
module.wanted({"DVD_ENABLED": "false"})["DVD"] is False)
|
|
check("a drive plus DVD_ENABLED=true is the player",
|
|
module.wanted({"DVD_ENABLED": "true"})["DVD"] is True)
|
|
|
|
|
|
print("4. the script rewrites the layout the package actually ships")
|
|
module.has_optical_drive = lambda: True
|
|
|
|
def run(config):
|
|
"""Run the real script over a copy of the real layout."""
|
|
work = Path(tempfile.mkdtemp(prefix="classroom-layout-"))
|
|
copy = work / "layout.js"
|
|
shutil.copy(LAYOUT, copy)
|
|
|
|
module.read_device_config = lambda: config
|
|
module.main.__globals__["sys"].argv = ["classroom-desktop-layout", str(copy)]
|
|
module.main()
|
|
|
|
return copy.read_text(), work
|
|
|
|
text, work = run({})
|
|
check("the shipped layout carries the feature block",
|
|
module.MARKER in text)
|
|
check("and an unconfigured machine keeps every application",
|
|
"var WONDERINK = true;" in text and "var DVD = true;" in text
|
|
and "var PARENT_CALL = true;" in text)
|
|
|
|
|
|
print("5. the bar that comes out, evaluated in node")
|
|
STUB = r"""
|
|
const fs = require("fs");
|
|
const captured = {widgets: [], config: {}};
|
|
let current = null;
|
|
function makeWidget(type) {
|
|
const w = {
|
|
type: type,
|
|
currentConfigGroup: [],
|
|
writeConfig: function (k, v) { captured.config[type + "." + k] = String(v); },
|
|
reloadConfig: function () {}
|
|
};
|
|
captured.widgets.push(type);
|
|
return w;
|
|
}
|
|
globalThis.desktops = () => [makeWidget("desktop")];
|
|
globalThis.Panel = function () {
|
|
this.addWidget = makeWidget;
|
|
return this;
|
|
};
|
|
eval(fs.readFileSync(process.argv[2], "utf8"));
|
|
console.log(JSON.stringify(captured));
|
|
"""
|
|
|
|
def bar(config):
|
|
text, work = run(config)
|
|
stub = work / "stub.js"
|
|
stub.write_text(STUB)
|
|
result = subprocess.run(
|
|
["node", str(stub), str(work / "layout.js")],
|
|
capture_output=True, text=True, timeout=60,
|
|
)
|
|
if result.returncode != 0:
|
|
return None, result.stderr
|
|
return json.loads(result.stdout), ""
|
|
|
|
|
|
if shutil.which("node") is None:
|
|
print(" SKIP node is not installed; the layout was not evaluated")
|
|
else:
|
|
LAUNCH = "org.kde.plasma.icontasks.launchers"
|
|
INK = "applications:ekklesia-wonderink.desktop"
|
|
WEB = "applications:google-chrome.desktop"
|
|
DISC = "applications:ekklesia-dvd.desktop"
|
|
|
|
every, why = bar({})
|
|
check("a classroom gets Wonder Ink, the browser and the player, in order",
|
|
every and every["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}",
|
|
(every or {}).get("config", {}).get(LAUNCH, why))
|
|
check("and Parent Call in the corner",
|
|
every and "org.kde.plasma.quicklaunch" in every["widgets"])
|
|
|
|
no_ink, why = bar({"WONDERINK_ENABLED": "false"})
|
|
check("a Bible study desktop has the browser first and no gap",
|
|
no_ink and no_ink["config"].get(LAUNCH) == f"{WEB},{DISC}",
|
|
(no_ink or {}).get("config", {}).get(LAUNCH, why))
|
|
|
|
module.has_optical_drive = lambda: False
|
|
bible, why = bar({"WONDERINK_ENABLED": "false",
|
|
"PARENT_CALL_ENABLED": "false",
|
|
"DEVICE_TYPE": "laptop"})
|
|
check("the laptop's bar is the browser alone",
|
|
bible and bible["config"].get(LAUNCH) == WEB,
|
|
(bible or {}).get("config", {}).get(LAUNCH, why))
|
|
check("with no Parent Call button on it",
|
|
bible and "org.kde.plasma.quicklaunch" not in bible["widgets"],
|
|
str((bible or {}).get("widgets")))
|
|
check("and the status corner is untouched",
|
|
bible and "org.kde.plasma.volume" in bible["widgets"]
|
|
and "org.kde.plasma.digitalclock" in bible["widgets"]
|
|
and "org.kde.plasma.networkmanagement" in bible["widgets"])
|
|
|
|
module.has_optical_drive = lambda: True
|
|
ink_laptop, why = bar({"DEVICE_TYPE": "laptop"})
|
|
check("a laptop taking children's church gets the same bar as a tower",
|
|
ink_laptop and ink_laptop["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}",
|
|
(ink_laptop or {}).get("config", {}).get(LAUNCH, why))
|
|
|
|
print("6. the bar cannot be rearranged by dragging")
|
|
check("sorting is off, which is what refuses a reordering drop",
|
|
every and every["config"].get(
|
|
"org.kde.plasma.icontasks.sortingStrategy") == "0",
|
|
str((every or {}).get("config", {}).get(
|
|
"org.kde.plasma.icontasks.sortingStrategy")))
|
|
check("and launchers stay ahead of running windows",
|
|
every and every["config"].get(
|
|
"org.kde.plasma.icontasks.separateLaunchers") == "true")
|
|
|
|
print("7. a disabled application does not start either")
|
|
# Taking the launcher off the bar is half of it. The command itself has
|
|
# to refuse, so a device that does not offer an application cannot be
|
|
# talked into running it by a stale desktop entry or a technician's
|
|
# muscle memory.
|
|
for script, setting in (("classroom-wonderink", "WONDERINK_ENABLED"),
|
|
("classroom-parent-call", "PARENT_CALL_ENABLED")):
|
|
text = (root / "scripts" / script).read_text()
|
|
check(f"{script} refuses when {setting} is false",
|
|
setting in text and "not starting" in text)
|
|
check(f"{script} defaults to starting, so existing rooms are unaffected",
|
|
'default=True' in text or 'feature(' in text)
|
|
|
|
print()
|
|
print(f"{passed} passed, {failed} failed")
|
|
raise SystemExit(1 if failed else 0)
|
|
PY
|