Files
Classroom_Desktop/lib/classroom_display.py
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

97 lines
3.1 KiB
Python

"""Deciding which displays a classroom machine drives.
The decision only, so it can be tested against the arrangement a real
machine is in rather than against a running compositor.
The laptop that prompted this is a fixed appliance: on AC, battery out,
lid shut, one external television on HDMI. Its internal panel should be
off. But external-only is a SETTING and not a property of laptops —
another laptop may want its own screen, or both — so nothing here asks
what shape the machine is.
IDENTIFYING THE INTERNAL PANEL.
By kscreen's own output type rather than by connector name. KScreen
reports an integer from KScreen::Output::Type, and 7 is Panel: the
built-in screen, whatever the connector happens to be called on that
hardware. 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 still checked, but only as a fallback for a driver that
reports type Unknown, which some virtual and older devices do.
"""
# KScreen::Output::Type. Only the two this has to tell apart are named.
TYPE_PANEL = 7
# Fallback for a driver that reports no type at all.
INTERNAL_NAMES = ("edp", "lvds", "dsi", "emb")
ALL = "all"
EXTERNAL_ONLY = "external-only"
def is_internal(output):
"""Whether this is the machine's own built-in screen."""
if output.get("type") == TYPE_PANEL:
return True
if output.get("type") not in (None, 0):
return False
name = (output.get("name") or "").lower()
return any(name.startswith(prefix) for prefix in INTERNAL_NAMES)
def connected(outputs):
return [item for item in outputs or [] if item.get("connected", True)]
def plan(outputs, mode=ALL):
"""Which connected outputs to enable and which to disable.
Returns (enable, disable) as lists of names.
The one rule that overrides the setting: never leave the machine with
no screen at all. A laptop asked for external-only whose television is
switched off keeps its own panel, because a machine nobody can see is
a machine nobody can fix standing in front of it. It is corrected the
next time this runs with the television on.
"""
here = connected(outputs)
if not here:
return [], []
internal = [item for item in here if is_internal(item)]
external = [item for item in here if not is_internal(item)]
if mode != EXTERNAL_ONLY or not external:
return [item.get("name") for item in here], []
return (
[item.get("name") for item in external],
[item.get("name") for item in internal],
)
def changes(outputs, mode=ALL):
"""The subset of plan() that is not already true.
Applying only what differs is what keeps a television power cycle from
becoming a stream of mode sets, and keeps this quiet in the journal
when there is nothing to do.
"""
enable, disable = plan(outputs, mode)
state = {
item.get("name"): item.get("enabled", True)
for item in connected(outputs)
}
return (
[name for name in enable if not state.get(name, False)],
[name for name in disable if state.get(name, False)],
)