feat: one setting per application, and never the shape of the machine
A laptop is going in for the women's Bible study, and the obvious way to
build that — a laptop profile that turns off the children's church
applications — is the wrong way. A laptop may well take children's church,
and a tower may well be a Bible study. Hardware cannot be allowed to
decide purpose, so every optional application gets a setting of its own:
WONDERINK_ENABLED already existed; behaviour unchanged
DVD_ENABLED new
PARENT_CALL_ENABLED new
and DEVICE_TYPE says only what the hardware IS. classroom-desktop-layout
never reads it, and there is a test that says so, because this is the rule
that will be under pressure the first time somebody adds a device.
All three default to true, so an existing classroom that has never heard
of any of them comes up exactly as it did before. The settings are
appended to device.conf by the same mechanism every other setting has
used, so an upgrade gains them without losing a byte an administrator
wrote.
The disc player is the one place hardware still votes, and it is a veto:
DVD_ENABLED can take the player away from a machine that has a drive,
nothing can put it on a machine that has none. A button that cannot work
is worse than no button.
The layout's launcher markers are replaced by a feature block of three
plain booleans. The old mechanism commented a line out of an array, which
worked for launchers and could not express Parent Call, because that is a
whole widget rather than one entry. Three booleans express both, and the
launcher list is now built from them in one fixed order, so a disabled
application closes the row up rather than leaving a gap, and the same
enabled set produces the same bar on every machine in the church.
Also here, because it is the bar and not a feature: sortingStrategy=0.
Plasma's default is 1, "manually", and the task manager accepts a
reordering drop only in that mode — MouseHandler.qml tests
tasksModel.sortMode === SortManual before it will move anything. The
classroom never wrote the key, so it had the default, and any teacher
could drag the launchers into a different order. This is the applet's own
lever refusing the drop rather than something of ours putting the bar back
afterwards.
The test now runs the layout the package actually ships, in node, with
Plasma's API stubbed, and asserts the launcher string that comes out.
Asserting against a copy of the file's text in the test was how the old
one worked, and a copy is a thing that drifts.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fc764b0621
commit
7f4a122900
@@ -10,6 +10,23 @@
|
|||||||
// applications, the right is the small amount of status a teacher needs.
|
// applications, the right is the small amount of status a teacher needs.
|
||||||
// Nothing else is on the screen.
|
// Nothing else is on the screen.
|
||||||
|
|
||||||
|
// WHICH APPLICATIONS THIS DEVICE OFFERS.
|
||||||
|
//
|
||||||
|
// classroom-desktop-layout rewrites the three lines between the markers
|
||||||
|
// to match device.conf and this machine's hardware, and nothing else in
|
||||||
|
// this file. Each application is its own line because each is its own
|
||||||
|
// setting: a laptop taking children's church needs Wonder Ink and a tower
|
||||||
|
// used for Bible study does not, so the shape of the machine is never
|
||||||
|
// allowed to decide what it is for.
|
||||||
|
//
|
||||||
|
// All true here is the shipped default, which is what an existing
|
||||||
|
// classroom must come up as.
|
||||||
|
/* CLASSROOM-FEATURES-BEGIN */
|
||||||
|
var WONDERINK = true;
|
||||||
|
var DVD = true;
|
||||||
|
var PARENT_CALL = true;
|
||||||
|
/* CLASSROOM-FEATURES-END */
|
||||||
|
|
||||||
var WALLPAPER = "/usr/share/backgrounds/ekklesia/classroom-calm.png";
|
var WALLPAPER = "/usr/share/backgrounds/ekklesia/classroom-calm.png";
|
||||||
|
|
||||||
// Matches the 76px strip the classroom has always used, so muscle memory
|
// Matches the 76px strip the classroom has always used, so muscle memory
|
||||||
@@ -52,11 +69,12 @@ panel.opacity = "adaptive";
|
|||||||
// lesson's tasks.
|
// lesson's tasks.
|
||||||
//
|
//
|
||||||
// Quick Launch reads the entry live and shows nothing but the button.
|
// Quick Launch reads the entry live and shows nothing but the button.
|
||||||
|
if (PARENT_CALL) {
|
||||||
var parentCall = panel.addWidget("org.kde.plasma.quicklaunch");
|
var parentCall = panel.addWidget("org.kde.plasma.quicklaunch");
|
||||||
parentCall.currentConfigGroup = ["General"];
|
parentCall.currentConfigGroup = ["General"];
|
||||||
// A file URL, not applications:. Quick Launch resolves the entry itself
|
// A file URL, not applications:. Quick Launch resolves the entry
|
||||||
// and does not copy it, so the path is a live reference to what the
|
// itself and does not copy it, so the path is a live reference to
|
||||||
// package installs.
|
// what the package installs.
|
||||||
parentCall.writeConfig(
|
parentCall.writeConfig(
|
||||||
"launcherUrls",
|
"launcherUrls",
|
||||||
"file:///usr/share/applications/ekklesia-parent-call.desktop"
|
"file:///usr/share/applications/ekklesia-parent-call.desktop"
|
||||||
@@ -64,6 +82,7 @@ parentCall.writeConfig(
|
|||||||
parentCall.writeConfig("showLauncherNames", false);
|
parentCall.writeConfig("showLauncherNames", false);
|
||||||
parentCall.writeConfig("enablePopup", false);
|
parentCall.writeConfig("enablePopup", false);
|
||||||
parentCall.writeConfig("maxSectionCount", 1);
|
parentCall.writeConfig("maxSectionCount", 1);
|
||||||
|
}
|
||||||
|
|
||||||
var leftGap = panel.addWidget("org.kde.plasma.panelspacer");
|
var leftGap = panel.addWidget("org.kde.plasma.panelspacer");
|
||||||
leftGap.currentConfigGroup = ["General"];
|
leftGap.currentConfigGroup = ["General"];
|
||||||
@@ -73,21 +92,49 @@ leftGap.writeConfig("expanding", true);
|
|||||||
// story. This applet is the reason the classroom stops writing a dock.
|
// story. This applet is the reason the classroom stops writing a dock.
|
||||||
var tasks = panel.addWidget("org.kde.plasma.icontasks");
|
var tasks = panel.addWidget("org.kde.plasma.icontasks");
|
||||||
tasks.currentConfigGroup = ["General"];
|
tasks.currentConfigGroup = ["General"];
|
||||||
// Two of these three lines are rewritten by classroom-desktop-layout to
|
// THE LAUNCHERS, IN ONE FIXED ORDER.
|
||||||
// match the machine, and each is marked so the edit is a line replacement
|
|
||||||
// and not a parse.
|
|
||||||
//
|
//
|
||||||
// The disc player follows the hardware: a room with no optical drive has
|
// The order is written here and nowhere else, so the same set of enabled
|
||||||
// it removed, so no teacher is offered a player that cannot exist.
|
// applications produces the same bar on every machine in the church.
|
||||||
|
// Anything switched off is left out of the list rather than blanked, so
|
||||||
|
// the remaining launchers close up and there is never a gap where a
|
||||||
|
// disabled application used to be.
|
||||||
//
|
//
|
||||||
// Wonder Ink follows WONDERINK_ENABLED in device.conf: a device that is
|
// The browser is always present. Wonder Ink and the disc player are
|
||||||
// not a classroom — the laptop that goes out with a volunteer — has no
|
// answered above.
|
||||||
// lesson application, and the ordinary browser is unaffected either way.
|
var launchers = [];
|
||||||
tasks.writeConfig("launchers", [
|
|
||||||
/* CLASSROOM-WONDERINK-LAUNCHER */ "applications:ekklesia-wonderink.desktop",
|
if (WONDERINK) {
|
||||||
"applications:google-chrome.desktop",
|
launchers.push("applications:ekklesia-wonderink.desktop");
|
||||||
/* CLASSROOM-DVD-LAUNCHER */ "applications:ekklesia-dvd.desktop"
|
}
|
||||||
].join(","));
|
|
||||||
|
launchers.push("applications:google-chrome.desktop");
|
||||||
|
|
||||||
|
if (DVD) {
|
||||||
|
launchers.push("applications:ekklesia-dvd.desktop");
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.writeConfig("launchers", launchers.join(","));
|
||||||
|
|
||||||
|
// THE ORDER STAYS PUT.
|
||||||
|
//
|
||||||
|
// 0 is "do not sort", and it is what stops a teacher dragging the
|
||||||
|
// launchers into a different order. Plasma's default is 1, "manually",
|
||||||
|
// and manual sorting is the only mode in which the task manager accepts a
|
||||||
|
// reordering drop at all — see MouseHandler.qml, which tests
|
||||||
|
// tasksModel.sortMode === SortManual before it will move anything. The
|
||||||
|
// classroom shipped without this line, so it had Plasma's default and the
|
||||||
|
// bar could be rearranged by anybody who dragged an icon.
|
||||||
|
//
|
||||||
|
// This is the supported lever rather than a patched task manager: the
|
||||||
|
// drop is refused by the applet's own code, not caught afterwards by
|
||||||
|
// something of ours putting the bar back.
|
||||||
|
tasks.writeConfig("sortingStrategy", 0);
|
||||||
|
// Launchers stay together at the start of the row and running windows
|
||||||
|
// appear after them, so opening or closing an application cannot move a
|
||||||
|
// pinned icon. This is Plasma's default; it is written down because the
|
||||||
|
// classroom depends on it.
|
||||||
|
tasks.writeConfig("separateLaunchers", true);
|
||||||
// Every window gets its own button. Teachers should never have to open a
|
// Every window gets its own button. Teachers should never have to open a
|
||||||
// group to find out that a second window exists.
|
// group to find out that a second window exists.
|
||||||
tasks.writeConfig("groupingStrategy", 0);
|
tasks.writeConfig("groupingStrategy", 0);
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -73,6 +73,56 @@ MQTT_PASSWORD=
|
|||||||
# false takes the launcher off the panel and stops the command starting.
|
# false takes the launcher off the panel and stops the command starting.
|
||||||
# The ordinary browser, Parent Call and everything else are untouched.
|
# The ordinary browser, Parent Call and everything else are untouched.
|
||||||
WONDERINK_ENABLED=true
|
WONDERINK_ENABLED=true
|
||||||
|
|
||||||
|
# What kind of machine this is.
|
||||||
|
#
|
||||||
|
# desktop a classroom tower. This is every existing room.
|
||||||
|
# laptop a machine with a lid and an internal panel.
|
||||||
|
#
|
||||||
|
# This says what the hardware IS. It decides nothing about which
|
||||||
|
# applications the device offers: a laptop running children's church needs
|
||||||
|
# Wonder Ink, and a desktop used for Bible study does not. Every
|
||||||
|
# application below is its own setting, on purpose, so that moving a
|
||||||
|
# machine between rooms is a matter of saying what it is for rather than
|
||||||
|
# what shape it is.
|
||||||
|
DEVICE_TYPE=desktop
|
||||||
|
|
||||||
|
# Whether this device offers the disc player.
|
||||||
|
#
|
||||||
|
# The machine is asked as well: a room with no optical drive never shows
|
||||||
|
# the player whatever this says, because a button that cannot work is
|
||||||
|
# worse than no button. This setting is how a device that HAS a drive is
|
||||||
|
# told not to offer it anyway.
|
||||||
|
DVD_ENABLED=true
|
||||||
|
|
||||||
|
# Whether this device offers Parent Call.
|
||||||
|
#
|
||||||
|
# A room full of children needs it. A Bible study does not, and the button
|
||||||
|
# in the corner is then one more thing to explain.
|
||||||
|
PARENT_CALL_ENABLED=true
|
||||||
|
|
||||||
|
# Which network this device is meant to be on.
|
||||||
|
#
|
||||||
|
# wired an Ethernet cable is the intended connection.
|
||||||
|
# wifi this device is meant to be on Wi-Fi. Normal and expected; the
|
||||||
|
# room is not warned about a missing cable.
|
||||||
|
#
|
||||||
|
# This changes what the network indicator offers, never what
|
||||||
|
# NetworkManager is allowed to connect to. Wi-Fi hardware is not disabled
|
||||||
|
# on a wired device, and a wired device is not stopped from falling back.
|
||||||
|
NETWORK_PREFERENCE=wired
|
||||||
|
|
||||||
|
# Which displays this device drives.
|
||||||
|
#
|
||||||
|
# all every connected display, which is what a tower with one
|
||||||
|
# television wants and what every existing room gets.
|
||||||
|
# external-only the external display only, with an internal laptop
|
||||||
|
# panel switched off. For a laptop installed as a fixed
|
||||||
|
# appliance with its lid shut.
|
||||||
|
#
|
||||||
|
# external-only is a setting rather than something inferred from having a
|
||||||
|
# lid, because another laptop may want its own screen or both.
|
||||||
|
DISPLAY_MODE=all
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +177,71 @@ UI_SCALE=1.0
|
|||||||
# The ordinary browser, Parent Call and everything else are untouched.
|
# The ordinary browser, Parent Call and everything else are untouched.
|
||||||
WONDERINK_ENABLED=true
|
WONDERINK_ENABLED=true
|
||||||
''',
|
''',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"DEVICE_TYPE",
|
||||||
|
"""# What kind of machine this is.
|
||||||
|
#
|
||||||
|
# desktop a classroom tower. This is every existing room.
|
||||||
|
# laptop a machine with a lid and an internal panel.
|
||||||
|
#
|
||||||
|
# This says what the hardware IS. It decides nothing about which
|
||||||
|
# applications the device offers: a laptop running children's church needs
|
||||||
|
# Wonder Ink, and a desktop used for Bible study does not. Every
|
||||||
|
# application below is its own setting, on purpose, so that moving a
|
||||||
|
# machine between rooms is a matter of saying what it is for rather than
|
||||||
|
# what shape it is.
|
||||||
|
DEVICE_TYPE=desktop
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"DVD_ENABLED",
|
||||||
|
"""# Whether this device offers the disc player.
|
||||||
|
#
|
||||||
|
# The machine is asked as well: a room with no optical drive never shows
|
||||||
|
# the player whatever this says, because a button that cannot work is
|
||||||
|
# worse than no button. This setting is how a device that HAS a drive is
|
||||||
|
# told not to offer it anyway.
|
||||||
|
DVD_ENABLED=true
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"PARENT_CALL_ENABLED",
|
||||||
|
"""# Whether this device offers Parent Call.
|
||||||
|
#
|
||||||
|
# A room full of children needs it. A Bible study does not, and the button
|
||||||
|
# in the corner is then one more thing to explain.
|
||||||
|
PARENT_CALL_ENABLED=true
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"NETWORK_PREFERENCE",
|
||||||
|
"""# Which network this device is meant to be on.
|
||||||
|
#
|
||||||
|
# wired an Ethernet cable is the intended connection.
|
||||||
|
# wifi this device is meant to be on Wi-Fi. Normal and expected; the
|
||||||
|
# room is not warned about a missing cable.
|
||||||
|
#
|
||||||
|
# This changes what the network indicator offers, never what
|
||||||
|
# NetworkManager is allowed to connect to. Wi-Fi hardware is not disabled
|
||||||
|
# on a wired device, and a wired device is not stopped from falling back.
|
||||||
|
NETWORK_PREFERENCE=wired
|
||||||
|
""",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"DISPLAY_MODE",
|
||||||
|
"""# Which displays this device drives.
|
||||||
|
#
|
||||||
|
# all every connected display, which is what a tower with one
|
||||||
|
# television wants and what every existing room gets.
|
||||||
|
# external-only the external display only, with an internal laptop
|
||||||
|
# panel switched off. For a laptop installed as a fixed
|
||||||
|
# appliance with its lid shut.
|
||||||
|
#
|
||||||
|
# external-only is a setting rather than something inferred from having a
|
||||||
|
# lid, because another laptop may want its own screen or both.
|
||||||
|
DISPLAY_MODE=all
|
||||||
|
""",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"MQTT_ENABLED",
|
"MQTT_ENABLED",
|
||||||
@@ -198,6 +313,52 @@ def ensure_device_config(path=None):
|
|||||||
path.write_text(text + separator + "\n\n".join(additions) + "\n")
|
path.write_text(text + separator + "\n\n".join(additions) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
DEVICE_TYPES = ("desktop", "laptop")
|
||||||
|
NETWORK_PREFERENCES = ("wired", "wifi")
|
||||||
|
DISPLAY_MODES = ("all", "external-only")
|
||||||
|
|
||||||
|
# Every optional application, and what a device that has never heard of
|
||||||
|
# the setting should do. All true: an existing classroom upgrading to this
|
||||||
|
# release must come up exactly as it was, and every existing classroom has
|
||||||
|
# all of these.
|
||||||
|
FEATURES = {
|
||||||
|
"WONDERINK_ENABLED": True,
|
||||||
|
"DVD_ENABLED": True,
|
||||||
|
"PARENT_CALL_ENABLED": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def feature(config, name):
|
||||||
|
"""Whether an optional application is offered on this device.
|
||||||
|
|
||||||
|
Deliberately reads one key per application and never consults
|
||||||
|
DEVICE_TYPE. A laptop taking children's church needs Wonder Ink and a
|
||||||
|
tower used for Bible study does not, so shape cannot be allowed to
|
||||||
|
decide purpose — that rule is the whole reason these are separate
|
||||||
|
settings rather than a profile.
|
||||||
|
"""
|
||||||
|
return enabled(config, name, default=FEATURES.get(name, False))
|
||||||
|
|
||||||
|
|
||||||
|
def _choice(config, key, allowed, default):
|
||||||
|
value = (config.get(key) or "").strip().lower()
|
||||||
|
|
||||||
|
return value if value in allowed else default
|
||||||
|
|
||||||
|
|
||||||
|
def device_type(config):
|
||||||
|
"""What the hardware is. Never what it is for."""
|
||||||
|
return _choice(config, "DEVICE_TYPE", DEVICE_TYPES, "desktop")
|
||||||
|
|
||||||
|
|
||||||
|
def network_preference(config):
|
||||||
|
return _choice(config, "NETWORK_PREFERENCE", NETWORK_PREFERENCES, "wired")
|
||||||
|
|
||||||
|
|
||||||
|
def display_mode(config):
|
||||||
|
return _choice(config, "DISPLAY_MODE", DISPLAY_MODES, "all")
|
||||||
|
|
||||||
|
|
||||||
def ui_scale(config):
|
def ui_scale(config):
|
||||||
"""UI_SCALE, clamped to something a classroom can actually use."""
|
"""UI_SCALE, clamped to something a classroom can actually use."""
|
||||||
raw = config.get("UI_SCALE", "")
|
raw = config.get("UI_SCALE", "")
|
||||||
|
|||||||
@@ -2,29 +2,33 @@
|
|||||||
|
|
||||||
"""Match the classroom's panel to the machine it is on.
|
"""Match the classroom's panel to the machine it is on.
|
||||||
|
|
||||||
Two of the bar's launchers depend on the machine rather than on the
|
Which applications a device offers is decided here, once, and written into
|
||||||
release, and this edits the marked line for each of them in the installed
|
the three feature lines of the installed layout. The panel is then built
|
||||||
layout, and nothing else.
|
from that layout at the next login.
|
||||||
|
|
||||||
the disc player a room with no optical drive should not be offered
|
Each application is its own question, and the shape of the machine is
|
||||||
one. The machine is asked; nothing is remembered.
|
never one of them:
|
||||||
|
|
||||||
Wonder Ink WONDERINK_ENABLED in device.conf. One future device
|
Wonder Ink WONDERINK_ENABLED. A laptop taking children's church
|
||||||
is a laptop that will not have Wonder Ink at all,
|
needs it; a tower used for the women's Bible study does
|
||||||
and a device that is not a classroom should not
|
not. DEVICE_TYPE is not consulted and must not be.
|
||||||
carry a lesson application it cannot use.
|
|
||||||
|
|
||||||
The two are deliberately answered differently. A drive is something a
|
Parent Call PARENT_CALL_ENABLED. Same rule.
|
||||||
machine can be asked about, and asking beats remembering to edit a file
|
|
||||||
when a computer moves between rooms. A licence is not, so it is a
|
the disc DVD_ENABLED *and* a drive being present. The setting can
|
||||||
setting.
|
player take the player away from a machine that has a drive;
|
||||||
|
nothing can put it on a machine that has none, because a
|
||||||
|
button that cannot work is worse than no button.
|
||||||
|
|
||||||
|
That last one is the only place hardware gets a vote, and it can only ever
|
||||||
|
remove a launcher. Everything else is configuration, so that moving a
|
||||||
|
machine between rooms is a matter of saying what it is for.
|
||||||
|
|
||||||
Run before the display manager by ekklesia-classroom-layout.service, and
|
Run before the display manager by ekklesia-classroom-layout.service, and
|
||||||
again by udev when a drive is plugged in or pulled out. That ordering is
|
again by udev when a drive is plugged in or pulled out. That ordering is
|
||||||
the whole point: the panel is built from this layout the moment a session
|
the whole point: the panel is built from this layout the moment a session
|
||||||
starts, and a room that is told about its hardware afterwards keeps the
|
starts, and a room that is told about its hardware afterwards keeps the
|
||||||
wrong bar until the next login. It was being left to the provisioning
|
wrong bar until the next login.
|
||||||
service, which runs at boot alongside the session rather than before it.
|
|
||||||
|
|
||||||
It writes the *layout*, not the running panel. Editing
|
It writes the *layout*, not the running panel. Editing
|
||||||
plasma-org.kde.plasma.desktop-appletsrc underneath a live plasmashell was
|
plasma-org.kde.plasma.desktop-appletsrc underneath a live plasmashell was
|
||||||
@@ -32,7 +36,6 @@ tried and it destroyed the panel: the shell owns that file and rewrites
|
|||||||
it, so an edit races with it. The layout is a file the package owns and
|
it, so an edit races with it. The layout is a file the package owns and
|
||||||
the shell only reads, and classroom-desktop-reset makes the shell read it
|
the shell only reads, and classroom-desktop-reset makes the shell read it
|
||||||
again at the next login.
|
again at the next login.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import glob
|
import glob
|
||||||
@@ -46,18 +49,21 @@ sys.path.insert(
|
|||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"),
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"),
|
||||||
)
|
)
|
||||||
|
|
||||||
from classroom_config import enabled, read_device_config # noqa: E402
|
from classroom_config import feature, read_device_config # noqa: E402
|
||||||
|
|
||||||
LAYOUT = (
|
LAYOUT = (
|
||||||
"/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop"
|
"/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop"
|
||||||
"/contents/layouts/org.kde.plasma.desktop-layout.js"
|
"/contents/layouts/org.kde.plasma.desktop-layout.js"
|
||||||
)
|
)
|
||||||
|
|
||||||
DVD_MARKER = "/* CLASSROOM-DVD-LAUNCHER */"
|
# The variable in the layout, and the setting that answers it.
|
||||||
DVD_ENTRY = "ekklesia-dvd.desktop"
|
FEATURE_LINES = (
|
||||||
|
("WONDERINK", "WONDERINK_ENABLED"),
|
||||||
|
("DVD", "DVD_ENABLED"),
|
||||||
|
("PARENT_CALL", "PARENT_CALL_ENABLED"),
|
||||||
|
)
|
||||||
|
|
||||||
WONDERINK_MARKER = "/* CLASSROOM-WONDERINK-LAUNCHER */"
|
MARKER = "/* CLASSROOM-FEATURES-BEGIN */"
|
||||||
WONDERINK_ENTRY = "ekklesia-wonderink.desktop"
|
|
||||||
|
|
||||||
# The kernel names optical drives sr0, sr1, ... regardless of bus. Both
|
# The kernel names optical drives sr0, sr1, ... regardless of bus. Both
|
||||||
# views are checked: /dev is what anything opening the drive will use, and
|
# views are checked: /dev is what anything opening the drive will use, and
|
||||||
@@ -67,50 +73,52 @@ DRIVE_GLOB = "/dev/sr[0-9]*"
|
|||||||
SYSFS_GLOB = "/sys/block/sr[0-9]*"
|
SYSFS_GLOB = "/sys/block/sr[0-9]*"
|
||||||
|
|
||||||
|
|
||||||
def drives():
|
def say(message):
|
||||||
return sorted(glob.glob(DRIVE_GLOB)), sorted(glob.glob(SYSFS_GLOB))
|
print(f"classroom-desktop-layout: {message}")
|
||||||
|
|
||||||
|
|
||||||
def has_optical_drive():
|
def has_optical_drive():
|
||||||
nodes, blocks = drives()
|
nodes = sorted(glob.glob(DRIVE_GLOB))
|
||||||
|
blocks = sorted(glob.glob(SYSFS_GLOB))
|
||||||
|
|
||||||
# Said out loud every time. When a room shows a disc player it has no
|
# Said out loud every time. When a room shows a disc player it has no
|
||||||
# drive for, or hides one it has, this line in the journal is the
|
# drive for, or hides one it has, this line in the journal is the
|
||||||
# difference between an answer and an afternoon.
|
# difference between an answer and an afternoon.
|
||||||
print("classroom-desktop-layout: /dev "
|
say(f"/dev [{', '.join(nodes) or 'none'}], "
|
||||||
f"[{', '.join(nodes) or 'none'}], /sys/block "
|
f"/sys/block [{', '.join(blocks) or 'none'}]")
|
||||||
f"[{', '.join(blocks) or 'none'}]")
|
|
||||||
|
|
||||||
return bool(nodes and blocks)
|
return bool(nodes and blocks)
|
||||||
|
|
||||||
|
|
||||||
def rewrite(text, marker, entry, wanted):
|
def wanted(config):
|
||||||
"""Comment one marked launcher in or out, idempotently."""
|
"""What each feature line in the layout should say."""
|
||||||
|
answers = {}
|
||||||
|
|
||||||
|
for variable, setting in FEATURE_LINES:
|
||||||
|
answer = feature(config, setting)
|
||||||
|
say(f"{setting} [{config.get(setting, 'unset')}] -> {answer}")
|
||||||
|
answers[variable] = answer
|
||||||
|
|
||||||
|
# A drive is the one thing the machine itself gets a vote on, and it
|
||||||
|
# is a veto: it can only ever take the player away.
|
||||||
|
if answers["DVD"] and not has_optical_drive():
|
||||||
|
say("DVD_ENABLED is true but this machine has no drive")
|
||||||
|
answers["DVD"] = False
|
||||||
|
|
||||||
|
return answers
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite(text, variable, answer):
|
||||||
|
"""Set one `var NAME = true;` line, idempotently."""
|
||||||
line = re.compile(
|
line = re.compile(
|
||||||
r"^(\s*)(//\s*)?(" + re.escape(marker)
|
r"^(\s*var\s+" + re.escape(variable) + r"\s*=\s*)(true|false)(\s*;)",
|
||||||
+ r'\s*"applications:' + re.escape(entry) + r'")',
|
|
||||||
re.M,
|
re.M,
|
||||||
)
|
)
|
||||||
|
|
||||||
def swap(match):
|
return line.sub(
|
||||||
indent, _, body = match.groups()
|
lambda m: f"{m.group(1)}{'true' if answer else 'false'}{m.group(3)}",
|
||||||
return f"{indent}{'' if wanted else '// '}{body}"
|
text,
|
||||||
|
)
|
||||||
return line.sub(swap, text)
|
|
||||||
|
|
||||||
|
|
||||||
def wonderink_wanted():
|
|
||||||
"""WONDERINK_ENABLED, defaulting to true.
|
|
||||||
|
|
||||||
A classroom that has never heard of the setting is a classroom, so the
|
|
||||||
absent case is the one every existing room is in and it has to mean
|
|
||||||
yes. Said out loud for the same reason the drive is.
|
|
||||||
"""
|
|
||||||
config = read_device_config()
|
|
||||||
wanted = enabled(config, "WONDERINK_ENABLED", default=True)
|
|
||||||
print(f"classroom-desktop-layout: WONDERINK_ENABLED "
|
|
||||||
f"[{config.get('WONDERINK_ENABLED', 'unset')}]")
|
|
||||||
return wanted
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -123,24 +131,18 @@ def main():
|
|||||||
print(f"classroom-desktop-layout: {error}", file=sys.stderr)
|
print(f"classroom-desktop-layout: {error}", file=sys.stderr)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if DVD_MARKER not in before:
|
if MARKER not in before:
|
||||||
print("classroom-desktop-layout: no DVD marker in the layout",
|
print("classroom-desktop-layout: no feature block in the layout",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
after = rewrite(before, DVD_MARKER, DVD_ENTRY, has_optical_drive())
|
after = before
|
||||||
|
|
||||||
if WONDERINK_MARKER in before:
|
for variable, answer in wanted(read_device_config()).items():
|
||||||
after = rewrite(
|
after = rewrite(after, variable, answer)
|
||||||
after, WONDERINK_MARKER, WONDERINK_ENTRY, wonderink_wanted()
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print("classroom-desktop-layout: no Wonder Ink marker in the layout",
|
|
||||||
file=sys.stderr)
|
|
||||||
|
|
||||||
if after == before:
|
if after == before:
|
||||||
print("classroom-desktop-layout: the layout already matches "
|
say("the layout already matches this machine")
|
||||||
"this machine")
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# Replace the file rather than edit in place, so a full disk or a
|
# Replace the file rather than edit in place, so a full disk or a
|
||||||
@@ -155,7 +157,7 @@ def main():
|
|||||||
print(f"classroom-desktop-layout: {error}", file=sys.stderr)
|
print(f"classroom-desktop-layout: {error}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print("classroom-desktop-layout: the layout now matches this machine")
|
say("the layout now matches this machine")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -166,13 +166,17 @@ fi
|
|||||||
|
|
||||||
# THE DISC PLAYER, AND WHETHER THIS MACHINE HAS ONE.
|
# THE DISC PLAYER, AND WHETHER THIS MACHINE HAS ONE.
|
||||||
#
|
#
|
||||||
# The panel about to be built comes from the packaged layout, and the one
|
# The panel about to be built comes from the packaged layout, and the
|
||||||
# line in it that depends on hardware is written by
|
# feature lines in it are written by ekklesia-classroom-layout.service
|
||||||
# ekklesia-classroom-layout.service before the display manager starts.
|
# before the display manager starts. This cannot write /usr and does not
|
||||||
# This cannot write /usr and does not try. It checks, because a room
|
# try. It checks one direction only: a layout that offers a player on a
|
||||||
# showing a disc player it has no drive for is a teacher clicking a button
|
# machine with no drive is a teacher clicking a button that can only
|
||||||
# that can only disappoint, and the journal should say so at the moment
|
# disappoint, and the journal should say so at the moment the panel is
|
||||||
# the panel is built rather than after somebody notices.
|
# built rather than after somebody notices.
|
||||||
|
#
|
||||||
|
# The other direction is not a fault. A machine with a drive and
|
||||||
|
# DVD_ENABLED=false is a device that has been told not to offer the
|
||||||
|
# player, which is a supported thing to want.
|
||||||
LAYOUT=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js
|
LAYOUT=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js
|
||||||
|
|
||||||
if [[ -r "$LAYOUT" ]]; then
|
if [[ -r "$LAYOUT" ]]; then
|
||||||
@@ -182,15 +186,15 @@ if [[ -r "$LAYOUT" ]]; then
|
|||||||
drive=no
|
drive=no
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -q '^\s*/\* CLASSROOM-DVD-LAUNCHER \*/' "$LAYOUT"; then
|
if grep -qE '^\s*var DVD = true;' "$LAYOUT"; then
|
||||||
offered=yes
|
offered=yes
|
||||||
else
|
else
|
||||||
offered=no
|
offered=no
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$drive" != "$offered" ]]; then
|
if [[ "$offered" == yes && "$drive" == no ]]; then
|
||||||
say "WARNING: this machine has drive=$drive but the layout offers"
|
say "WARNING: the layout offers the disc player but this machine"
|
||||||
say " the disc player=$offered — run classroom-desktop-layout"
|
say " has no drive — run classroom-desktop-layout"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,19 @@ def check(label, condition, detail=""):
|
|||||||
print("1. the room no longer keeps a list of dock buttons")
|
print("1. the room no longer keeps a list of dock buttons")
|
||||||
check("dock_items is gone with the dock it described",
|
check("dock_items is gone with the dock it described",
|
||||||
not hasattr(__import__("classroom_config"), "dock_items"))
|
not hasattr(__import__("classroom_config"), "dock_items"))
|
||||||
check("and DVD_ENABLED with it, because the machine can be asked",
|
# DVD_ENABLED is back, and it is not the setting that was removed with the
|
||||||
"DVD_ENABLED" not in __import__("classroom_config").DEVICE_CONFIG_TEMPLATE)
|
# dock. That one was the whole answer; this one is a veto an administrator
|
||||||
|
# holds over a machine that HAS a drive, and the hardware is still asked.
|
||||||
|
# A Bible study on a tower with a drive in it does not want a disc player.
|
||||||
|
check("DVD_ENABLED is a setting again, alongside asking the machine",
|
||||||
|
"DVD_ENABLED" in __import__("classroom_config").DEVICE_CONFIG_TEMPLATE)
|
||||||
|
check("and every optional application has one of its own",
|
||||||
|
all(key in __import__("classroom_config").DEVICE_CONFIG_TEMPLATE
|
||||||
|
for key in ("WONDERINK_ENABLED", "DVD_ENABLED",
|
||||||
|
"PARENT_CALL_ENABLED")))
|
||||||
|
check("while DEVICE_TYPE decides none of them",
|
||||||
|
__import__("classroom_config").feature(
|
||||||
|
{"DEVICE_TYPE": "laptop"}, "WONDERINK_ENABLED") is True)
|
||||||
|
|
||||||
print("2. fresh device configuration")
|
print("2. fresh device configuration")
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
#!/bin/bash
|
#!/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
|
set -uo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from importlib.machinery import SourceFileLoader
|
from importlib.machinery import SourceFileLoader
|
||||||
@@ -16,6 +27,8 @@ spec = importlib.util.spec_from_loader("layout", loader)
|
|||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
loader.exec_module(module)
|
loader.exec_module(module)
|
||||||
|
|
||||||
|
LAYOUT = root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js"
|
||||||
|
|
||||||
passed = failed = 0
|
passed = failed = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -31,115 +44,172 @@ def check(label, condition, detail=""):
|
|||||||
failed += 1
|
failed += 1
|
||||||
|
|
||||||
|
|
||||||
WITH = '''tasks.writeConfig("launchers", [
|
BLOCK = """/* CLASSROOM-FEATURES-BEGIN */
|
||||||
/* CLASSROOM-WONDERINK-LAUNCHER */ "applications:ekklesia-wonderink.desktop",
|
var WONDERINK = true;
|
||||||
"applications:google-chrome.desktop",
|
var DVD = true;
|
||||||
/* CLASSROOM-DVD-LAUNCHER */ "applications:ekklesia-dvd.desktop"
|
var PARENT_CALL = true;
|
||||||
].join(","));'''
|
/* CLASSROOM-FEATURES-END */"""
|
||||||
|
|
||||||
|
|
||||||
def dvd(text, wanted):
|
print("1. one line per application, rewritten in place")
|
||||||
return module.rewrite(text, module.DVD_MARKER, module.DVD_ENTRY, wanted)
|
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))
|
||||||
|
|
||||||
|
|
||||||
def ink(text, wanted):
|
print("2. the shape of the machine never decides what it is for")
|
||||||
return module.rewrite(
|
# This is the architecture rule, and it is the one worth a test: a laptop
|
||||||
text, module.WONDERINK_MARKER, module.WONDERINK_ENTRY, wanted
|
# 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), ""
|
||||||
|
|
||||||
|
|
||||||
WITHOUT = WITH.replace(' /* CLASSROOM-DVD-LAUNCHER */',
|
if shutil.which("node") is None:
|
||||||
' // /* CLASSROOM-DVD-LAUNCHER */')
|
print(" SKIP node is not installed; the layout was not evaluated")
|
||||||
NO_INK = WITH.replace(' /* CLASSROOM-WONDERINK-LAUNCHER */',
|
else:
|
||||||
' // /* CLASSROOM-WONDERINK-LAUNCHER */')
|
LAUNCH = "org.kde.plasma.icontasks.launchers"
|
||||||
|
INK = "applications:ekklesia-wonderink.desktop"
|
||||||
|
WEB = "applications:google-chrome.desktop"
|
||||||
|
DISC = "applications:ekklesia-dvd.desktop"
|
||||||
|
|
||||||
print("1. the DVD launcher follows the hardware")
|
every, why = bar({})
|
||||||
check("a machine with a drive keeps it", dvd(WITH, True) == WITH)
|
check("a classroom gets Wonder Ink, the browser and the player, in order",
|
||||||
check("a machine without a drive loses it",
|
every and every["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}",
|
||||||
dvd(WITH, False) == WITHOUT, dvd(WITH, False))
|
(every or {}).get("config", {}).get(LAUNCH, why))
|
||||||
check("and gets it back when a drive appears", dvd(WITHOUT, True) == WITH)
|
check("and Parent Call in the corner",
|
||||||
check("removing twice changes nothing the second time",
|
every and "org.kde.plasma.quicklaunch" in every["widgets"])
|
||||||
dvd(dvd(WITH, False), False) == WITHOUT)
|
|
||||||
check("adding twice changes nothing the second time",
|
|
||||||
dvd(dvd(WITHOUT, True), True) == WITH)
|
|
||||||
|
|
||||||
print()
|
no_ink, why = bar({"WONDERINK_ENABLED": "false"})
|
||||||
print("2. Wonder Ink follows the device setting")
|
check("a Bible study desktop has the browser first and no gap",
|
||||||
check("a device with Wonder Ink keeps it", ink(WITH, True) == WITH)
|
no_ink and no_ink["config"].get(LAUNCH) == f"{WEB},{DISC}",
|
||||||
check("a device without it loses the launcher",
|
(no_ink or {}).get("config", {}).get(LAUNCH, why))
|
||||||
ink(WITH, False) == NO_INK, ink(WITH, False))
|
|
||||||
check("and gets it back when the setting changes",
|
|
||||||
ink(NO_INK, True) == WITH)
|
|
||||||
check("turning it off twice changes nothing the second time",
|
|
||||||
ink(ink(WITH, False), False) == NO_INK)
|
|
||||||
# The line carries a trailing comma, so a comment that stopped short of it
|
|
||||||
# would leave the array with a hole in it.
|
|
||||||
check("the comma goes with the line",
|
|
||||||
'// /* CLASSROOM-WONDERINK-LAUNCHER */ "applications:'
|
|
||||||
'ekklesia-wonderink.desktop",' in ink(WITH, False))
|
|
||||||
check("the browser is untouched either way",
|
|
||||||
"applications:google-chrome.desktop" in ink(WITH, False))
|
|
||||||
check("and so is the disc player",
|
|
||||||
'CLASSROOM-DVD-LAUNCHER */ "applications:ekklesia-dvd.desktop"'
|
|
||||||
in ink(WITH, False))
|
|
||||||
|
|
||||||
print()
|
module.has_optical_drive = lambda: False
|
||||||
print("3. the two settings are independent")
|
bible, why = bar({"WONDERINK_ENABLED": "false",
|
||||||
neither = ink(dvd(WITH, False), False)
|
"PARENT_CALL_ENABLED": "false",
|
||||||
check("a laptop with no drive and no Wonder Ink keeps only the browser",
|
"DEVICE_TYPE": "laptop"})
|
||||||
neither.count("// /* CLASSROOM") == 2
|
check("the laptop's bar is the browser alone",
|
||||||
and "applications:google-chrome.desktop" in neither)
|
bible and bible["config"].get(LAUNCH) == WEB,
|
||||||
check("and comes all the way back",
|
(bible or {}).get("config", {}).get(LAUNCH, why))
|
||||||
ink(dvd(neither, True), True) == WITH)
|
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"])
|
||||||
|
|
||||||
print()
|
module.has_optical_drive = lambda: True
|
||||||
print("4. nothing else in the layout is touched")
|
ink_laptop, why = bar({"DEVICE_TYPE": "laptop"})
|
||||||
other = WITH + "\npanel.addWidget(\"org.kde.plasma.digitalclock\");\n"
|
check("a laptop taking children's church gets the same bar as a tower",
|
||||||
check("a line without the marker is left alone",
|
ink_laptop and ink_laptop["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}",
|
||||||
dvd(other, False).endswith('digitalclock");\n'))
|
(ink_laptop or {}).get("config", {}).get(LAUNCH, why))
|
||||||
check("the other launchers survive",
|
|
||||||
"ekklesia-wonderink.desktop" in dvd(WITH, False)
|
|
||||||
and "google-chrome.desktop" in dvd(WITH, False))
|
|
||||||
|
|
||||||
print()
|
print("6. the bar cannot be rearranged by dragging")
|
||||||
print("5. the real layout carries both markers")
|
check("sorting is off, which is what refuses a reordering drop",
|
||||||
layout = (root / "config/plasma/lookandfeel/contents/layouts"
|
every and every["config"].get(
|
||||||
/ "org.kde.plasma.desktop-layout.js").read_text()
|
"org.kde.plasma.icontasks.sortingStrategy") == "0",
|
||||||
check("exactly one DVD marker on a launcher line",
|
str((every or {}).get("config", {}).get(
|
||||||
layout.count(module.DVD_MARKER + ' "applications:') == 1)
|
"org.kde.plasma.icontasks.sortingStrategy")))
|
||||||
check("exactly one Wonder Ink marker on a launcher line",
|
check("and launchers stay ahead of running windows",
|
||||||
layout.count(module.WONDERINK_MARKER + ' "applications:') == 1)
|
every and every["config"].get(
|
||||||
check("the shipped layout is one a drive-less machine can be given",
|
"org.kde.plasma.icontasks.separateLaunchers") == "true")
|
||||||
dvd(layout, False) != layout)
|
|
||||||
check("and one a Wonder-Ink-less device can be given",
|
|
||||||
ink(layout, False) != layout)
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("6. the setting defaults to yes")
|
|
||||||
check("an unset WONDERINK_ENABLED means the room has Wonder Ink",
|
|
||||||
module.enabled({}, "WONDERINK_ENABLED", default=True))
|
|
||||||
for value in ("true", "1", "yes", "on"):
|
|
||||||
check(f"{value} is on",
|
|
||||||
module.enabled({"WONDERINK_ENABLED": value},
|
|
||||||
"WONDERINK_ENABLED", default=True))
|
|
||||||
for value in ("false", "0", "no", "off", "banana"):
|
|
||||||
check(f"{value} is off",
|
|
||||||
not module.enabled({"WONDERINK_ENABLED": value},
|
|
||||||
"WONDERINK_ENABLED", default=True))
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("7. a missing or unmarked layout is not an error")
|
|
||||||
with tempfile.TemporaryDirectory() as work:
|
|
||||||
empty = Path(work) / "none.js"
|
|
||||||
check("a layout that does not exist exits cleanly",
|
|
||||||
module.main.__code__ is not None)
|
|
||||||
unmarked = Path(work) / "plain.js"
|
|
||||||
unmarked.write_text("panel.addWidget('x');\n")
|
|
||||||
sys.argv = ["classroom-desktop-layout", str(unmarked)]
|
|
||||||
check("an unmarked layout is left alone", module.main() == 0)
|
|
||||||
check("and unchanged", unmarked.read_text() == "panel.addWidget('x');\n")
|
|
||||||
sys.argv = ["classroom-desktop-layout", str(Path(work) / "missing.js")]
|
|
||||||
check("a missing layout is not a failure", module.main() == 0)
|
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"{passed} passed, {failed} failed")
|
print(f"{passed} passed, {failed} failed")
|
||||||
|
|||||||
@@ -343,8 +343,13 @@ check("started at every boot", "WantedBy=graphical.target" in layout_unit)
|
|||||||
check("enabled by the package", "enable ekklesia-classroom-layout.service" in postinst)
|
check("enabled by the package", "enable ekklesia-classroom-layout.service" in postinst)
|
||||||
check("a drive plugged in later is noticed", 'ENV{ID_CDROM}=="1"' in udev)
|
check("a drive plugged in later is noticed", 'ENV{ID_CDROM}=="1"' in udev)
|
||||||
check("and the rules are reloaded on install", "udevadm control --reload-rules" in postinst)
|
check("and the rules are reloaded on install", "udevadm control --reload-rules" in postinst)
|
||||||
check("the login says so when the layout and the hardware disagree",
|
# One direction only. A layout offering a player on a machine with no
|
||||||
"the disc player=" in reset)
|
# drive is a button that cannot work; a machine with a drive and
|
||||||
|
# DVD_ENABLED=false is an administrator's choice and not a fault.
|
||||||
|
check("the login says so when a player is offered with no drive",
|
||||||
|
"has no drive" in reset)
|
||||||
|
check("and reads the layout's own feature line to decide",
|
||||||
|
"var DVD = true" in reset)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("7c. the room's own popups")
|
print("7c. the room's own popups")
|
||||||
|
|||||||
Reference in New Issue
Block a user