feat: give the classroom a desktop instead of a shell
The whole appliance UI is now configuration for a desktop Debian already ships: a Plasma 6.3 session with a colour scheme, a look and feel package that builds the bar, window rules, and KDE's own Kiosk restrictions. Two things were measured rather than assumed, and both changed a decision. Plasma restarts a crashed panel three times a minute and then gives up, which is the failure a classroom has already had. Four lines of systemd drop-in make it unconditional; the panel then survived eight SIGKILLs a second apart, back each time in 2.4s. Plasma draws a minimised task and a background task identically, so the five states the room needs cannot all be shown. Rather than write a task manager, the classroom removes minimise: clicking a launcher always means show me this. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bfc7caf28f
commit
87b55761c6
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the light colour scheme from the dark one.
|
||||
|
||||
There is one set of design tokens, not two themes. The light scheme is the
|
||||
same file with the roles swapped, so a colour can only be changed in one
|
||||
place and the two can never drift apart.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
COLORS = ROOT / "config" / "plasma" / "colors"
|
||||
|
||||
# dark value -> light value
|
||||
ROLES = {
|
||||
"20,28,32": "242,245,244", # window background
|
||||
"25,35,40": "233,238,237", # alternate
|
||||
"15,23,26": "255,255,255", # view background
|
||||
"18,26,30": "236,241,240", # header
|
||||
"16,23,26": "228,234,233", # header, inactive
|
||||
"30,42,47": "225,231,230", # button alternate
|
||||
"230,237,239": "22,35,42", # normal text
|
||||
"147,165,170": "96,116,122", # inactive text
|
||||
"170,186,191": "80,99,105", # inactive header text
|
||||
"125,142,147": "120,138,144",
|
||||
"79,179,168": "46,140,130", # accent
|
||||
"126,196,255": "26,110,180", # link
|
||||
"227,106,106": "192,57,57", # negative
|
||||
"224,177,92": "166,116,24", # neutral
|
||||
"111,207,151": "38,142,86", # positive
|
||||
"156,138,222": "104,80,180", # visited
|
||||
"14,21,24": "255,255,255", # text on a selection
|
||||
"10,16,18": "255,255,255",
|
||||
}
|
||||
|
||||
HEADER = (
|
||||
"# Ekklesia Classroom — light.\n"
|
||||
"#\n"
|
||||
"# GENERATED from EkklesiaDark.colors by tools/build-light-scheme.\n"
|
||||
"# Edit the dark scheme and regenerate; do not edit this file by hand."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
dark = (COLORS / "EkklesiaDark.colors").read_text()
|
||||
|
||||
light = dark
|
||||
for old, new in sorted(ROLES.items(), key=lambda item: -len(item[0])):
|
||||
light = re.sub(rf"(?<![\d,]){re.escape(old)}(?![\d,])", "\0" + new, light)
|
||||
|
||||
light = light.replace("\0", "")
|
||||
light = light.replace("Ekklesia Classroom Dark", "Ekklesia Classroom Light")
|
||||
light = light.replace("ColorScheme=EkklesiaDark", "ColorScheme=EkklesiaLight")
|
||||
light = light.replace("# Ekklesia Classroom — dark. The default.", HEADER)
|
||||
|
||||
(COLORS / "EkklesiaLight.colors").write_text(light)
|
||||
print("wrote", COLORS / "EkklesiaLight.colors")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the classroom's idle desktop background.
|
||||
|
||||
This is a STAND-IN. The classroom's chosen wallpaper is an illustrated
|
||||
landscape that is not in this repository; when it arrives it replaces the
|
||||
output of this script at the same path and nothing else changes.
|
||||
|
||||
It exists so the desktop can be judged in the right palette rather than
|
||||
against the bright confetti background it is replacing: sage, teal, blue
|
||||
and lavender, a river leading into the picture, and a large quiet area in
|
||||
the upper middle where nothing happens.
|
||||
|
||||
No image library is installed on the development host, so the PNG is
|
||||
written by hand — zlib and struct are enough for a truecolour image.
|
||||
"""
|
||||
|
||||
import math
|
||||
import pathlib
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
WIDTH = 1920
|
||||
HEIGHT = 1080
|
||||
OUT = pathlib.Path(__file__).resolve().parent.parent / "config" / "wallpapers"
|
||||
|
||||
|
||||
def mix(a, b, t):
|
||||
t = 0.0 if t < 0 else 1.0 if t > 1 else t
|
||||
return tuple(a[i] + (b[i] - a[i]) * t for i in range(3))
|
||||
|
||||
|
||||
def ease(t):
|
||||
"""Smoothstep. Bands in a gradient are the one thing that would read as
|
||||
a rendering fault rather than as a picture."""
|
||||
t = 0.0 if t < 0 else 1.0 if t > 1 else t
|
||||
return t * t * (3 - 2 * t)
|
||||
|
||||
|
||||
# The palette, top of the sky downwards.
|
||||
SKY = [
|
||||
(0.00, (34, 38, 58)), # deep lavender-slate
|
||||
(0.30, (58, 68, 96)), # lavender
|
||||
(0.55, (86, 108, 130)), # blue
|
||||
(0.78, (139, 165, 168)), # sage haze at the horizon
|
||||
(1.00, (178, 198, 192)),
|
||||
]
|
||||
|
||||
HORIZON = 0.56 # where the land starts, as a fraction of the height
|
||||
GLOW_X, GLOW_Y = 0.56, 0.545
|
||||
GLOW = (232, 232, 214)
|
||||
|
||||
RIDGES = [
|
||||
# (base height, amplitude, wavelength, phase, colour, haze)
|
||||
(0.575, 0.028, 2.1, 0.4, (74, 96, 110), 0.55),
|
||||
(0.615, 0.040, 1.5, 2.2, (60, 84, 92), 0.34),
|
||||
(0.665, 0.052, 1.1, 4.1, (48, 72, 74), 0.18),
|
||||
]
|
||||
|
||||
RIVER_TOP = 0.585
|
||||
RIVER_COLOUR = (96, 140, 146)
|
||||
RIVER_LIGHT = (186, 212, 202)
|
||||
|
||||
BANK = (34, 52, 48) # the sage foreground the river runs between
|
||||
|
||||
|
||||
def sky_colour(y):
|
||||
t = y / HEIGHT
|
||||
for i in range(len(SKY) - 1):
|
||||
t0, c0 = SKY[i]
|
||||
t1, c1 = SKY[i + 1]
|
||||
if t <= t1 or i == len(SKY) - 2:
|
||||
return mix(c0, c1, ease((t - t0) / (t1 - t0)))
|
||||
return SKY[-1][1]
|
||||
|
||||
|
||||
def river_edges(y):
|
||||
"""The river narrows towards the horizon and opens towards the viewer,
|
||||
which is what makes it lead into the picture rather than across it."""
|
||||
t = (y / HEIGHT - RIVER_TOP) / (1 - RIVER_TOP)
|
||||
if t < 0:
|
||||
return None
|
||||
t = ease(t)
|
||||
centre = 0.56 - 0.14 * t * t
|
||||
half = 0.004 + 0.30 * t * t
|
||||
return (centre - half) * WIDTH, (centre + half) * WIDTH
|
||||
|
||||
|
||||
def render():
|
||||
rows = []
|
||||
for y in range(HEIGHT):
|
||||
base = sky_colour(y)
|
||||
row = bytearray()
|
||||
edges = river_edges(y)
|
||||
yn = y / HEIGHT
|
||||
|
||||
# Ridge heights for this column are cheap to evaluate, so the land
|
||||
# is computed per pixel rather than filled as polygons.
|
||||
for x in range(WIDTH):
|
||||
xn = x / WIDTH
|
||||
r, g, b = base
|
||||
|
||||
# A soft light low in the sky, off centre. It is the only
|
||||
# bright thing in the picture.
|
||||
dx = (xn - GLOW_X) * 1.7
|
||||
dy = yn - GLOW_Y
|
||||
d = math.sqrt(dx * dx + dy * dy)
|
||||
glow = math.exp(-(d * d) / 0.055) * 0.55
|
||||
if glow > 0.002:
|
||||
r, g, b = mix((r, g, b), GLOW, glow)
|
||||
|
||||
land = None
|
||||
for height, amp, wavelength, phase, colour, haze in RIDGES:
|
||||
ridge = height - amp * (
|
||||
0.6 * math.sin(xn * math.pi * 2 * wavelength + phase)
|
||||
+ 0.4 * math.sin(xn * math.pi * 2 * wavelength * 2.3 + phase * 1.7)
|
||||
)
|
||||
if yn >= ridge:
|
||||
# Nearer ridges are darker and less hazy, which is the
|
||||
# whole of the depth in this picture.
|
||||
shade = mix(colour, (r, g, b), haze)
|
||||
depth = ease((yn - ridge) / 0.16)
|
||||
land = mix(shade, tuple(c * 0.82 for c in shade), depth)
|
||||
|
||||
if land is not None:
|
||||
r, g, b = land
|
||||
|
||||
if edges is not None:
|
||||
left, right = edges
|
||||
if left <= x <= right:
|
||||
width = max(right - left, 1.0)
|
||||
across = (x - left) / width
|
||||
# Light gathers down the middle of the water and the
|
||||
# banks stay dark, so the river reads as a surface.
|
||||
sheen = math.exp(-((across - 0.5) ** 2) / 0.06)
|
||||
ripple = 0.5 + 0.5 * math.sin(
|
||||
yn * 130 + math.sin(xn * 18) * 1.4
|
||||
)
|
||||
water = mix(RIVER_COLOUR, RIVER_LIGHT, 0.30 * sheen + 0.10 * ripple)
|
||||
water = mix(water, (r, g, b), max(0.0, 0.45 - 0.45 * ease((yn - RIVER_TOP) / 0.25)))
|
||||
edge = min(across, 1 - across)
|
||||
r, g, b = mix(BANK, water, ease(edge / 0.05))
|
||||
elif yn > RIVER_TOP + 0.10:
|
||||
near = ease((yn - RIVER_TOP - 0.10) / 0.34)
|
||||
r, g, b = mix((r, g, b), BANK, near * 0.85)
|
||||
|
||||
# The bottom of the screen carries the bar. Settling it down
|
||||
# keeps the panel legible without putting a band across the
|
||||
# picture.
|
||||
if yn > 0.78:
|
||||
r, g, b = mix((r, g, b), (16, 24, 28), ease((yn - 0.78) / 0.24) * 0.62)
|
||||
|
||||
row += bytes((int(r + 0.5) & 255, int(g + 0.5) & 255, int(b + 0.5) & 255))
|
||||
|
||||
rows.append(bytes(row))
|
||||
return rows
|
||||
|
||||
|
||||
def write_png(path, rows):
|
||||
raw = b"".join(b"\x00" + row for row in rows)
|
||||
|
||||
def chunk(tag, data):
|
||||
payload = tag + data
|
||||
return (
|
||||
struct.pack(">I", len(data))
|
||||
+ payload
|
||||
+ struct.pack(">I", zlib.crc32(payload) & 0xFFFFFFFF)
|
||||
)
|
||||
|
||||
png = b"\x89PNG\r\n\x1a\n"
|
||||
png += chunk(b"IHDR", struct.pack(">IIBBBBB", WIDTH, HEIGHT, 8, 2, 0, 0, 0))
|
||||
png += chunk(b"IDAT", zlib.compress(raw, 9))
|
||||
png += chunk(b"IEND", b"")
|
||||
path.write_bytes(png)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
target = OUT / "classroom-calm.png"
|
||||
write_png(target, render())
|
||||
print(f"wrote {target} ({target.stat().st_size} bytes)")
|
||||
@@ -50,3 +50,23 @@ CMD ["/sbin/init"]
|
||||
# sanctioned way to capture a KWin session that has no outer compositor.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends kde-spectacle \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# The classroom's browser, from Google's own repository, the same way a
|
||||
# classroom machine gets it. Testing the browser question on a different
|
||||
# browser would not answer it.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gnupg \
|
||||
&& install -d -m 0755 /etc/apt/keyrings \
|
||||
&& curl -fsS https://dl.google.com/linux/linux_signing_key.pub \
|
||||
| gpg --dearmor --batch --yes --output /etc/apt/keyrings/google-chrome.gpg \
|
||||
&& echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main' \
|
||||
> /etc/apt/sources.list.d/google-chrome.list \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends google-chrome-stable \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# VLC, CUPS and PipeWire: the rest of the classroom's runtime, so the
|
||||
# desktop is judged with the room's real applications in it.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
vlc vlc-plugin-base cups cups-client cups-filters \
|
||||
pipewire pipewire-pulse wireplumber pulseaudio-utils \
|
||||
libnotify-bin \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# Put the classroom desktop onto a machine, from the repository, at the
|
||||
# paths the package will use. Run as root inside the lab.
|
||||
#
|
||||
# Every destination here is a normal system path that Plasma already reads.
|
||||
# There is no classroom daemon in this list, and nothing is generated: the
|
||||
# desktop is a set of files.
|
||||
set -euo pipefail
|
||||
|
||||
SRC="${1:-/srv/classroom}"
|
||||
|
||||
install -Dm644 "$SRC/config/plasma/xdg/kdeglobals" /etc/xdg/kdeglobals
|
||||
install -Dm644 "$SRC/config/plasma/xdg/kwinrc" /etc/xdg/kwinrc
|
||||
install -Dm644 "$SRC/config/plasma/xdg/kwinrulesrc" /etc/xdg/kwinrulesrc
|
||||
install -Dm644 "$SRC/config/plasma/xdg/kscreenlockerrc" /etc/xdg/kscreenlockerrc
|
||||
install -Dm644 "$SRC/config/plasma/xdg/ksmserverrc" /etc/xdg/ksmserverrc
|
||||
install -Dm644 "$SRC/config/plasma/xdg/krunnerrc" /etc/xdg/krunnerrc
|
||||
install -Dm644 "$SRC/config/plasma/xdg/plasmanotifyrc" /etc/xdg/plasmanotifyrc
|
||||
|
||||
install -Dm644 "$SRC/config/plasma/colors/EkklesiaDark.colors" \
|
||||
/usr/share/color-schemes/EkklesiaDark.colors
|
||||
install -Dm644 "$SRC/config/plasma/colors/EkklesiaLight.colors" \
|
||||
/usr/share/color-schemes/EkklesiaLight.colors
|
||||
|
||||
LNF=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop
|
||||
rm -rf "$LNF"
|
||||
install -Dm644 "$SRC/config/plasma/lookandfeel/metadata.json" "$LNF/metadata.json"
|
||||
install -Dm644 "$SRC/config/plasma/lookandfeel/contents/defaults" \
|
||||
"$LNF/contents/defaults"
|
||||
install -Dm644 \
|
||||
"$SRC/config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js" \
|
||||
"$LNF/contents/layouts/org.kde.plasma.desktop-layout.js"
|
||||
|
||||
for entry in "$SRC"/config/plasma/applications/*.desktop; do
|
||||
install -Dm644 "$entry" "/usr/share/applications/$(basename "$entry")"
|
||||
done
|
||||
|
||||
# Entries that deliberately shadow a vendor one. XDG_DATA_DIRS searches
|
||||
# /usr/local/share before /usr/share, so these win without touching, and
|
||||
# without being overwritten by, the package that provides the original.
|
||||
for entry in "$SRC"/config/plasma/applications-local/*.desktop; do
|
||||
install -Dm644 "$entry" "/usr/local/share/applications/$(basename "$entry")"
|
||||
done
|
||||
|
||||
install -Dm644 "$SRC/config/plasma/solid/ekklesia-dvd.desktop" \
|
||||
/usr/share/solid/actions/ekklesia-dvd.desktop
|
||||
|
||||
for icon in "$SRC"/config/icons/ekklesia-*.svg; do
|
||||
[ -e "$icon" ] || continue
|
||||
install -Dm644 "$icon" \
|
||||
"/usr/share/icons/hicolor/scalable/apps/$(basename "$icon")"
|
||||
done
|
||||
|
||||
install -Dm644 "$SRC/config/wallpapers/classroom-calm.png" \
|
||||
/usr/share/backgrounds/ekklesia/classroom-calm.png
|
||||
|
||||
install -Dm644 "$SRC/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf" \
|
||||
/etc/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf
|
||||
|
||||
for script in classroom-browser classroom-wonderink; do
|
||||
[ -e "$SRC/scripts/$script" ] || continue
|
||||
install -Dm755 "$SRC/scripts/$script" "/usr/local/bin/$script"
|
||||
done
|
||||
|
||||
command -v gtk-update-icon-cache >/dev/null 2>&1 \
|
||||
&& gtk-update-icon-cache -qtf /usr/share/icons/hicolor || true
|
||||
command -v update-desktop-database >/dev/null 2>&1 \
|
||||
&& update-desktop-database -q /usr/share/applications || true
|
||||
|
||||
echo "classroom desktop installed"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Run a KWin script in the session under test and print what it logged.
|
||||
#
|
||||
# KWin's scripting interface is the only way to ask the compositor what it
|
||||
# actually believes about a window — class, caption, desktop file, geometry,
|
||||
# minimised, keep-above. Everything this experiment claims about window
|
||||
# behaviour is measured through here rather than inferred from a screenshot.
|
||||
set -euo pipefail
|
||||
|
||||
NAME="${LAB_NAME:-classroom-v2-lab}"
|
||||
SCRIPT="${1:?usage: kwin-script FILE}"
|
||||
ID="labscript$RANDOM"
|
||||
|
||||
# /tmp inside the lab is a tmpfs created after the container started, which
|
||||
# `docker cp` cannot see, so the script is piped in instead.
|
||||
docker exec -i "$NAME" sh -c "cat > /tmp/$ID.js && chown classroom /tmp/$ID.js" \
|
||||
< "$SCRIPT"
|
||||
|
||||
run() {
|
||||
docker exec -u classroom \
|
||||
-e XDG_RUNTIME_DIR=/run/user/1000 \
|
||||
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
|
||||
-e HOME=/home/classroom \
|
||||
"$NAME" "$@"
|
||||
}
|
||||
|
||||
since=$(docker exec "$NAME" date '+%Y-%m-%d %H:%M:%S')
|
||||
number=$(run qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript \
|
||||
"/tmp/$ID.js" "$ID" 2>/dev/null | tr -d '\r')
|
||||
run qdbus6 org.kde.KWin "/Scripting/Script$number" org.kde.kwin.Script.run >/dev/null 2>&1
|
||||
sleep 1
|
||||
run qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.unloadScript "$ID" >/dev/null 2>&1 || true
|
||||
|
||||
# KWin's print() reaches the journal, tagged by the wrapper process.
|
||||
docker exec "$NAME" journalctl --since "$since" --no-pager -o cat 2>/dev/null \
|
||||
| grep -a "LAB:" | sed 's/.*LAB://'
|
||||
|
||||
+40
-6
@@ -75,8 +75,19 @@ down)
|
||||
;;
|
||||
|
||||
outer-up)
|
||||
# A headless compositor has no input devices, and a wl_seat with no
|
||||
# pointer capability never gives the session under test a pointer to
|
||||
# click with — sway's cursor commands then report success and move
|
||||
# nothing. So the lab runs wlroots' libinput backend as well, over the
|
||||
# devices the privileged container inherits, with seatd standing in for
|
||||
# the login session it does not have. Clicks in this experiment are
|
||||
# real pointer events arriving through libinput.
|
||||
docker exec "$NAME" sh -c 'pgrep -x seatd >/dev/null || \
|
||||
(SEATD_VTBOUND=0 setsid seatd -g input > /tmp/seatd.log 2>&1 &)'
|
||||
docker exec "$NAME" usermod -aG input classroom 2>/dev/null || true
|
||||
sleep 2
|
||||
session_env \
|
||||
-e WLR_BACKENDS=headless -e WLR_LIBINPUT_NO_DEVICES=1 \
|
||||
-e WLR_BACKENDS=headless,libinput -e LIBSEAT_BACKEND=seatd \
|
||||
-e WLR_RENDERER=pixman -e LIBGL_ALWAYS_SOFTWARE=1 \
|
||||
-d "$NAME" sh -c 'exec sway -c /srv/classroom/tools/lab/outer-sway.conf \
|
||||
> /tmp/outer-sway.log 2>&1'
|
||||
@@ -91,6 +102,7 @@ plasma-up)
|
||||
session_env \
|
||||
-e WAYLAND_DISPLAY="$OUTER" -e XDG_SESSION_TYPE=wayland \
|
||||
-e XDG_CURRENT_DESKTOP=KDE -e LIBGL_ALWAYS_SOFTWARE=1 \
|
||||
-e QT_LOGGING_RULES="kwin_scripting.debug=true" \
|
||||
-d "$NAME" sh -c 'exec startplasma-wayland > /tmp/plasma.log 2>&1'
|
||||
for _ in $(seq 1 60); do
|
||||
docker exec "$NAME" pgrep -x plasmashell >/dev/null 2>&1 && break
|
||||
@@ -114,27 +126,45 @@ session)
|
||||
;;
|
||||
|
||||
shot)
|
||||
# shot NAME [--delay SECONDS]
|
||||
# shot NAME [GEOMETRY] — geometry as grim wants it, e.g. "0,980 1920x100"
|
||||
name="${2:?usage: lab shot NAME}"
|
||||
outer grim /tmp/lab-shot.png >/dev/null
|
||||
if [[ -n "${3:-}" ]]; then
|
||||
outer grim -g "$3" /tmp/lab-shot.png >/dev/null
|
||||
else
|
||||
outer grim /tmp/lab-shot.png >/dev/null
|
||||
fi
|
||||
mkdir -p "$SHOTS"
|
||||
docker exec "$NAME" cat /tmp/lab-shot.png > "$SHOTS/$name.png"
|
||||
echo "$SHOTS/$name.png"
|
||||
;;
|
||||
|
||||
click)
|
||||
# click X Y — a real pointer press in the session under test
|
||||
# Absolute, through sway's own seat. This only works because the seat
|
||||
# has a pointer capability at all, which is what the uinput device in
|
||||
# `outer-up` is for.
|
||||
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
||||
sleep 0.3
|
||||
sleep 0.4
|
||||
outer swaymsg seat - cursor press button1 >/dev/null
|
||||
sleep 0.1
|
||||
sleep 0.15
|
||||
outer swaymsg seat - cursor release button1 >/dev/null
|
||||
sleep 0.5
|
||||
;;
|
||||
|
||||
move)
|
||||
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
||||
;;
|
||||
|
||||
drag)
|
||||
outer swaymsg seat - cursor set "$2" "$3" >/dev/null
|
||||
sleep 0.3
|
||||
outer swaymsg seat - cursor press button1 >/dev/null
|
||||
sleep 0.3
|
||||
outer swaymsg seat - cursor set "$4" "$5" >/dev/null
|
||||
sleep 0.3
|
||||
outer swaymsg seat - cursor release button1 >/dev/null
|
||||
sleep 0.4
|
||||
;;
|
||||
|
||||
key)
|
||||
shift
|
||||
outer wtype "$@"
|
||||
@@ -148,6 +178,8 @@ sh)
|
||||
run) shift; session_env "$NAME" "$@" ;;
|
||||
irun) shift; session_env -it "$NAME" "$@" ;;
|
||||
inner) shift; inner "$@" ;;
|
||||
inner-d) shift; session_env -d -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE \
|
||||
-e QT_QPA_PLATFORM=wayland "$NAME" "$@" ;;
|
||||
inner-it) shift; session_env -it -e WAYLAND_DISPLAY=wayland-0 -e XDG_CURRENT_DESKTOP=KDE "$NAME" "$@" ;;
|
||||
outer) shift; outer "$@" ;;
|
||||
|
||||
@@ -164,9 +196,11 @@ usage: tools/lab/lab COMMAND
|
||||
plasma-down stop the Plasma session
|
||||
shot NAME capture the screen to docs/screenshots/NAME.png
|
||||
click X Y press the pointer at a screen position
|
||||
drag X1 Y1 X2 Y2 drag from one position to another
|
||||
move X Y move the pointer
|
||||
key ARGS... wtype, aimed at the session
|
||||
inner CMD... run CMD as a client of the Plasma session
|
||||
inner-d CMD... the same, detached
|
||||
outer CMD... run CMD against the lab compositor
|
||||
run CMD... run CMD as the classroom user, no display
|
||||
sh root shell in the lab
|
||||
|
||||
Reference in New Issue
Block a user