"""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"