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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
886f8a70d2
commit
52301084a5
@@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Classroom displays
|
||||
# Named to sort before ekklesia-classroom-scale.desktop, which is how XDG
|
||||
# autostart entries are ordered in practice. It matters only a little:
|
||||
# this decides WHICH displays are on and the scale applies to whatever is
|
||||
# on, so running them the other way round costs one login before a newly
|
||||
# enabled display is scaled, not a broken room.
|
||||
Exec=/usr/local/bin/classroom-display-mode
|
||||
OnlyShowIn=KDE;
|
||||
NoDisplay=true
|
||||
@@ -230,6 +230,82 @@ If step 7 fails, the jack change is not reaching PipeWire at all; capture
|
||||
`pactl subscribe` and `classroom-audio show` with the set off and then on,
|
||||
which is what distinguishes that from a policy fault.
|
||||
|
||||
## Device features
|
||||
|
||||
Every optional application is its own setting in device.conf, and
|
||||
`DEVICE_TYPE` decides none of them. A laptop can take children's church
|
||||
and a tower can be a Bible study.
|
||||
|
||||
```
|
||||
DEVICE_TYPE=desktop|laptop what the hardware IS
|
||||
WONDERINK_ENABLED=true|false
|
||||
DVD_ENABLED=true|false a drive is still required as well
|
||||
PARENT_CALL_ENABLED=true|false
|
||||
NETWORK_PREFERENCE=wired|wifi
|
||||
DISPLAY_MODE=all|external-only
|
||||
```
|
||||
|
||||
All default to the classroom's existing behaviour, so an upgrade changes
|
||||
nothing until somebody edits the file. `classroom-desktop-layout` writes
|
||||
the answers into the packaged layout; the bar is rebuilt from it at the
|
||||
next login.
|
||||
|
||||
- [ ] `tests/test-classroom-desktop-layout.sh` passes (also run by the build)
|
||||
- [ ] After changing a setting, `classroom-desktop-layout` then a
|
||||
logout/login shows the new bar
|
||||
- [ ] A disabled application leaves no gap: the remaining launchers close up
|
||||
- [ ] The launcher order is the same on two machines with the same
|
||||
features enabled
|
||||
- [ ] Dragging a launcher does not move it
|
||||
- [ ] Right-clicking a launcher offers no Pin, Unpin, Configure or panel
|
||||
edit entry
|
||||
- [ ] The order survives launching and closing applications, logout/login
|
||||
and a reboot
|
||||
- [ ] The bar is right at 1080p/100% and 4K/200%
|
||||
|
||||
## The Bible study laptop
|
||||
|
||||
A laptop installed as a fixed appliance: AC power, battery out, lid shut,
|
||||
one television on HDMI, Wi-Fi.
|
||||
|
||||
```
|
||||
DEVICE_TYPE=laptop
|
||||
DISPLAY_MODE=external-only
|
||||
NETWORK_PREFERENCE=wifi
|
||||
WONDERINK_ENABLED=false
|
||||
PARENT_CALL_ENABLED=false
|
||||
DVD_ENABLED=false
|
||||
```
|
||||
|
||||
Nothing about `DEVICE_TYPE=laptop` turns those applications off; they are
|
||||
off because those lines say so.
|
||||
|
||||
### Physical verification — a real lid and a real television
|
||||
|
||||
The lab has no lid and no HDMI hotplug, so none of this is proven by a
|
||||
passing build. Do it once at the machine:
|
||||
|
||||
1. With the lid **open** and the television on, check
|
||||
`classroom-display-mode` reports the panel switched off and the
|
||||
television on, and that only the television is lit.
|
||||
2. Close the lid. The machine must keep running: the television stays on,
|
||||
audio keeps playing, SSH stays up. `systemctl is-system-running`
|
||||
still answers, and `loginctl` shows the session active.
|
||||
3. Leave it closed for ten minutes. It must not suspend, hibernate, log
|
||||
out or lock. `journalctl -b | grep -i -E "suspend|hibernate|lid"`
|
||||
should show nothing acting.
|
||||
4. With the lid still closed, switch the television **off** and on again.
|
||||
The picture must come back. **Known limitation of 0.2.4:** the display
|
||||
mode is applied at login, not continuously, so if KWin re-enables the
|
||||
internal panel on that power cycle it stays enabled until the next
|
||||
login. Check whether it does, and report it — that decides whether the
|
||||
follow-up needs an event-driven applier like the audio policy's.
|
||||
5. Unplug and replug HDMI, lid closed. Same check.
|
||||
6. Reboot with the lid closed and the television on. It must come back to
|
||||
the classroom desktop on the television.
|
||||
7. Confirm Wi-Fi reconnects on its own, with nobody typing a password.
|
||||
8. Confirm the HDMI audio policy still works (see Audio, above).
|
||||
|
||||
## Messaging
|
||||
|
||||
- [ ] With `MQTT_ENABLED=false`, `journalctl -u ekklesia-classroom-mqtt`
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""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)],
|
||||
)
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/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())
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/bin/bash
|
||||
# Which displays a machine drives, and what a closed lid does.
|
||||
#
|
||||
# The laptop going in for the women's Bible study sits on AC with its
|
||||
# battery out, its lid shut and one television on HDMI. Two separate
|
||||
# things have to be true for that: its internal panel is off, and closing
|
||||
# the lid does nothing at all.
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
sys.path.insert(0, str(root / "lib"))
|
||||
|
||||
from classroom_display import (
|
||||
ALL, EXTERNAL_ONLY, TYPE_PANEL, changes, is_internal, plan,
|
||||
)
|
||||
from classroom_config import display_mode
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
|
||||
def check(label, condition, detail=""):
|
||||
global passed, failed
|
||||
if condition:
|
||||
print(f" ok {label}")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" FAIL {label}")
|
||||
if detail:
|
||||
print(f" {detail}")
|
||||
failed += 1
|
||||
|
||||
|
||||
def out(name, kind, enabled=True, connected=True):
|
||||
return {"name": name, "type": kind, "enabled": enabled,
|
||||
"connected": connected}
|
||||
|
||||
|
||||
HDMI, DISPLAYPORT, UNKNOWN = 6, 14, 0
|
||||
|
||||
# The Bible study laptop: its own panel, and the television.
|
||||
LAPTOP = [out("eDP-1", TYPE_PANEL), out("HDMI-A-1", HDMI)]
|
||||
# A classroom tower with one television.
|
||||
TOWER = [out("HDMI-A-1", HDMI)]
|
||||
|
||||
print("1. the built-in panel is found by what it IS, not what it is called")
|
||||
check("kscreen's Panel type is the built-in screen",
|
||||
is_internal(out("whatever-3", TYPE_PANEL)))
|
||||
check("an HDMI output is not",
|
||||
not is_internal(out("HDMI-A-1", HDMI)))
|
||||
check("nor DisplayPort",
|
||||
not is_internal(out("DP-2", DISPLAYPORT)))
|
||||
# Some drivers report no type at all, so the names are a fallback and
|
||||
# only a fallback.
|
||||
check("a driver that reports no type falls back to the name",
|
||||
is_internal(out("eDP-1", UNKNOWN)) and is_internal(out("LVDS-1", UNKNOWN)))
|
||||
check("and an unknown-type television is still not internal",
|
||||
not is_internal(out("HDMI-A-1", UNKNOWN)))
|
||||
|
||||
print("2. every existing classroom is untouched")
|
||||
check("a tower on 'all' drives its television",
|
||||
plan(TOWER, ALL) == (["HDMI-A-1"], []))
|
||||
check("a device that has never heard of DISPLAY_MODE gets 'all'",
|
||||
display_mode({}) == ALL)
|
||||
check("and an unreadable value is 'all' rather than a dark room",
|
||||
display_mode({"DISPLAY_MODE": "nonsense"}) == ALL)
|
||||
|
||||
print("3. the Bible study laptop")
|
||||
enable, disable = plan(LAPTOP, EXTERNAL_ONLY)
|
||||
check("the television is driven", enable == ["HDMI-A-1"], str(enable))
|
||||
check("and the laptop's own panel is switched off",
|
||||
disable == ["eDP-1"], str(disable))
|
||||
|
||||
print("4. the same laptop is not forced into it")
|
||||
# external-only must be a setting, never a property of having a lid.
|
||||
check("a laptop on 'all' keeps both screens",
|
||||
plan(LAPTOP, ALL) == (["eDP-1", "HDMI-A-1"], []))
|
||||
|
||||
print("5. never leave the machine with no screen")
|
||||
# A television that is off at boot is the ordinary case, and a laptop
|
||||
# that answers it by blinding itself is one nobody can fix standing in
|
||||
# front of it.
|
||||
lid_only = [out("eDP-1", TYPE_PANEL)]
|
||||
check("external-only with no external display keeps the panel on",
|
||||
plan(lid_only, EXTERNAL_ONLY) == (["eDP-1"], []),
|
||||
str(plan(lid_only, EXTERNAL_ONLY)))
|
||||
check("and nothing at all connected decides nothing",
|
||||
plan([], EXTERNAL_ONLY) == ([], []))
|
||||
check("a disconnected output is not counted as somewhere to go",
|
||||
plan([out("eDP-1", TYPE_PANEL),
|
||||
out("HDMI-A-1", HDMI, connected=False)], EXTERNAL_ONLY)
|
||||
== (["eDP-1"], []))
|
||||
|
||||
print("6. only what is not already true is applied")
|
||||
# This is what keeps a television power cycle from becoming a stream of
|
||||
# mode sets, and what keeps the journal quiet when there is nothing to do.
|
||||
settled = [out("eDP-1", TYPE_PANEL, enabled=False), out("HDMI-A-1", HDMI)]
|
||||
check("a laptop already in external-only needs no change",
|
||||
changes(settled, EXTERNAL_ONLY) == ([], []),
|
||||
str(changes(settled, EXTERNAL_ONLY)))
|
||||
check("a television just switched on is enabled, panel switched off",
|
||||
changes([out("eDP-1", TYPE_PANEL), out("HDMI-A-1", HDMI, enabled=False)],
|
||||
EXTERNAL_ONLY) == (["HDMI-A-1"], ["eDP-1"]))
|
||||
check("a settled tower needs no change",
|
||||
changes(TOWER, ALL) == ([], []))
|
||||
|
||||
print("7. a closed lid does nothing, at every layer that could act on it")
|
||||
logind = (root / "config/systemd/logind-ekklesia-classroom.conf").read_text()
|
||||
powerdevil = (root / "config/plasma/xdg/powermanagementprofilesrc").read_text()
|
||||
sleep_rules = (root / "config/polkit/org.ekklesia.classroom.sleep.rules").read_text()
|
||||
locker = (root / "config/plasma/xdg/kscreenlockerrc").read_text()
|
||||
|
||||
for key in ("HandleLidSwitch=ignore", "HandleLidSwitchExternalPower=ignore",
|
||||
"HandleLidSwitchDocked=ignore"):
|
||||
check(f"logind: {key}", key in logind)
|
||||
|
||||
# PowerDevil takes logind's inhibitor while a session is running, so
|
||||
# logind alone would be overruled. Both halves, in every profile.
|
||||
check("PowerDevil does nothing on a lid event, on AC and on battery",
|
||||
powerdevil.count("lidAction=0") >= 3, powerdevil.count("lidAction=0"))
|
||||
check("and does not act just because a monitor is attached",
|
||||
powerdevil.count("triggerLidActionWhenExternalMonitorPresent=false") >= 3)
|
||||
check("every power profile is immutable, so a stray file cannot add one",
|
||||
powerdevil.count("[$i]") >= 6)
|
||||
check("the classroom account cannot suspend by any other route",
|
||||
"login1.suspend" in sleep_rules and "login1.hibernate" in sleep_rules)
|
||||
check("and the session never locks",
|
||||
"Autolock=false" in locker or "LockOnResume=false" in locker,
|
||||
locker[:200])
|
||||
|
||||
print()
|
||||
print(f"{passed} passed, {failed} failed")
|
||||
raise SystemExit(1 if failed else 0)
|
||||
PY
|
||||
@@ -107,11 +107,17 @@ check("settings install to the classroom's own directory",
|
||||
# above into the cascade at all. A drop-in directory is built to be
|
||||
# added to; a name plasma-workspace already owns is not.
|
||||
xdg_paths = sorted(re.findall(r'\$STAGE(/etc/xdg/[^"]+)', build))
|
||||
check("what the classroom puts under /etc/xdg is two drop-ins",
|
||||
check("what the classroom puts under /etc/xdg is drop-ins only",
|
||||
xdg_paths == [
|
||||
"/etc/xdg/autostart/ekklesia-classroom-display.desktop",
|
||||
"/etc/xdg/autostart/ekklesia-classroom-scale.desktop",
|
||||
"/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh",
|
||||
])
|
||||
# Which displays are on is decided before the scale is applied to them,
|
||||
# and XDG autostart order is the filename order in practice.
|
||||
check("and the displays are decided before they are scaled",
|
||||
xdg_paths.index("/etc/xdg/autostart/ekklesia-classroom-display.desktop")
|
||||
< xdg_paths.index("/etc/xdg/autostart/ekklesia-classroom-scale.desktop"))
|
||||
check("and no settings file takes a name plasma-workspace owns",
|
||||
not any(path.rsplit("/", 1)[0] == "/etc/xdg" for path in xdg_paths))
|
||||
check("and that directory is searched first",
|
||||
|
||||
@@ -112,6 +112,10 @@ install -Dm755 \
|
||||
"$ROOT/scripts/classroom-scale" \
|
||||
"$STAGE/usr/local/bin/classroom-scale"
|
||||
|
||||
install -Dm755 \
|
||||
"$ROOT/scripts/classroom-display-mode" \
|
||||
"$STAGE/usr/local/bin/classroom-display-mode"
|
||||
|
||||
install -Dm755 \
|
||||
"$ROOT/scripts/classroom-shutdown" \
|
||||
"$STAGE/usr/local/bin/classroom-shutdown"
|
||||
@@ -139,6 +143,10 @@ install -Dm644 \
|
||||
"$ROOT/lib/classroom_config.py" \
|
||||
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_config.py"
|
||||
|
||||
install -Dm644 \
|
||||
"$ROOT/lib/classroom_display.py" \
|
||||
"$STAGE/usr/local/lib/ekklesia-classroom/classroom_display.py"
|
||||
|
||||
|
||||
|
||||
install -Dm644 \
|
||||
@@ -427,6 +435,11 @@ install -Dm644 \
|
||||
"$ROOT/config/plasma/autostart/ekklesia-classroom-scale.desktop" \
|
||||
"$STAGE/etc/xdg/autostart/ekklesia-classroom-scale.desktop"
|
||||
|
||||
# Which displays are on, decided before the scale is applied to them.
|
||||
install -Dm644 \
|
||||
"$ROOT/config/plasma/autostart/ekklesia-classroom-display.desktop" \
|
||||
"$STAGE/etc/xdg/autostart/ekklesia-classroom-display.desktop"
|
||||
|
||||
# The panel's disc player follows this machine's hardware, and the
|
||||
# detection has to have happened before a session starts.
|
||||
install -Dm644 \
|
||||
|
||||
Reference in New Issue
Block a user