feat: take the last of sway out of the classroom
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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
18cf0249dd
commit
d2d30d6b0c
@@ -1,66 +0,0 @@
|
||||
"""Stable roles for Chrome's otherwise indistinguishable top-level windows."""
|
||||
|
||||
from classroom_sway import matches
|
||||
|
||||
|
||||
CHROME_PATTERNS = (
|
||||
r"google-chrome",
|
||||
r"chrome",
|
||||
r"chrome-.*",
|
||||
r"google-chrome-.*",
|
||||
)
|
||||
|
||||
|
||||
def chrome_windows(current):
|
||||
"""Chrome containers in Sway tree order, without using page titles."""
|
||||
return [window for window in current if matches(window, CHROME_PATTERNS)]
|
||||
|
||||
|
||||
class ChromeRoles:
|
||||
"""Remember the first Chrome container and classify every later one."""
|
||||
|
||||
def __init__(self):
|
||||
self.primary = None
|
||||
self.secondary = []
|
||||
|
||||
def update(self, current):
|
||||
identifiers = [window["id"] for window in chrome_windows(current)]
|
||||
previous_primary = self.primary
|
||||
previous_secondary = list(self.secondary)
|
||||
|
||||
promoted = None
|
||||
|
||||
if self.primary not in identifiers:
|
||||
self.primary = identifiers[0] if identifiers else None
|
||||
|
||||
if self.primary is not None and previous_primary is not None:
|
||||
promoted = self.primary
|
||||
|
||||
self.secondary = [
|
||||
identifier
|
||||
for identifier in previous_secondary
|
||||
if identifier in identifiers and identifier != self.primary
|
||||
]
|
||||
|
||||
new_secondary = []
|
||||
|
||||
for identifier in identifiers:
|
||||
if identifier == self.primary or identifier in self.secondary:
|
||||
continue
|
||||
|
||||
self.secondary.append(identifier)
|
||||
new_secondary.append(identifier)
|
||||
|
||||
removed = [
|
||||
identifier
|
||||
for identifier in previous_secondary
|
||||
if identifier not in identifiers
|
||||
]
|
||||
|
||||
return {
|
||||
"primary": self.primary,
|
||||
"promoted": promoted,
|
||||
"new_secondary": new_secondary,
|
||||
"removed": removed,
|
||||
"active_secondary": self.secondary[-1] if self.secondary else None,
|
||||
}
|
||||
@@ -24,9 +24,6 @@ DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration
|
||||
UPDATE_URL=https://updates.riverofglorymn.org
|
||||
CHANNEL=stable
|
||||
|
||||
# Set to true on classrooms that have a DVD drive.
|
||||
DVD_ENABLED=false
|
||||
|
||||
# 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
|
||||
@@ -61,12 +58,6 @@ MQTT_PASSWORD=
|
||||
# an upgrade make a setting discoverable without replacing anything an
|
||||
# administrator has already put in device.conf.
|
||||
DEVICE_CONFIG_SECTIONS = (
|
||||
(
|
||||
"DVD_ENABLED",
|
||||
"""# Set to true on classrooms that have a DVD drive.
|
||||
DVD_ENABLED=false
|
||||
""",
|
||||
),
|
||||
(
|
||||
"LIVE_ENABLED",
|
||||
"""# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
|
||||
@@ -136,19 +127,6 @@ def enabled(config, key, default=False):
|
||||
return config[key].lower() in TRUTHY
|
||||
|
||||
|
||||
def dock_items(config):
|
||||
"""Dock items in display order, with disabled capabilities omitted."""
|
||||
items = ["browser"]
|
||||
|
||||
if enabled(config, "DVD_ENABLED"):
|
||||
items.append("dvd")
|
||||
|
||||
if enabled(config, "LIVE_ENABLED"):
|
||||
items.append("live")
|
||||
|
||||
return tuple(items)
|
||||
|
||||
|
||||
def ensure_device_config(path=None):
|
||||
"""Create device.conf or append any missing settings exactly once.
|
||||
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
"""Talking to sway.
|
||||
|
||||
Shared by classroom-window, which moves windows around, and by
|
||||
classroom-dvd-session, which needs to know when VLC has actually taken
|
||||
the screen.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
HIDDEN_WORKSPACE = "__ekklesia_hidden"
|
||||
|
||||
# sway parks scratchpad windows on a workspace of its own. Nothing here
|
||||
# puts them there, but treating it as hidden means a window left in the
|
||||
# scratchpad by an older classroom build can still be restored.
|
||||
HIDDEN_WORKSPACES = {HIDDEN_WORKSPACE, "__i3_scratch"}
|
||||
|
||||
|
||||
def swaymsg(*arguments):
|
||||
"""Run swaymsg, returning stdout or None when sway is unavailable."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["swaymsg", *arguments],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
return result.stdout
|
||||
|
||||
|
||||
def command(argument):
|
||||
"""Run a sway command, ignoring criteria that match nothing."""
|
||||
swaymsg("--", argument)
|
||||
|
||||
|
||||
def query(kind):
|
||||
output = swaymsg("-t", kind, "-r")
|
||||
|
||||
if not output:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(output)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def collect(node, workspace, found, floating=False):
|
||||
"""Flatten the sway tree into the windows the classroom cares about."""
|
||||
if node.get("type") == "workspace":
|
||||
workspace = node.get("name")
|
||||
|
||||
is_window = node.get("pid") is not None and not node.get("nodes")
|
||||
|
||||
if is_window:
|
||||
properties = node.get("window_properties") or {}
|
||||
|
||||
found.append(
|
||||
{
|
||||
"id": node.get("id"),
|
||||
"app_id": node.get("app_id") or "",
|
||||
"name": node.get("name") or "",
|
||||
"class": properties.get("class") or "",
|
||||
"instance": properties.get("instance") or "",
|
||||
"focused": bool(node.get("focused")),
|
||||
"fullscreen": bool(node.get("fullscreen_mode")),
|
||||
"workspace": workspace,
|
||||
"hidden": workspace in HIDDEN_WORKSPACES,
|
||||
"floating": floating,
|
||||
}
|
||||
)
|
||||
|
||||
for child in node.get("nodes", []):
|
||||
collect(child, workspace, found, floating)
|
||||
|
||||
for child in node.get("floating_nodes", []):
|
||||
collect(child, workspace, found, True)
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def windows():
|
||||
tree = query("get_tree")
|
||||
|
||||
if tree is None:
|
||||
return []
|
||||
|
||||
return collect(tree, None, [])
|
||||
|
||||
|
||||
def matches(window, patterns):
|
||||
"""Match a window against app_id / X11 class / X11 instance."""
|
||||
identities = [
|
||||
window["app_id"],
|
||||
window["class"],
|
||||
window["instance"],
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
expression = re.compile(pattern, re.IGNORECASE)
|
||||
|
||||
for identity in identities:
|
||||
if identity and expression.fullmatch(identity):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def visible_workspace():
|
||||
"""The workspace a restored window should come back to."""
|
||||
workspaces = query("get_workspaces") or []
|
||||
|
||||
for workspace in workspaces:
|
||||
if workspace.get("focused") and workspace.get("name") not in HIDDEN_WORKSPACES:
|
||||
return workspace.get("name")
|
||||
|
||||
for workspace in workspaces:
|
||||
if workspace.get("visible") and workspace.get("name") not in HIDDEN_WORKSPACES:
|
||||
return workspace.get("name")
|
||||
|
||||
return "1"
|
||||
Reference in New Issue
Block a user