fix: the bar keeps its order when an application opens

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]>
This commit is contained in:
ekklesia
2026-09-21 19:34:15 -05:00
co-authored by Claude Opus 5
parent 0fa26f398d
commit a31fa75d55
14 changed files with 807 additions and 19 deletions
+154
View File
@@ -0,0 +1,154 @@
#!/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())