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:
co-authored by
Claude Opus 5
parent
0fa26f398d
commit
a31fa75d55
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
# The launcher order, read off the screen, with real windows opening and
|
||||
# closing underneath it.
|
||||
#
|
||||
# 0.2.4 shipped a launcher order that was right in every configuration
|
||||
# test and wrong on the bar, so this test opens actual applications and
|
||||
# looks at actual pixels. Run with a classroom session already up:
|
||||
#
|
||||
# tools/lab/lab session
|
||||
# tools/lab/order-check
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
NAME="${LAB_NAME:-classroom-v2-lab}"
|
||||
WANT="wonderink browser player"
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
|
||||
# Consecutive repeats are collapsed before comparing. The classroom sets
|
||||
# groupingStrategy=0 on purpose — every window gets its own button, so a
|
||||
# teacher never has to open a group to discover a second window exists —
|
||||
# so two browser windows are two browser buttons and that is correct. What
|
||||
# must not change is the order of the KINDS: Wonder Ink, then the browser,
|
||||
# then the player.
|
||||
check() {
|
||||
local label="$1"
|
||||
local got
|
||||
got="$("$ROOT/tools/lab/panel-order" | uniq | tr '\n' ' ' | sed 's/ *$//')"
|
||||
|
||||
if [[ "$got" == "$WANT" ]]; then
|
||||
echo " ok $label"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
echo " FAIL $label"
|
||||
echo " wanted [$WANT]"
|
||||
echo " got [$got]"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
app() {
|
||||
docker exec -d -u classroom \
|
||||
-e XDG_RUNTIME_DIR=/run/user/1000 -e WAYLAND_DISPLAY=wayland-0 \
|
||||
-e HOME=/home/classroom -e XDG_CURRENT_DESKTOP=KDE \
|
||||
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
|
||||
"$NAME" sh -c "$1"
|
||||
}
|
||||
|
||||
echo "1. nothing running"
|
||||
docker exec "$NAME" pkill -u classroom chrome >/dev/null 2>&1 || true
|
||||
sleep 4
|
||||
check "the bar is Wonder Ink, the browser, the player"
|
||||
|
||||
echo "2. the browser opens"
|
||||
app 'exec google-chrome --ozone-platform=wayland --no-sandbox --disable-gpu --no-first-run about:blank'
|
||||
sleep 16
|
||||
check "the browser stays in its place and does not move behind the player"
|
||||
|
||||
echo "3. a second browser window"
|
||||
app 'exec google-chrome --ozone-platform=wayland --no-sandbox --disable-gpu --new-window about:blank'
|
||||
sleep 12
|
||||
check "a second window adds a button beside it and moves nothing"
|
||||
|
||||
echo "4. Wonder Ink opens as well, with the browser still running"
|
||||
app 'exec classroom-wonderink'
|
||||
sleep 18
|
||||
check "the first launcher does not move when it is the one that opens"
|
||||
|
||||
echo "5. Wonder Ink closes, the browser stays"
|
||||
docker exec "$NAME" pkill -u classroom -f classroom-wonderink >/dev/null 2>&1 || true
|
||||
sleep 6
|
||||
check "closing the middle of three changes nothing"
|
||||
|
||||
echo "6. the browser closes again"
|
||||
docker exec "$NAME" pkill -u classroom chrome >/dev/null 2>&1 || true
|
||||
sleep 6
|
||||
check "closing it puts nothing back in a different order"
|
||||
|
||||
echo
|
||||
echo "$passed passed, $failed failed"
|
||||
[[ $failed -eq 0 ]]
|
||||
Executable
+154
@@ -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())
|
||||
Reference in New Issue
Block a user