detached f2d166b

record.sh RawHistoryFolder

1#!/usr/bin/env bash
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,
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.
11# KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up.
12set -euo pipefail
13root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
14cd "$root"
15for command in docker go ffmpeg; do
16    command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
17done
18docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; }
19amd=''
20for vendor in /sys/class/drm/renderD*/device/vendor; do
21    if [[ -r $vendor && $(<"$vendor") == 0x1002 ]]; then amd=yes; fi
22done
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}
28work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")
29launcher='' feeder='' success=''
30container=neferwl-demo-$$
31cleanup() {
32    local status=$?
33    trap - EXIT
34    # launcher and feeder run with job control (set -m) in their own process
35    # groups, so killing the group also stops their sleep/docker exec children.
36    for pid in "$launcher" "$feeder"; do
37        if [[ -n $pid ]]; then kill -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true; fi
38    done
39    docker rm -f "$container" >/dev/null 2>&1 || true
40    for pid in "$launcher" "$feeder"; do
41        if [[ -n $pid ]]; then wait "$pid" 2>/dev/null || true; fi
42    done
43    exec 3>&-
44    if [[ $success != yes || ${KEEP_CAPTURE:-0} != 1 ]]; then
45        rm -rf -- "$work"
46    fi
47    exit "$status"
48}
49trap cleanup EXIT
50trap 'exit 130' INT
51trap 'exit 143' TERM
52printf 'Capture directory: %s\n' "$work"
53mkdir -p "$work/shots" "$work/state" docs/media
54docker build -t neferwl-demo -f tools/demo/Dockerfile tools/demo
55CGO_ENABLED=0 go build -o "$work/neferwl" ./cmd/neferwl
56log=$work/state/neferwl/runs/headless/latest.log
57mkfifo "$work/input"
58# Hold both ends open so the FIFO cannot block compositor startup or produce
59# premature EOF. No input is fed until the first app is ready.
60exec 3<>"$work/input"
61# The compositor is the container's main process, so the container ends with
62# it. Root in a rootless container is the host user: the bind-mounted work
63# directory (binary, FIFO, frames, log, hint dumps) stays owned by the host user.
64# The GPU render node gives the compositor the sync files that hint scans need.
65docker run -d --name "$container" --hostname neferwl-demo --device /dev/dri \
66    -e XDG_STATE_HOME=/work/state -e NEFERWL_HINT_DEBUG=1 \
67    -v "$work:/work" -v "$work/neferwl:/usr/local/bin/neferwl:ro" neferwl-demo \
68    --backend=headless --size 1920x1080 --config /home/demo/config --no-xwayland \
69    --screenshot /work/shots --input /work/input --timeout 75s >/dev/null
70display=''
71deadline=$((SECONDS + 30))
72while ((SECONDS < deadline)); do
73    [[ $(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null) == true ]] ||
74        { echo "Compositor exited before readiness; see $log" >&2; docker logs "$container" >&2 || true; exit 1; }
75    if [[ -f $log ]]; then
76        display=$(sed -n '/"message":"listening"/s/.*"WAYLAND_DISPLAY":"\([^" ]*\)".*/\1/p' "$log" | tail -1)
77        [[ -n $display ]] && break
78    fi
79    sleep 0.1
80done
81[[ -n $display ]] || { echo "Timed out after 30s waiting for the compositor to listen; see $log" >&2; exit 1; }
82# Mousepad runs as another user (see launch_apps) and needs the socket.
83docker exec "$container" chmod 666 "/run/wl/${display##*/}"
84app() { docker exec -d -e "WAYLAND_DISPLAY=${display##*/}" "$container" "$@"; }
85at() { local delay=$(($1 - SECONDS)); if ((delay > 0)); then sleep "$delay"; fi; }
86launch_apps() {
87    # This shell timeline shares t=0 with input.txt. Only keys live in input.txt;
88    # app launches cannot be invoked by compositor input scripts.
89    at 3
90    app firefox --no-remote --profile /root/firefox 'https://duckduckgo.com/?q=wayland+compositor'
91    # btop's first per-core sample is bogus in this container. Let it collect
92    # real deltas on workspace 2; input.txt brings it into view at t=17.
93    at 5
94    app foot --app-id demo-monitor --title 'Monitor / btop' /usr/local/bin/btop
95    at 9
96    # Mousepad runs as demo (it warns about root), with a file-backed
97    # GSettings and no session bus (it needs the root-owned bus otherwise).
98    app setpriv --reuid=1000 --regid=1000 --clear-groups env HOME=/home/demo \
99        GSETTINGS_BACKEND=keyfile DBUS_SESSION_BUS_ADDRESS=disabled: \
100        mousepad --disable-server /home/demo/config
101    at 30
102    app foot --title 'Fonts / workspace 2' bash -c \
103        'eza --tree --level=2 --icons --color=always /usr/share/fonts | head -30; exec sleep infinity'
104}
105feed() {
106    # Forwards input.txt to the compositor in real time. `sleep` lines are
107    # served here (not forwarded), so host-side steps can sit between two
108    # compositor steps:
109    #   hint-read              reads the still page with OCR, in the background
110    #   hint-click <word>...   types the label of the link that shows all the
111    #                          words, from the dump of the scan the last
112    #                          `hint` bind started (demo-hint-label reproduces
113    #                          the labelling of internal/core/hint.go)
114    local line cmd d mark label reader=''
115    while IFS= read -r line || [[ -n $line ]]; do
116        cmd=${line%% *}
117        case $cmd in
118        '' | '#'*) ;;
119        sleep)
120            d=${line#sleep }
121            if [[ $d == *ms ]]; then d=$(printf '%d.%03d' $((${d%ms} / 1000)) $((${d%ms} % 1000))); fi
122            sleep "$d" ;;
123        hint-read)
124            docker exec "$container" demo-hint-label ocr &
125            reader=$! ;;
126        hint-click)
127            [[ -n $reader ]] && wait "$reader"
128            # shellcheck disable=SC2086
129            label=$(docker exec "$container" demo-hint-label label "$mark" ${line#hint-click }) ||
130                { echo "No hint label for: ${line#hint-click }" >&2; docker kill "$container" >/dev/null; return 1; }
131            echo "Hint label for ${line#hint-click }: $label" >&2
132            printf 'type %s\n' "$label" ;;
133        *)
134            [[ $line == 'key Super+g' ]] && mark=$(date +%s.%N)
135            printf '%s\n' "$line" ;;
136        esac
137    done <tools/demo/input.txt
138    printf 'key Ctrl+Alt+BackSpace\n'
139}
140app foot --title 'System / NeferWL' bash -c \
141    'fastfetch --logo small --structure OS:Kernel:WM:Terminal:Memory:Colors; eza -la --no-user --icons --color=always /etc | head -24; exec sleep infinity'
142# The first app is launched before t=0. FIFO input and shell launches now
143# share this epoch, independent of compositor/image/container startup time.
144SECONDS=0
145set -m
146feed >&3 &
147feeder=$!
148launch_apps &
149launcher=$!
150set +m
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.
153status=$(docker wait "$container")
154if grep -q '"reason":"timeout"' "$log"; then
155    echo 'The compositor hit --timeout before the quit key: the recording is truncated.' >&2
156    exit 1
157fi
158[[ $status == 0 ]] || { echo "Compositor failed (exit $status); see $log" >&2; docker logs "$container" >&2 || true; exit 1; }
159wait "$feeder" || { echo 'Scripted input failed.' >&2; exit 1; }
160feeder=
161wait "$launcher"
162launcher=
163# Firefox's links come from the accessibility tree; OCR (tesseract) must still load.
164if grep -qE 'OCR unavailable|English OCR data missing' "$log"; then echo 'tesseract is missing in the image.' >&2; exit 1; fi
165if 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
168fi
169grep -qE '"atspi_targets":[1-9][0-9]*,.*"message":"hint scan"' "$log" ||
170    { echo 'No hint scan found accessibility targets:' >&2; grep -E '"component":"(hints|atspi)"' "$log" | cut -c1-300 >&2; exit 1; }
171grep '"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
172ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \
173    -i "$work/shots/frames.ffconcat" -vf 'fps=30,format=yuv420p' \
174    -c:v libx264 -crf 23 -movflags +faststart docs/media/demo.mp4 -y
175ffmpeg -hide_banner -loglevel warning -ss 8 -i docs/media/demo.mp4 \
176    -frames:v 1 -update 1 docs/media/demo.png -y
177success=yes
178if [[ ${KEEP_CAPTURE:-0} == 1 ]]; then printf 'Captures kept: %s\n' "$work"; fi
179printf 'Recorded: docs/media/demo.mp4 and docs/media/demo.png\n'

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 23

Tags 10

↑↓ moveenter openesc close