Files
Classroom_Desktop/scripts/classroom-display-mode
T
ekklesiaandClaude Opus 5 52301084a5 feat: an external-only display mode, chosen rather than inferred
The women's Bible study laptop is a fixed appliance: AC power, battery
out, lid shut, one television on HDMI. Its own panel should be off.

DISPLAY_MODE=external-only says so, and it is a setting rather than
something inferred from having a lid, because another laptop may want its
own screen or both. Nothing reads DEVICE_TYPE to decide it.

The built-in panel is identified by kscreen's output TYPE — 7 is Panel —
rather than by connector name. Matching "eDP-1" would be matching one
machine's spelling: eDP, LVDS and DSI are all internal, numbered
differently per driver and renamed between kernel versions. The names are
kept only as a fallback for a driver that reports no type, which some
virtual devices do.

One rule overrides the setting: never leave the machine with no screen. A
laptop asked for external-only whose television is off keeps its own
panel and says so in the journal. A machine nobody can see is one nobody
can fix standing in front of it, and it corrects itself the next time
this runs with the set on. Only the outputs that are not already right
are touched, so a television power cycle is not a stream of mode sets.

Applied at login from an autostart entry named to sort before the scale
one, so the scale lands on the displays this leaves enabled.

The lid itself needed no new code — logind ignores every lid event,
PowerDevil's lidAction is 0 in all three profiles with
triggerLidActionWhenExternalMonitorPresent=false, the profiles are
immutable and the classroom account cannot reach suspend through polkit.
That was all already true for towers. What was missing was a test saying
so, and there is one now, because it is the property the laptop depends
on and nothing would have noticed it regressing.

KNOWN LIMITATION, and it is written into docs/testing.md rather than left
to be discovered: the mode is applied at login, not continuously. If KWin
re-enables the internal panel when the television is power cycled, it
stays enabled until the next login. The lab has no lid and no HDMI
hotplug, so this cannot be settled here; the physical procedure says to
check it specifically, because the answer decides whether the follow-up
needs an event-driven applier like the audio policy's.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:17:06 -05:00

131 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Put this machine's displays the way device.conf asks.
The decision is in classroom_display; this applies it through Plasma's
own tool, which is the same way classroom-scale applies UI_SCALE.
DISPLAY_MODE:
all every connected display. Every existing classroom.
external-only the external display, with the built-in panel off.
Run at login from an XDG autostart entry, before classroom-scale, so that
the scale is applied to the displays this leaves enabled rather than to a
panel that is about to go off.
It never leaves the machine with no screen: a laptop asked for
external-only whose television is off keeps its own panel and says so.
That is a deliberate refusal to obey — a machine nobody can see is one
nobody can fix standing in front of it — and it corrects itself the next
time this runs with the television on.
"""
import json
import os
import pwd
import subprocess
import sys
sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
sys.path.insert(
0,
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"),
)
import classroom_display as policy # noqa: E402
from classroom_config import display_mode, read_device_config # noqa: E402
def say(message):
print(f"classroom-display-mode: {message}", flush=True)
def kscreen(*args):
try:
return subprocess.run(
["kscreen-doctor"] + list(args),
capture_output=True, text=True, timeout=30, check=False,
)
except (OSError, subprocess.SubprocessError) as error:
say(str(error))
return None
def outputs():
result = kscreen("-j")
if result is None or result.returncode != 0:
say("could not read the displays")
return []
try:
return json.loads(result.stdout or "{}").get("outputs") or []
except ValueError as error:
say(f"unreadable display list: {error}")
return []
def main():
if pwd.getpwuid(os.getuid()).pw_name != "classroom":
return 0
mode = display_mode(read_device_config())
found = outputs()
if not found:
say("no displays")
return 1
described = ", ".join(
f"{item.get('name')}"
f"{' (built-in)' if policy.is_internal(item) else ''}"
f"{'' if item.get('enabled', True) else ' [off]'}"
for item in policy.connected(found)
)
say(f"DISPLAY_MODE={mode}; connected: {described or 'none'}")
enable, disable = policy.plan(found, mode)
if mode == policy.EXTERNAL_ONLY and not disable and not any(
policy.is_internal(item) for item in policy.connected(found)):
say("external-only, and there is no built-in panel to switch off")
elif mode == policy.EXTERNAL_ONLY and all(
policy.is_internal(item) for item in policy.connected(found)):
say("external-only asked for, but the only display is the built-in "
"panel — leaving it on rather than blinding the machine")
enable, disable = policy.changes(found, mode)
if not enable and not disable:
say("the displays already match")
return 0
failed = False
# Enable first. Switching the panel off before the television is on
# would leave a moment with no output at all.
for name in enable:
result = kscreen(f"output.{name}.enable")
if result is None or result.returncode != 0:
failed = True
say(f"{name}: could not enable")
else:
say(f"{name} on")
for name in disable:
result = kscreen(f"output.{name}.disable")
if result is None or result.returncode != 0:
failed = True
say(f"{name}: could not disable")
else:
say(f"{name} off")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())