The DVD player was the only thing still speaking a compositor the room no longer runs. It asked sway's tree whether VLC's window had really gone fullscreen, because VLC reports playing while its window is still the black startup one the loading cover exists to hide. VLC's own status was measured as a replacement and is not one: it reports fullscreen true in the same 250ms poll as playing, because the flag is the option it was started with and not a report about its window. So the cover is held for a floor and capped by the deadline that was already there, and the compositor is not asked anything. Focus-or-launch went the same way. The session already takes an exclusive lock, so a second one cannot start, and bringing an existing one to the front is the panel's job. classroom-window, classroom_sway and classroom_chrome are gone with it — the last of them was reachable from nothing at all. DVD also moves out of the status row, where it sat beside the clock as though a disc were a machine reading. It is an application: it is in the row with Wonder Ink and the browser, and classroom-provision removes it from the layout on a machine with no optical drive, which retires DVD_ENABLED. A computer that moves between rooms takes its drive with it; nobody remembers to edit a file when it does. The panel is rebuilt from the package at every login now. Plasma keeps whatever panel it first built, so an update could change the bar in the package and never change it in a room — measured, with an icon that outlived the file it came from. Editing the live panel configuration was tried instead and destroyed the panel, because the shell owns that file. Removing it before the shell starts is safe and makes the desktop stateless. Co-Authored-By: Claude Opus 5 <[email protected]>
190 lines
5.6 KiB
Python
190 lines
5.6 KiB
Python
"""Device configuration for the Ekklesia Classroom shell.
|
|
|
|
Everything that has to agree about /etc/ekklesia-classroom/device.conf
|
|
reads it through here, so the dock, the notification daemon and the DVD
|
|
commands cannot drift apart over what a setting means.
|
|
"""
|
|
|
|
import math
|
|
from pathlib import Path
|
|
|
|
CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf")
|
|
|
|
TRUTHY = {"1", "true", "yes", "on", "enabled"}
|
|
|
|
# The dock is tuned for a 1080p classroom TV at 1.0. Smaller rooms with a
|
|
# 32" panel sit near the bottom of this range, a 65" set near the top.
|
|
UI_SCALE_DEFAULT = 1.0
|
|
UI_SCALE_MIN = 0.8
|
|
UI_SCALE_MAX = 1.5
|
|
|
|
|
|
DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration
|
|
|
|
UPDATE_URL=https://updates.riverofglorymn.org
|
|
CHANNEL=stable
|
|
|
|
# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
|
|
# implemented yet; this only controls whether the current placeholder appears.
|
|
LIVE_ENABLED=false
|
|
|
|
# Which output the room plays through.
|
|
#
|
|
# auto prefer a connected HDMI or DisplayPort television, and fall
|
|
# back to the analog jack. This is right for almost every room.
|
|
# hdmi always the television, even if it does not report itself.
|
|
# analog always the jack.
|
|
#
|
|
# auto re-decides whenever the hardware changes, so a television switched
|
|
# on after the computer is picked up without anybody touching anything,
|
|
# and sound that is already playing is moved across to it.
|
|
AUDIO_OUTPUT=auto
|
|
|
|
# Size of the dock, clock and notifications, 0.8 to 1.5.
|
|
# Larger rooms and larger TVs want a larger number.
|
|
UI_SCALE=1.0
|
|
|
|
# Church message broker. The room is this machine's hostname unless ROOM
|
|
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
|
MQTT_ENABLED=false
|
|
MQTT_HOST=
|
|
MQTT_PORT=1883
|
|
MQTT_USERNAME=
|
|
MQTT_PASSWORD=
|
|
"""
|
|
|
|
|
|
# Sections added after the first classroom release. Keeping them separate lets
|
|
# an upgrade make a setting discoverable without replacing anything an
|
|
# administrator has already put in device.conf.
|
|
DEVICE_CONFIG_SECTIONS = (
|
|
(
|
|
"LIVE_ENABLED",
|
|
"""# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
|
|
# implemented yet; this only controls whether the current placeholder appears.
|
|
LIVE_ENABLED=false
|
|
""",
|
|
),
|
|
(
|
|
"AUDIO_OUTPUT",
|
|
"""# Which output the room plays through.
|
|
#
|
|
# auto prefer a connected HDMI or DisplayPort television, and fall
|
|
# back to the analog jack. This is right for almost every room.
|
|
# hdmi always the television, even if it does not report itself.
|
|
# analog always the jack.
|
|
#
|
|
# auto re-decides whenever the hardware changes, so a television switched
|
|
# on after the computer is picked up without anybody touching anything,
|
|
# and sound that is already playing is moved across to it.
|
|
AUDIO_OUTPUT=auto
|
|
""",
|
|
),
|
|
(
|
|
"UI_SCALE",
|
|
"""# Size of the dock, clock and notifications, 0.8 to 1.5.
|
|
# Larger rooms and larger TVs want a larger number.
|
|
UI_SCALE=1.0
|
|
""",
|
|
),
|
|
(
|
|
"MQTT_ENABLED",
|
|
"""# Church message broker. The room is this machine's hostname unless ROOM
|
|
# is set. Leave MQTT_ENABLED=false until a broker exists.
|
|
MQTT_ENABLED=false
|
|
MQTT_HOST=
|
|
MQTT_PORT=1883
|
|
MQTT_USERNAME=
|
|
MQTT_PASSWORD=
|
|
""",
|
|
),
|
|
)
|
|
|
|
|
|
def read_device_config(path=None):
|
|
config = {}
|
|
path = Path(path) if path is not None else CONFIG_FILE
|
|
|
|
if not path.exists():
|
|
return config
|
|
|
|
for raw_line in path.read_text().splitlines():
|
|
line = raw_line.strip()
|
|
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
|
|
key, value = line.split("=", 1)
|
|
config[key.strip()] = value.strip()
|
|
|
|
return config
|
|
|
|
|
|
def enabled(config, key, default=False):
|
|
if key not in config:
|
|
return default
|
|
|
|
return config[key].lower() in TRUTHY
|
|
|
|
|
|
def ensure_device_config(path=None):
|
|
"""Create device.conf or append any missing settings exactly once.
|
|
|
|
Existing bytes are otherwise left alone: per-room settings, comments and
|
|
even keys this release does not know about all survive an upgrade.
|
|
"""
|
|
path = Path(path) if path is not None else CONFIG_FILE
|
|
|
|
if not path.exists():
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(DEVICE_CONFIG_TEMPLATE)
|
|
return
|
|
|
|
text = path.read_text()
|
|
config = read_device_config(path)
|
|
additions = []
|
|
|
|
for key, section in DEVICE_CONFIG_SECTIONS:
|
|
if key not in config:
|
|
additions.append(section.rstrip("\n"))
|
|
|
|
if not additions:
|
|
return
|
|
|
|
separator = "" if not text or text.endswith("\n") else "\n"
|
|
path.write_text(text + separator + "\n\n".join(additions) + "\n")
|
|
|
|
|
|
def ui_scale(config):
|
|
"""UI_SCALE, clamped to something a classroom can actually use."""
|
|
raw = config.get("UI_SCALE", "")
|
|
|
|
try:
|
|
value = float(raw)
|
|
except (TypeError, ValueError):
|
|
return UI_SCALE_DEFAULT
|
|
|
|
if not math.isfinite(value):
|
|
return UI_SCALE_DEFAULT
|
|
|
|
return min(max(value, UI_SCALE_MIN), UI_SCALE_MAX)
|
|
|
|
|
|
# The scale the classroom's own GTK surfaces render at.
|
|
#
|
|
# UI_SCALE is applied by sway to the whole output (see classroom-scale),
|
|
# which scales Chrome, VLC and every other application along with the
|
|
# dock. Our surfaces are measured in the same logical pixels sway is
|
|
# already scaling, so scaling them here as well would apply UI_SCALE
|
|
# twice and the dock would come out half again too big.
|
|
#
|
|
# This is the seam: if compositor scaling ever has to be abandoned,
|
|
# changing this back to ui_scale(config) restores per-surface scaling
|
|
# without touching the templates.
|
|
SHELL_SCALE = 1.0
|
|
|
|
|
|
def scaled(value, scale):
|
|
"""A pixel measurement at the given scale, never smaller than 1."""
|
|
return max(1, int(round(value * scale)))
|