detached 95445f0

fix(demo): fail truncated recordings and document the AMD GPU requirement

commit
95445f0186132715bbf114dff5a60482daa8920e
author
brice · · 2026-10-22 04:56 UTC
parent
b35bb20
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
1/2
4 files · +44 -17
M docs/dev/headless.md +4 -2
@@ -7,7 +7,7 @@ neferwl --backend=headless --screenshot /tmp/neferwl-shots
77 WAYLAND_DISPLAY=<name from the log> foot
88 ```
99
10-- `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows.
10+- `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. `latest.png` is a hard link to the newest frame, so replace it rather than editing it in place.
1111 - `--size 640x360,640x360` creates two outputs, arranged left to right,
1212 for testing `focus-monitor-left/right` and moving columns between outputs.
1313 - With several outputs, each writes to `<dir>/HEADLESS-<n>/`.
@@ -25,12 +25,14 @@ ffmpeg -f concat -safe 0 -i /tmp/shots/frames.ffconcat -vf "fps=60,format=yuv420
2525 For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filters.
2626
2727 ### Demo recording
28-Run `tools/demo/record.sh` with Go 1.27, rootless Docker, ffmpeg and network access installed.
28+Run `tools/demo/record.sh`. It requires Go 1.27, rootless Docker, ffmpeg, network access and an AMD GPU.
2929 It builds the Arch image (apps, tesseract, AT-SPI) and the compositor binary, then records the 1080p README demo.
3030 The compositor runs headless inside a disposable container, with the binary and a work directory bind-mounted and `/dev/dri` passed for the GPU.
31+The image only carries Mesa's radeon Vulkan driver, and hint scans need the sync files that a hardware driver exports; the script stops early without an AMD render node.
3132 Firefox opens a live DuckDuckGo page, so the recording needs the network and its content varies.
3233 A FIFO feeds `input.txt` while the script launches the apps on the same clock.
3334 The hint step makes Firefox fullscreen and labels the page with `hint`; `hint-click <words>` then types the label of the link that shows those words, found from the scan's dump and an OCR read of the still page (`hint-read`).
35+The recording only counts if the compositor stops on the quit key that ends `input.txt`; reaching its `--timeout` first fails the run as truncated.
3436 Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs.
3537 The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.
3638
@@ -7,7 +7,7 @@ neferwl --backend=headless --screenshot /tmp/neferwl-shots
7 WAYLAND_DISPLAY=<name from the log> foot7 WAYLAND_DISPLAY=<name from the log> foot
8 ```8 ```
9 9
10-- `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows.10+- `latest.png` shows the current frame; `frames.ffconcat` records frame timing. Every frame is also saved as `frame-NNNNNN.png`, so the directory grows. `latest.png` is a hard link to the newest frame, so replace it rather than editing it in place.
11 - `--size 640x360,640x360` creates two outputs, arranged left to right,11 - `--size 640x360,640x360` creates two outputs, arranged left to right,
12 for testing `focus-monitor-left/right` and moving columns between outputs.12 for testing `focus-monitor-left/right` and moving columns between outputs.
13 - With several outputs, each writes to `<dir>/HEADLESS-<n>/`.13 - With several outputs, each writes to `<dir>/HEADLESS-<n>/`.
@@ -25,12 +25,14 @@ ffmpeg -f concat -safe 0 -i /tmp/shots/frames.ffconcat -vf "fps=60,format=yuv420
25 For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filters.25 For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filters.
26 26
27 ### Demo recording27 ### Demo recording
28-Run `tools/demo/record.sh` with Go 1.27, rootless Docker, ffmpeg and network access installed.28+Run `tools/demo/record.sh`. It requires Go 1.27, rootless Docker, ffmpeg, network access and an AMD GPU.
29 It builds the Arch image (apps, tesseract, AT-SPI) and the compositor binary, then records the 1080p README demo.29 It builds the Arch image (apps, tesseract, AT-SPI) and the compositor binary, then records the 1080p README demo.
30 The compositor runs headless inside a disposable container, with the binary and a work directory bind-mounted and `/dev/dri` passed for the GPU.30 The compositor runs headless inside a disposable container, with the binary and a work directory bind-mounted and `/dev/dri` passed for the GPU.
31+The image only carries Mesa's radeon Vulkan driver, and hint scans need the sync files that a hardware driver exports; the script stops early without an AMD render node.
31 Firefox opens a live DuckDuckGo page, so the recording needs the network and its content varies.32 Firefox opens a live DuckDuckGo page, so the recording needs the network and its content varies.
32 A FIFO feeds `input.txt` while the script launches the apps on the same clock.33 A FIFO feeds `input.txt` while the script launches the apps on the same clock.
33 The hint step makes Firefox fullscreen and labels the page with `hint`; `hint-click <words>` then types the label of the link that shows those words, found from the scan's dump and an OCR read of the still page (`hint-read`).34 The hint step makes Firefox fullscreen and labels the page with `hint`; `hint-click <words>` then types the label of the link that shows those words, found from the scan's dump and an OCR read of the still page (`hint-read`).
35+The recording only counts if the compositor stops on the quit key that ends `input.txt`; reaching its `--timeout` first fails the run as truncated.
34 Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs.36 Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs.
35 The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.37 The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.
36 38
M tools/demo/Dockerfile +2 -1
@@ -16,7 +16,7 @@ COPY foot.ini /root/.config/foot/foot.ini
1616 COPY btop.conf /root/.config/btop/btop.conf
1717 COPY btop-vesper.theme /root/.config/btop/themes/vesper.theme
1818 COPY demo-entry demo-hint-label /usr/local/bin/
19-COPY config /root/config
19+# One config for both users: the compositor (root) reads it, Mousepad (demo) edits it.
2020 COPY config /home/demo/config
2121 COPY mousepad.keyfile /home/demo/.config/glib-2.0/settings/keyfile
2222 RUN chown -R demo:demo /home/demo
@@ -24,6 +24,7 @@ ENV HOME=/root XDG_RUNTIME_DIR=/run/wl WAYLAND_DISPLAY=wayland-0 \
2424 GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \
2525 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/wl/bus \
2626 MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor
27+# LIBGL_ALWAYS_SOFTWARE applies to the apps' OpenGL, not to the compositor (Vulkan).
2728 WORKDIR /root
2829 # Everything runs as the container's root, which rootless Docker maps to the
2930 # host user, so the bind-mounted work directory stays owned by the host user.
@@ -16,7 +16,7 @@ COPY foot.ini /root/.config/foot/foot.ini
16 COPY btop.conf /root/.config/btop/btop.conf16 COPY btop.conf /root/.config/btop/btop.conf
17 COPY btop-vesper.theme /root/.config/btop/themes/vesper.theme17 COPY btop-vesper.theme /root/.config/btop/themes/vesper.theme
18 COPY demo-entry demo-hint-label /usr/local/bin/18 COPY demo-entry demo-hint-label /usr/local/bin/
19-COPY config /root/config19+# One config for both users: the compositor (root) reads it, Mousepad (demo) edits it.
20 COPY config /home/demo/config20 COPY config /home/demo/config
21 COPY mousepad.keyfile /home/demo/.config/glib-2.0/settings/keyfile21 COPY mousepad.keyfile /home/demo/.config/glib-2.0/settings/keyfile
22 RUN chown -R demo:demo /home/demo22 RUN chown -R demo:demo /home/demo
@@ -24,6 +24,7 @@ ENV HOME=/root XDG_RUNTIME_DIR=/run/wl WAYLAND_DISPLAY=wayland-0 \
24 GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \24 GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \
25 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/wl/bus \25 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/wl/bus \
26 MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor26 MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor
27+# LIBGL_ALWAYS_SOFTWARE applies to the apps' OpenGL, not to the compositor (Vulkan).
27 WORKDIR /root28 WORKDIR /root
28 # Everything runs as the container's root, which rootless Docker maps to the29 # Everything runs as the container's root, which rootless Docker maps to the
29 # host user, so the bind-mounted work directory stays owned by the host user.30 # host user, so the bind-mounted work directory stays owned by the host user.
M tools/demo/demo-hint-label +6 -3
@@ -33,7 +33,8 @@ def ocr():
3333 if text and float(row["conf"]) > 30:
3434 x, y, w, h = (int(row[k]) for k in ("left", "top", "width", "height"))
3535 words.append([text, x + w / 2, y + h / 2])
36- json.dump(words, open(WORDS, "w"))
36+ with open(WORDS, "w") as f:
37+ json.dump(words, f)
3738
3839
3940 def newest_dump(since):
@@ -47,8 +48,10 @@ def newest_dump(since):
4748
4849
4950 def label(since, wanted):
50- meta = json.load(open(newest_dump(since)))
51- words = json.load(open(WORDS))
51+ with open(newest_dump(since)) as f:
52+ meta = json.load(f)
53+ with open(WORDS) as f:
54+ words = json.load(f)
5255 # Core keeps the targets whose centre is on the output, then sorts them.
5356 width, height = meta["width"], meta["height"]
5457 targets = [t for t in meta["targets"]
@@ -33,7 +33,8 @@ def ocr():
33 if text and float(row["conf"]) > 30:33 if text and float(row["conf"]) > 30:
34 x, y, w, h = (int(row[k]) for k in ("left", "top", "width", "height"))34 x, y, w, h = (int(row[k]) for k in ("left", "top", "width", "height"))
35 words.append([text, x + w / 2, y + h / 2])35 words.append([text, x + w / 2, y + h / 2])
36- json.dump(words, open(WORDS, "w"))36+ with open(WORDS, "w") as f:
37+ json.dump(words, f)
37 38
38 39
39 def newest_dump(since):40 def newest_dump(since):
@@ -47,8 +48,10 @@ def newest_dump(since):
47 48
48 49
49 def label(since, wanted):50 def label(since, wanted):
50- meta = json.load(open(newest_dump(since)))51+ with open(newest_dump(since)) as f:
51- words = json.load(open(WORDS))52+ meta = json.load(f)
53+ with open(WORDS) as f:
54+ words = json.load(f)
52 # Core keeps the targets whose centre is on the output, then sorts them.55 # Core keeps the targets whose centre is on the output, then sorts them.
53 width, height = meta["width"], meta["height"]56 width, height = meta["width"], meta["height"]
54 targets = [t for t in meta["targets"]57 targets = [t for t in meta["targets"]
M tools/demo/record.sh +32 -11
@@ -1,7 +1,13 @@
11 #!/usr/bin/env bash
2-# Requires Go 1.27, rootless Docker, bash, ffmpeg and network access.
2+# Requires Go 1.27, rootless Docker, bash, ffmpeg, network access and an AMD GPU.
33 # Run from any directory. The image holds the compositor's runtime (tesseract,
4-# AT-SPI, a Mesa Vulkan driver) and the apps; the host builds the binary.
4+# AT-SPI, Mesa's radeon Vulkan driver) and the apps; the host builds the binary.
5+# The compositor renders on the host GPU through /dev/dri, and hint scans need
6+# the sync files only a hardware driver exports. The image only carries
7+# vulkan-radeon, so another GPU needs its Mesa driver in tools/demo/Dockerfile
8+# and the vendor check below changed.
9+# A recording only succeeds if the compositor stops on the quit key that ends
10+# tools/demo/input.txt: hitting --timeout first means a truncated video.
511 # KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up.
612 set -euo pipefail
713 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
@@ -10,6 +16,15 @@ for command in docker go ffmpeg; do
1016 command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
1117 done
1218 docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; }
19+amd=''
20+for vendor in /sys/class/drm/renderD*/device/vendor; do
21+ if [[ -r $vendor && $(<"$vendor") == 0x1002 ]]; then amd=yes; fi
22+done
23+[[ -n $amd ]] || {
24+ echo 'No AMD GPU render node (PCI vendor 0x1002) found: the demo image only carries the radeon Vulkan driver.' >&2
25+ echo 'See the header of this script to use another GPU.' >&2
26+ exit 1
27+}
1328 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")
1429 launcher='' feeder='' success=''
1530 container=neferwl-demo-$$
@@ -47,15 +62,14 @@ exec 3<>"$work/input"
4762 # it. Root in a rootless container is the host user: the bind-mounted work
4863 # directory (binary, FIFO, frames, log, hint dumps) stays owned by the host user.
4964 # The GPU render node gives the compositor the sync files that hint scans need.
50-gpu=()
51-if [[ -d /dev/dri ]]; then gpu=(--device /dev/dri); else echo 'No /dev/dri: hint scans will fail.' >&2; fi
52-docker run -d --name "$container" --hostname neferwl-demo "${gpu[@]}" \
65+docker run -d --name "$container" --hostname neferwl-demo --device /dev/dri \
5366 -e XDG_STATE_HOME=/work/state -e NEFERWL_HINT_DEBUG=1 \
5467 -v "$work:/work" -v "$work/neferwl:/usr/local/bin/neferwl:ro" neferwl-demo \
55- --backend=headless --size 1920x1080 --config /root/config --no-xwayland \
68+ --backend=headless --size 1920x1080 --config /home/demo/config --no-xwayland \
5669 --screenshot /work/shots --input /work/input --timeout 75s >/dev/null
5770 display=''
58-for ((attempt = 0; attempt < 300; attempt++)); do
71+deadline=$((SECONDS + 30))
72+while ((SECONDS < deadline)); do
5973 [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == true ]] ||
6074 { echo "Compositor exited before readiness; see $log" >&2; docker logs "$container" >&2 || true; exit 1; }
6175 if [[ -f $log ]]; then
@@ -64,7 +78,7 @@ for ((attempt = 0; attempt < 300; attempt++)); do
6478 fi
6579 sleep 0.1
6680 done
67-[[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor; see $log" >&2; exit 1; }
81+[[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor to listen; see $log" >&2; exit 1; }
6882 # Mousepad runs as another user (see launch_apps) and needs the socket.
6983 docker exec "$container" chmod 666 "/run/wl/${display##*/}"
7084 app() { docker exec -d -e "WAYLAND_DISPLAY=${display##*/}" "$container" "$@"; }
@@ -134,17 +148,24 @@ feeder=$!
134148 launch_apps &
135149 launcher=$!
136150 set +m
137-# The compositor ends with the quit key at the end of input.txt, or at --timeout.
151+# The compositor ends with the quit key at the end of input.txt. Hitting
152+# --timeout first (a stalled timeline) leaves a truncated recording: that fails.
138153 status=$(docker wait "$container")
139-if [[ $status != 0 ]]; then
140- grep -q '"reason":"timeout"' "$log" || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; }
154+if grep -q '"reason":"timeout"' "$log"; then
155+ echo 'The compositor hit --timeout before the quit key: the recording is truncated.' >&2
156+ exit 1
141157 fi
158+[[ $status == 0 ]] || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; }
142159 wait "$feeder" || { echo 'Scripted input failed.' >&2; exit 1; }
143160 feeder=
144161 wait "$launcher"
145162 launcher=
146163 # Firefox's links come from the accessibility tree; OCR (tesseract) must still load.
147164 if grep -qE 'OCR unavailable|English OCR data missing' "$log"; then echo 'tesseract is missing in the image.' >&2; exit 1; fi
165+if grep -q 'capture needs sync file export' "$log"; then
166+ echo 'Hint scans failed: the compositor is not on a hardware Vulkan driver (radeon); see the header of this script.' >&2
167+ exit 1
168+fi
148169 grep -qE '"atspi_targets":[1-9][0-9]*,.*"message":"hint scan"' "$log" ||
149170 { echo 'No hint scan found accessibility targets:' >&2; grep -E '"component":"(hints|atspi)"' "$log" | cut -c1-300 >&2; exit 1; }
150171 grep '"message":"hint scan"' "$log" | sed -E 's/.*"atspi_targets":([0-9]+).*"ocr_links":([0-9]+).*"targets":([0-9]+).*/hint scan: \1 accessibility + \2 OCR links = \3 targets/' | tail -3
@@ -1,7 +1,13 @@
1 #!/usr/bin/env bash1 #!/usr/bin/env bash
2-# Requires Go 1.27, rootless Docker, bash, ffmpeg and network access.2+# Requires Go 1.27, rootless Docker, bash, ffmpeg, network access and an AMD GPU.
3 # Run from any directory. The image holds the compositor's runtime (tesseract,3 # Run from any directory. The image holds the compositor's runtime (tesseract,
4-# AT-SPI, a Mesa Vulkan driver) and the apps; the host builds the binary.4+# AT-SPI, Mesa's radeon Vulkan driver) and the apps; the host builds the binary.
5+# The compositor renders on the host GPU through /dev/dri, and hint scans need
6+# the sync files only a hardware driver exports. The image only carries
7+# vulkan-radeon, so another GPU needs its Mesa driver in tools/demo/Dockerfile
8+# and the vendor check below changed.
9+# A recording only succeeds if the compositor stops on the quit key that ends
10+# tools/demo/input.txt: hitting --timeout first means a truncated video.
5 # KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up.11 # KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up.
6 set -euo pipefail12 set -euo pipefail
7 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)13 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
@@ -10,6 +16,15 @@ for command in docker go ffmpeg; do
10 command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }16 command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
11 done17 done
12 docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; }18 docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; }
19+amd=''
20+for vendor in /sys/class/drm/renderD*/device/vendor; do
21+ if [[ -r $vendor && $(<"$vendor") == 0x1002 ]]; then amd=yes; fi
22+done
23+[[ -n $amd ]] || {
24+ echo 'No AMD GPU render node (PCI vendor 0x1002) found: the demo image only carries the radeon Vulkan driver.' >&2
25+ echo 'See the header of this script to use another GPU.' >&2
26+ exit 1
27+}
13 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")28 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")
14 launcher='' feeder='' success=''29 launcher='' feeder='' success=''
15 container=neferwl-demo-$$30 container=neferwl-demo-$$
@@ -47,15 +62,14 @@ exec 3<>"$work/input"
47 # it. Root in a rootless container is the host user: the bind-mounted work62 # it. Root in a rootless container is the host user: the bind-mounted work
48 # directory (binary, FIFO, frames, log, hint dumps) stays owned by the host user.63 # directory (binary, FIFO, frames, log, hint dumps) stays owned by the host user.
49 # The GPU render node gives the compositor the sync files that hint scans need.64 # The GPU render node gives the compositor the sync files that hint scans need.
50-gpu=()65+docker run -d --name "$container" --hostname neferwl-demo --device /dev/dri \
51-if [[ -d /dev/dri ]]; then gpu=(--device /dev/dri); else echo 'No /dev/dri: hint scans will fail.' >&2; fi
52-docker run -d --name "$container" --hostname neferwl-demo "${gpu[@]}" \
53 -e XDG_STATE_HOME=/work/state -e NEFERWL_HINT_DEBUG=1 \66 -e XDG_STATE_HOME=/work/state -e NEFERWL_HINT_DEBUG=1 \
54 -v "$work:/work" -v "$work/neferwl:/usr/local/bin/neferwl:ro" neferwl-demo \67 -v "$work:/work" -v "$work/neferwl:/usr/local/bin/neferwl:ro" neferwl-demo \
55- --backend=headless --size 1920x1080 --config /root/config --no-xwayland \68+ --backend=headless --size 1920x1080 --config /home/demo/config --no-xwayland \
56 --screenshot /work/shots --input /work/input --timeout 75s >/dev/null69 --screenshot /work/shots --input /work/input --timeout 75s >/dev/null
57 display=''70 display=''
58-for ((attempt = 0; attempt < 300; attempt++)); do71+deadline=$((SECONDS + 30))
72+while ((SECONDS < deadline)); do
59 [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == true ]] ||73 [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == true ]] ||
60 { echo "Compositor exited before readiness; see $log" >&2; docker logs "$container" >&2 || true; exit 1; }74 { echo "Compositor exited before readiness; see $log" >&2; docker logs "$container" >&2 || true; exit 1; }
61 if [[ -f $log ]]; then75 if [[ -f $log ]]; then
@@ -64,7 +78,7 @@ for ((attempt = 0; attempt < 300; attempt++)); do
64 fi78 fi
65 sleep 0.179 sleep 0.1
66 done80 done
67-[[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor; see $log" >&2; exit 1; }81+[[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor to listen; see $log" >&2; exit 1; }
68 # Mousepad runs as another user (see launch_apps) and needs the socket.82 # Mousepad runs as another user (see launch_apps) and needs the socket.
69 docker exec "$container" chmod 666 "/run/wl/${display##*/}"83 docker exec "$container" chmod 666 "/run/wl/${display##*/}"
70 app() { docker exec -d -e "WAYLAND_DISPLAY=${display##*/}" "$container" "$@"; }84 app() { docker exec -d -e "WAYLAND_DISPLAY=${display##*/}" "$container" "$@"; }
@@ -134,17 +148,24 @@ feeder=$!
134 launch_apps &148 launch_apps &
135 launcher=$!149 launcher=$!
136 set +m150 set +m
137-# The compositor ends with the quit key at the end of input.txt, or at --timeout.151+# The compositor ends with the quit key at the end of input.txt. Hitting
152+# --timeout first (a stalled timeline) leaves a truncated recording: that fails.
138 status=$(docker wait "$container")153 status=$(docker wait "$container")
139-if [[ $status != 0 ]]; then154+if grep -q '"reason":"timeout"' "$log"; then
140- grep -q '"reason":"timeout"' "$log" || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; }155+ echo 'The compositor hit --timeout before the quit key: the recording is truncated.' >&2
156+ exit 1
141 fi157 fi
158+[[ $status == 0 ]] || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; }
142 wait "$feeder" || { echo 'Scripted input failed.' >&2; exit 1; }159 wait "$feeder" || { echo 'Scripted input failed.' >&2; exit 1; }
143 feeder=160 feeder=
144 wait "$launcher"161 wait "$launcher"
145 launcher=162 launcher=
146 # Firefox's links come from the accessibility tree; OCR (tesseract) must still load.163 # Firefox's links come from the accessibility tree; OCR (tesseract) must still load.
147 if grep -qE 'OCR unavailable|English OCR data missing' "$log"; then echo 'tesseract is missing in the image.' >&2; exit 1; fi164 if grep -qE 'OCR unavailable|English OCR data missing' "$log"; then echo 'tesseract is missing in the image.' >&2; exit 1; fi
165+if grep -q 'capture needs sync file export' "$log"; then
166+ echo 'Hint scans failed: the compositor is not on a hardware Vulkan driver (radeon); see the header of this script.' >&2
167+ exit 1
168+fi
148 grep -qE '"atspi_targets":[1-9][0-9]*,.*"message":"hint scan"' "$log" ||169 grep -qE '"atspi_targets":[1-9][0-9]*,.*"message":"hint scan"' "$log" ||
149 { echo 'No hint scan found accessibility targets:' >&2; grep -E '"component":"(hints|atspi)"' "$log" | cut -c1-300 >&2; exit 1; }170 { echo 'No hint scan found accessibility targets:' >&2; grep -E '"component":"(hints|atspi)"' "$log" | cut -c1-300 >&2; exit 1; }
150 grep '"message":"hint scan"' "$log" | sed -E 's/.*"atspi_targets":([0-9]+).*"ocr_links":([0-9]+).*"targets":([0-9]+).*/hint scan: \1 accessibility + \2 OCR links = \3 targets/' | tail -3171 grep '"message":"hint scan"' "$log" | sed -E 's/.*"atspi_targets":([0-9]+).*"ocr_links":([0-9]+).*"targets":([0-9]+).*/hint scan: \1 accessibility + \2 OCR links = \3 targets/' | tail -3

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 24

Tags 10

↑↓ moveenter openesc close