#!/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())