Files
Classroom_Desktop/scripts/classroom-window
T
ekklesiaandClaude Opus 5 719b856040 refactor: share the sway helpers
classroom-dvd-session needs to ask sway whether VLC has taken the
screen, which is the same tree walking classroom-window already does.
One copy, in a module both import; classroom-window loses a third of its
length.

The state machine is worth protecting through a move like that, so its
regression test comes with it: the exact swaymsg commands for launch,
focus, minimise, restore, Super+M, Super+Shift+M, the scratchpad rescue
and sway not running at all.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 23:17:18 -05:00

209 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Classroom window control.
Sway has no real minimise concept, so the classroom uses a dedicated
hidden workspace. A minimised window is parked there as an ordinary
tiled container and moved back to the visible workspace on restore,
which is why it returns maximised instead of floating the way the old
scratchpad approach did.
"""
import argparse
import os
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"),
)
from classroom_sway import ( # noqa: E402
HIDDEN_WORKSPACE,
command,
matches,
visible_workspace,
windows,
)
STATE_DIR = os.path.join(
os.environ.get("XDG_RUNTIME_DIR") or "/tmp",
"ekklesia-classroom",
)
STATE_FILE = os.path.join(STATE_DIR, "minimized")
def read_state():
try:
with open(STATE_FILE, "r", encoding="utf-8") as handle:
return [int(line) for line in handle.read().split() if line.strip()]
except (OSError, ValueError):
return []
def write_state(identifiers):
try:
os.makedirs(STATE_DIR, exist_ok=True)
with open(STATE_FILE, "w", encoding="utf-8") as handle:
handle.write("\n".join(str(item) for item in identifiers))
except OSError:
pass
def remember(identifier):
"""Track minimise order so restore-last picks the newest window."""
state = [item for item in read_state() if item != identifier]
state.append(identifier)
write_state(state[-32:])
def forget(identifier):
write_state([item for item in read_state() if item != identifier])
def minimize(window):
# A fullscreen container keeps rendering over everything else if it
# is parked while still fullscreen, so always drop out of it first.
command(f'[con_id={window["id"]}] fullscreen disable')
command(f'[con_id={window["id"]}] move container to workspace {HIDDEN_WORKSPACE}')
remember(window["id"])
def restore(window):
workspace = visible_workspace()
command(f'[con_id={window["id"]}] move container to workspace {workspace}')
# Windows parked by older classroom builds came back floating because
# they had been through the scratchpad. Force them back into the tiling
# layout so a restored browser is maximised like the user expects.
command(f'[con_id={window["id"]}] floating disable')
command(f'[con_id={window["id"]}] focus')
forget(window["id"])
def launch(argv):
if not argv:
return
try:
subprocess.Popen(argv, start_new_session=True)
except OSError as error:
print(f"classroom-window: unable to launch {argv[0]}: {error}", file=sys.stderr)
def action_toggle(patterns, argv):
"""Launch, focus, minimise or restore, in the order a user expects."""
candidates = [window for window in windows() if matches(window, patterns)]
if not candidates:
launch(argv)
return 0
hidden = [window for window in candidates if window["hidden"]]
if hidden:
for window in hidden:
restore(window)
return 0
if any(window["focused"] for window in candidates):
for window in candidates:
minimize(window)
return 0
command(f'[con_id={candidates[-1]["id"]}] focus')
return 0
def action_minimize():
for window in windows():
if window["focused"] and not window["hidden"]:
minimize(window)
return 0
return 0
def action_restore_last():
hidden = {window["id"]: window for window in windows() if window["hidden"]}
if not hidden:
return 0
for identifier in reversed(read_state()):
if identifier in hidden:
restore(hidden[identifier])
return 0
restore(list(hidden.values())[-1])
return 0
def action_list():
for window in windows():
state = "minimized" if window["hidden"] else "visible"
state = "focused" if window["focused"] else state
print(
f'{window["id"]}\t{state}\t'
f'{window["app_id"] or window["class"]}\t{window["workspace"]}'
)
return 0
def main():
parser = argparse.ArgumentParser(
description="Minimise, restore and toggle classroom windows.",
)
subparsers = parser.add_subparsers(dest="action", required=True)
toggle = subparsers.add_parser(
"toggle",
help="Launch, focus, minimise or restore an application",
)
toggle.add_argument(
"--match",
action="append",
required=True,
metavar="REGEX",
help="app_id or X11 class to match (may be repeated)",
)
toggle.add_argument(
"launch",
nargs=argparse.REMAINDER,
help="Command to run when the application is not running",
)
subparsers.add_parser("minimize", help="Minimise the focused window")
subparsers.add_parser("restore-last", help="Restore the newest minimised window")
subparsers.add_parser("list", help="List classroom windows and their state")
arguments = parser.parse_args()
if arguments.action == "toggle":
argv = arguments.launch
if argv and argv[0] == "--":
argv = argv[1:]
return action_toggle(arguments.match, argv)
if arguments.action == "minimize":
return action_minimize()
if arguments.action == "restore-last":
return action_restore_last()
return action_list()
if __name__ == "__main__":
sys.exit(main())