On the laptop the browser jumped behind the disc player the moment it was
opened. 0.2.4 caused that, and the cause is two lines of the stock applet:
main.qml:153 hideActivatedLaunchers: tasks.iconsOnly || ...
main.qml:155 launchInPlace: tasks.iconsOnly && sortingStrategy === 1
For an icons-only bar the first is always true — a launcher is hidden the
instant its application starts — and the second is what puts the running
window back where the launcher was. It only does that at sortingStrategy
1. 0.2.4 set it to 0 to stop teachers dragging the icons about, so the
launcher vanished and the window was appended after the others.
Reproduced in the lab with a real Chrome window before changing anything,
and the bar read off the screen: Wonder Ink, the player, then the
browser. With 1 it is Wonder Ink, the browser, the player, through
opening, a second window, another application opening beside it, and
every close.
The launcher identity was suspected and cleared: only one browser button
appears, because google-chrome.desktop already carries Chrome's own file
name and StartupWMClass for exactly this reason.
THE COST, AND IT IS REAL. Manual sorting is also the only mode in which
MouseHandler.qml accepts a reordering drop, so a teacher who knows to drag
can rearrange the bar again. There is no value that gives both and no
Kiosk key that separates them. classroom-desktop-reset already rebuilds
the bar from the package at every login, so a drag never outlives the
session. docs/testing.md states the trade rather than burying it, and the
configuration escape is written down there too as still open.
tools/lab/order-check is the test this needed and did not have. 0.2.4
passed a full set of configuration assertions while the bar did something
else, so this one opens real application windows and reads the panel
strip as pixels — there is no image library in the lab, so it captures
PPM and names each icon by colour. Six checks, all of them behaviour.
Co-Authored-By: Claude Opus 5 <[email protected]>
155 lines
4.2 KiB
Python
Executable File
155 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
"""Read the classroom bar off the screen and say what order it is in.
|
|
|
|
The launcher-order bug of 0.2.4 got past a full set of configuration
|
|
tests, because the configuration was right and the applet did something
|
|
else with it. So this asks the picture.
|
|
|
|
It captures the panel strip as a PPM — no image library anywhere in this
|
|
lab, and PPM is a length, three numbers and the bytes — finds the icons by
|
|
looking for columns that are not the bar's own background, and names each
|
|
one by its colour. Three icons, three colours that are nothing like each
|
|
other:
|
|
|
|
Wonder Ink orange and blue, and orange is what a column of it
|
|
averages to
|
|
Browser Chrome's wheel: red, green, yellow and blue, so its
|
|
average is unusually grey for something this bright
|
|
the player one flat purple disc
|
|
|
|
Usage: panel-order [STRIP] default "0,990 1920x90"
|
|
Prints one name per icon, left to right.
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
|
|
CONTAINER = "classroom-v2-lab"
|
|
STRIP = sys.argv[1] if len(sys.argv) > 1 else "0,990 1920x90"
|
|
|
|
|
|
def capture():
|
|
socket = subprocess.run(
|
|
["docker", "exec", CONTAINER, "sh", "-c",
|
|
"ls /run/user/1000/sway-ipc.* 2>/dev/null | head -1"],
|
|
capture_output=True, text=True, check=True).stdout.strip()
|
|
|
|
raw = subprocess.run(
|
|
["docker", "exec", "-u", "classroom",
|
|
"-e", "XDG_RUNTIME_DIR=/run/user/1000",
|
|
"-e", "WAYLAND_DISPLAY=wayland-1",
|
|
"-e", f"SWAYSOCK={socket}",
|
|
CONTAINER, "grim", "-t", "ppm", "-g", STRIP, "-"],
|
|
capture_output=True, check=True).stdout
|
|
|
|
return raw
|
|
|
|
|
|
def pixels(raw):
|
|
"""(width, height, rows) from a binary PPM."""
|
|
fields = []
|
|
index = 0
|
|
|
|
while len(fields) < 4:
|
|
while raw[index:index + 1].isspace():
|
|
index += 1
|
|
|
|
if raw[index:index + 1] == b"#":
|
|
while raw[index:index + 1] not in (b"\n", b""):
|
|
index += 1
|
|
continue
|
|
|
|
start = index
|
|
|
|
while not raw[index:index + 1].isspace():
|
|
index += 1
|
|
|
|
fields.append(raw[start:index])
|
|
|
|
index += 1
|
|
width, height = int(fields[1]), int(fields[2])
|
|
body = raw[index:]
|
|
rows = [
|
|
body[y * width * 3:(y + 1) * width * 3]
|
|
for y in range(height)
|
|
]
|
|
|
|
return width, height, rows
|
|
|
|
|
|
def columns(width, height, rows):
|
|
"""Average colour and brightness of each column."""
|
|
found = []
|
|
|
|
for x in range(width):
|
|
r = g = b = 0
|
|
|
|
for row in rows:
|
|
r += row[x * 3]
|
|
g += row[x * 3 + 1]
|
|
b += row[x * 3 + 2]
|
|
|
|
found.append((r // height, g // height, b // height))
|
|
|
|
return found
|
|
|
|
|
|
def name(colour):
|
|
r, g, b = colour
|
|
|
|
# The disc: blue and red well above green.
|
|
if b > g + 25 and r > g + 15:
|
|
return "player"
|
|
|
|
# Wonder Ink: warm, red clearly ahead of blue.
|
|
if r > b + 25:
|
|
return "wonderink"
|
|
|
|
return "browser"
|
|
|
|
|
|
def main():
|
|
width, height, rows = pixels(capture())
|
|
found = columns(width, height, rows)
|
|
|
|
# The bar is dark and flat; an icon is a run of brighter columns.
|
|
floor = sorted(sum(c) for c in found)[len(found) // 2] + 60
|
|
runs = []
|
|
run = None
|
|
|
|
for x, colour in enumerate(found):
|
|
if sum(colour) > floor:
|
|
if run is None:
|
|
run = [x, x]
|
|
else:
|
|
run[1] = x
|
|
elif run is not None:
|
|
if run[1] - run[0] >= 12:
|
|
runs.append(run)
|
|
run = None
|
|
|
|
if run is not None and run[1] - run[0] >= 12:
|
|
runs.append(run)
|
|
|
|
# The applications live in the middle third; the corners are Parent
|
|
# Call on the left and the status icons on the right.
|
|
middle = [
|
|
item for item in runs
|
|
if width // 3 < (item[0] + item[1]) // 2 < (width * 2) // 3
|
|
]
|
|
|
|
for first, last in middle:
|
|
centre = (first + last) // 2
|
|
window = found[max(first, centre - 8):min(last, centre + 8) + 1]
|
|
r = sum(c[0] for c in window) // max(1, len(window))
|
|
g = sum(c[1] for c in window) // max(1, len(window))
|
|
b = sum(c[2] for c in window) // max(1, len(window))
|
|
print(name((r, g, b)))
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|