detached 87924a1

fix(demo): robust startup, timeline sync and cleanup in record.sh

commit
87924a1cf937b8f6fbad38ce94b645b8bc1fa2fd
author
brice · · 2026-10-21 05:26 UTC
parent
9d71793
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
12 files · +153 -57
M docs/dev/headless.md +2 -1
@@ -28,7 +28,8 @@ For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filter
2828 Run `tools/demo/record.sh` with Go 1.27, rootless Docker and ffmpeg installed.
2929 It builds an Arch app image and the host compositor, then records the 1080p README demo.
3030 Apps run in a disposable, offline container; only the headless Wayland socket is mounted.
31-The shell timeline launches apps, while `input.txt` drives keys; captures stay at the printed path.
31+A FIFO synchronizes `input.txt` with app launches after container startup.
32+Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs.
3233 The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.
3334
3435 ## Input scripts
@@ -28,7 +28,8 @@ For a GIF, convert `demo.mp4` with ffmpeg's `palettegen` and `paletteuse` filter
28 Run `tools/demo/record.sh` with Go 1.27, rootless Docker and ffmpeg installed.28 Run `tools/demo/record.sh` with Go 1.27, rootless Docker and ffmpeg installed.
29 It builds an Arch app image and the host compositor, then records the 1080p README demo.29 It builds an Arch app image and the host compositor, then records the 1080p README demo.
30 Apps run in a disposable, offline container; only the headless Wayland socket is mounted.30 Apps run in a disposable, offline container; only the headless Wayland socket is mounted.
31-The shell timeline launches apps, while `input.txt` drives keys; captures stay at the printed path.31+A FIFO synchronizes `input.txt` with app launches after container startup.
32+Temporary captures are removed by default; set `KEEP_CAPTURE=1` to keep successful runs.
32 The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.33 The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.
33 34
34 ## Input scripts35 ## Input scripts
M docs/media/demo.mp4 +0 -0 binary file: no text diff

binary file: no text diff

M docs/media/demo.png +0 -0 binary file: no text diff

binary file: no text diff

M tools/demo/Dockerfile +3 -1
@@ -3,7 +3,7 @@ FROM archlinux:latest
33 RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \
44 pacman -Sy --noconfirm archlinux-keyring \
55 && pacman -Syu --noconfirm --needed \
6- firefox foot btop fastfetch eza bat mousepad \
6+ firefox foot btop fastfetch eza mousepad \
77 ttf-jetbrains-mono-nerd noto-fonts noto-fonts-emoji dbus mesa vulkan-swrast
88 # Arch's btop carries file capabilities that rootless Docker cannot grant.
99 RUN install -m 0755 /usr/bin/btop /usr/local/bin/btop
@@ -11,8 +11,10 @@ RUN useradd -m -u 1000 demo \
1111 && mkdir -p /home/demo/firefox /home/demo/.config/btop /run/wl \
1212 && chmod 0700 /run/wl
1313 COPY firefox/user.js /home/demo/firefox/user.js
14+COPY firefox/userChrome.css /home/demo/firefox/chrome/userChrome.css
1415 COPY foot.ini /home/demo/.config/foot/foot.ini
1516 COPY btop.conf /home/demo/.config/btop/btop.conf
17+COPY btop-vesper.theme /home/demo/.config/btop/themes/vesper.theme
1618 COPY site /home/demo/site
1719 COPY config /home/demo/config
1820 RUN chown -R demo:demo /home/demo /run/wl
@@ -3,7 +3,7 @@ FROM archlinux:latest
3 RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \3 RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \
4 pacman -Sy --noconfirm archlinux-keyring \4 pacman -Sy --noconfirm archlinux-keyring \
5 && pacman -Syu --noconfirm --needed \5 && pacman -Syu --noconfirm --needed \
6- firefox foot btop fastfetch eza bat mousepad \6+ firefox foot btop fastfetch eza mousepad \
7 ttf-jetbrains-mono-nerd noto-fonts noto-fonts-emoji dbus mesa vulkan-swrast7 ttf-jetbrains-mono-nerd noto-fonts noto-fonts-emoji dbus mesa vulkan-swrast
8 # Arch's btop carries file capabilities that rootless Docker cannot grant.8 # Arch's btop carries file capabilities that rootless Docker cannot grant.
9 RUN install -m 0755 /usr/bin/btop /usr/local/bin/btop9 RUN install -m 0755 /usr/bin/btop /usr/local/bin/btop
@@ -11,8 +11,10 @@ RUN useradd -m -u 1000 demo \
11 && mkdir -p /home/demo/firefox /home/demo/.config/btop /run/wl \11 && mkdir -p /home/demo/firefox /home/demo/.config/btop /run/wl \
12 && chmod 0700 /run/wl12 && chmod 0700 /run/wl
13 COPY firefox/user.js /home/demo/firefox/user.js13 COPY firefox/user.js /home/demo/firefox/user.js
14+COPY firefox/userChrome.css /home/demo/firefox/chrome/userChrome.css
14 COPY foot.ini /home/demo/.config/foot/foot.ini15 COPY foot.ini /home/demo/.config/foot/foot.ini
15 COPY btop.conf /home/demo/.config/btop/btop.conf16 COPY btop.conf /home/demo/.config/btop/btop.conf
17+COPY btop-vesper.theme /home/demo/.config/btop/themes/vesper.theme
16 COPY site /home/demo/site18 COPY site /home/demo/site
17 COPY config /home/demo/config19 COPY config /home/demo/config
18 RUN chown -R demo:demo /home/demo /run/wl20 RUN chown -R demo:demo /home/demo /run/wl
A tools/demo/btop-vesper.theme +43 -0
@@ -0,0 +1,43 @@
1+# Vesper-inspired warm monochrome, with peach highlights.
2+theme[main_bg]="#101010"
3+theme[main_fg]="#ffffff"
4+theme[title]="#ffc799"
5+theme[hi_fg]="#ffc799"
6+theme[selected_bg]="#282828"
7+theme[selected_fg]="#ffffff"
8+theme[inactive_fg]="#7e7e7e"
9+theme[proc_misc]="#ffc799"
10+theme[cpu_box]="#7e7e7e"
11+theme[mem_box]="#7e7e7e"
12+theme[net_box]="#7e7e7e"
13+theme[proc_box]="#7e7e7e"
14+theme[div_line]="#282828"
15+theme[meter_bg]="#282828"
16+theme[graph_text]="#a0a0a0"
17+theme[temp_start]="#7e7e7e"
18+theme[temp_mid]="#e6b99d"
19+theme[temp_end]="#ffc799"
20+theme[cpu_start]="#7e7e7e"
21+theme[cpu_mid]="#e6b99d"
22+theme[cpu_end]="#ffc799"
23+theme[free_start]="#7e7e7e"
24+theme[free_mid]="#e6b99d"
25+theme[free_end]="#ffc799"
26+theme[cached_start]="#7e7e7e"
27+theme[cached_mid]="#e6b99d"
28+theme[cached_end]="#ffc799"
29+theme[available_start]="#7e7e7e"
30+theme[available_mid]="#e6b99d"
31+theme[available_end]="#ffc799"
32+theme[used_start]="#7e7e7e"
33+theme[used_mid]="#e6b99d"
34+theme[used_end]="#ffc799"
35+theme[download_start]="#7e7e7e"
36+theme[download_mid]="#e6b99d"
37+theme[download_end]="#ffc799"
38+theme[upload_start]="#7e7e7e"
39+theme[upload_mid]="#e6b99d"
40+theme[upload_end]="#ffc799"
41+theme[process_start]="#7e7e7e"
42+theme[process_mid]="#e6b99d"
43+theme[process_end]="#ffc799"
@@ -0,0 +1,43 @@
1+# Vesper-inspired warm monochrome, with peach highlights.
2+theme[main_bg]="#101010"
3+theme[main_fg]="#ffffff"
4+theme[title]="#ffc799"
5+theme[hi_fg]="#ffc799"
6+theme[selected_bg]="#282828"
7+theme[selected_fg]="#ffffff"
8+theme[inactive_fg]="#7e7e7e"
9+theme[proc_misc]="#ffc799"
10+theme[cpu_box]="#7e7e7e"
11+theme[mem_box]="#7e7e7e"
12+theme[net_box]="#7e7e7e"
13+theme[proc_box]="#7e7e7e"
14+theme[div_line]="#282828"
15+theme[meter_bg]="#282828"
16+theme[graph_text]="#a0a0a0"
17+theme[temp_start]="#7e7e7e"
18+theme[temp_mid]="#e6b99d"
19+theme[temp_end]="#ffc799"
20+theme[cpu_start]="#7e7e7e"
21+theme[cpu_mid]="#e6b99d"
22+theme[cpu_end]="#ffc799"
23+theme[free_start]="#7e7e7e"
24+theme[free_mid]="#e6b99d"
25+theme[free_end]="#ffc799"
26+theme[cached_start]="#7e7e7e"
27+theme[cached_mid]="#e6b99d"
28+theme[cached_end]="#ffc799"
29+theme[available_start]="#7e7e7e"
30+theme[available_mid]="#e6b99d"
31+theme[available_end]="#ffc799"
32+theme[used_start]="#7e7e7e"
33+theme[used_mid]="#e6b99d"
34+theme[used_end]="#ffc799"
35+theme[download_start]="#7e7e7e"
36+theme[download_mid]="#e6b99d"
37+theme[download_end]="#ffc799"
38+theme[upload_start]="#7e7e7e"
39+theme[upload_mid]="#e6b99d"
40+theme[upload_end]="#ffc799"
41+theme[process_start]="#7e7e7e"
42+theme[process_mid]="#e6b99d"
43+theme[process_end]="#ffc799"
M tools/demo/btop.conf +1 -1
@@ -1,4 +1,4 @@
1-color_theme = "tokyo-night"
1+color_theme = "/home/demo/.config/btop/themes/vesper.theme"
22 theme_background = False
33 truecolor = True
44 update_ms = 500
@@ -1,4 +1,4 @@
1-color_theme = "tokyo-night"1+color_theme = "/home/demo/.config/btop/themes/vesper.theme"
2 theme_background = False2 theme_background = False
3 truecolor = True3 truecolor = True
4 update_ms = 5004 update_ms = 500
M tools/demo/config +4 -4
@@ -1,12 +1,12 @@
11 # NeferWL · a little space to think
22 animations = on
3-layout.gaps = 8
3+layout.gaps = 0
44 layout.max-columns = 2
55 layout.overflow = cascade
66 border.width = 2
7-border.active = #7aa2f7
8-border.inactive = #414868
9-background = #1a1b26
7+border.active = #ffc799
8+border.inactive = #282828
9+background = #101010
1010 terminal.auto-open = off
1111 stash.spawn-terminal = off
1212 stash.capture = off
@@ -1,12 +1,12 @@
1 # NeferWL · a little space to think1 # NeferWL · a little space to think
2 animations = on2 animations = on
3-layout.gaps = 83+layout.gaps = 0
4 layout.max-columns = 24 layout.max-columns = 2
5 layout.overflow = cascade5 layout.overflow = cascade
6 border.width = 26 border.width = 2
7-border.active = #7aa2f77+border.active = #ffc799
8-border.inactive = #4148688+border.inactive = #282828
9-background = #1a1b269+background = #101010
10 terminal.auto-open = off10 terminal.auto-open = off
11 stash.spawn-terminal = off11 stash.spawn-terminal = off
12 stash.capture = off12 stash.capture = off
M tools/demo/firefox/user.js +2 -0
@@ -11,3 +11,5 @@ user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
1111 user_pref("browser.toolbars.bookmarks.visibility", "never");
1212 user_pref("widget.gtk.overlay-scrollbars.enabled", true);
1313 user_pref("browser.tabs.warnOnClose", false);
14+// Keep browser chrome neutral rather than Firefox's purple dark-theme tint.
15+user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
@@ -11,3 +11,5 @@ user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
11 user_pref("browser.toolbars.bookmarks.visibility", "never");11 user_pref("browser.toolbars.bookmarks.visibility", "never");
12 user_pref("widget.gtk.overlay-scrollbars.enabled", true);12 user_pref("widget.gtk.overlay-scrollbars.enabled", true);
13 user_pref("browser.tabs.warnOnClose", false);13 user_pref("browser.tabs.warnOnClose", false);
14+// Keep browser chrome neutral rather than Firefox's purple dark-theme tint.
15+user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
A tools/demo/firefox/userChrome.css +18 -0
@@ -0,0 +1,18 @@
1+/* Vesper chrome: neutral near-black surfaces and one peach selection accent. */
2+:root {
3+ --toolbar-bgcolor: #161616 !important;
4+ --toolbar-color: #ffffff !important;
5+ --lwt-accent-color: #101010 !important;
6+ --lwt-text-color: #ffffff !important;
7+ --lwt-selected-tab-background-color: #282828 !important;
8+ --toolbar-field-background-color: #101010 !important;
9+ --toolbar-field-color: #ffffff !important;
10+ --toolbar-field-focus-border-color: #ffc799 !important;
11+}
12+#navigator-toolbox, #TabsToolbar, #nav-bar {
13+ background: #161616 !important;
14+ background-image: none !important;
15+ color: #ffffff !important;
16+}
17+.tab-background[selected] { background: #282828 !important; outline-color: #ffc799 !important; }
18+#urlbar-background { background: #101010 !important; border-color: #282828 !important; }
@@ -0,0 +1,18 @@
1+/* Vesper chrome: neutral near-black surfaces and one peach selection accent. */
2+:root {
3+ --toolbar-bgcolor: #161616 !important;
4+ --toolbar-color: #ffffff !important;
5+ --lwt-accent-color: #101010 !important;
6+ --lwt-text-color: #ffffff !important;
7+ --lwt-selected-tab-background-color: #282828 !important;
8+ --toolbar-field-background-color: #101010 !important;
9+ --toolbar-field-color: #ffffff !important;
10+ --toolbar-field-focus-border-color: #ffc799 !important;
11+}
12+#navigator-toolbox, #TabsToolbar, #nav-bar {
13+ background: #161616 !important;
14+ background-image: none !important;
15+ color: #ffffff !important;
16+}
17+.tab-background[selected] { background: #282828 !important; outline-color: #ffc799 !important; }
18+#urlbar-background { background: #101010 !important; border-color: #282828 !important; }
M tools/demo/foot.ini +19 -18
@@ -3,21 +3,22 @@ font=JetBrainsMono Nerd Font:size=15
33 pad=20x20
44
55 [colors-dark]
6-background=1a1b26
7-foreground=c0caf5
8-regular0=15161e
9-regular1=f7768e
10-regular2=9ece6a
11-regular3=e0af68
12-regular4=7aa2f7
13-regular5=bb9af7
14-regular6=7dcfff
15-regular7=a9b1d6
16-bright0=414868
17-bright1=f7768e
18-bright2=9ece6a
19-bright3=e0af68
20-bright4=7aa2f7
21-bright5=bb9af7
22-bright6=7dcfff
23-bright7=c0caf5
6+cursor=101010 ffc799
7+background=101010
8+foreground=ffffff
9+regular0=101010
10+regular1=f5a191
11+regular2=90b99f
12+regular3=e6b99d
13+regular4=aca1cf
14+regular5=e29eca
15+regular6=ea83a5
16+regular7=a0a0a0
17+bright0=7e7e7e
18+bright1=ff8080
19+bright2=99ffe4
20+bright3=ffc799
21+bright4=b9aeda
22+bright5=ecaad6
23+bright6=f591b2
24+bright7=ffffff
@@ -3,21 +3,22 @@ font=JetBrainsMono Nerd Font:size=15
3 pad=20x203 pad=20x20
4 4
5 [colors-dark]5 [colors-dark]
6-background=1a1b266+cursor=101010 ffc799
7-foreground=c0caf57+background=101010
8-regular0=15161e8+foreground=ffffff
9-regular1=f7768e9+regular0=101010
10-regular2=9ece6a10+regular1=f5a191
11-regular3=e0af6811+regular2=90b99f
12-regular4=7aa2f712+regular3=e6b99d
13-regular5=bb9af713+regular4=aca1cf
14-regular6=7dcfff14+regular5=e29eca
15-regular7=a9b1d615+regular6=ea83a5
16-bright0=41486816+regular7=a0a0a0
17-bright1=f7768e17+bright0=7e7e7e
18-bright2=9ece6a18+bright1=ff8080
19-bright3=e0af6819+bright2=99ffe4
20-bright4=7aa2f720+bright3=ffc799
21-bright5=bb9af721+bright4=b9aeda
22-bright6=7dcfff22+bright5=ecaad6
23-bright7=c0caf523+bright6=f591b2
24+bright7=ffffff
M tools/demo/record.sh +60 -31
@@ -1,68 +1,93 @@
11 #!/usr/bin/env bash
22 # Requires Go 1.27, rootless Docker, bash and ffmpeg. Run from any directory.
3+# KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up.
34 set -euo pipefail
45 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
56 cd "$root"
7+for command in docker go ffmpeg; do
8+ command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
9+done
10+docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; }
611 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")
712 printf 'Capture directory: %s\n' "$work"
813 mkdir -p "$work/shots" docs/media
9-compositor='' launcher='' container_started=''
14+compositor='' launcher='' feeder='' success=''
15+container=neferwl-demo-$$
1016 cleanup() {
11- if [[ -n $launcher ]]; then kill "$launcher" 2>/dev/null || true; fi
12- if [[ -n $container_started ]]; then docker rm -f neferwl-demo >/dev/null 2>&1 || true; fi
13- if [[ -n $compositor ]]; then kill "$compositor" 2>/dev/null || true; fi
17+ local status=$?
18+ trap - EXIT
19+ for pid in "$launcher" "$feeder" "$compositor"; do
20+ if [[ -n $pid ]]; then kill "$pid" 2>/dev/null || true; fi
21+ done
22+ docker rm -f "$container" >/dev/null 2>&1 || true
23+ for pid in "$launcher" "$feeder" "$compositor"; do
24+ if [[ -n $pid ]]; then wait "$pid" 2>/dev/null || true; fi
25+ done
26+ exec 3>&-
27+ if [[ $success != yes || ${KEEP_CAPTURE:-0} != 1 ]]; then
28+ rm -rf -- "$work"
29+ fi
30+ exit "$status"
1431 }
1532 trap cleanup EXIT
1633 trap 'exit 130' INT
1734 trap 'exit 143' TERM
18-# Refuse to replace someone else's running container.
19-if [[ -n $(docker ps -aq --filter 'name=^/neferwl-demo$') ]]; then
20- echo 'Container neferwl-demo already exists; remove it before recording.' >&2
21- exit 1
22-fi
2335 docker build -t neferwl-demo -f tools/demo/Dockerfile tools/demo
2436 CGO_ENABLED=0 go build -o "$work/neferwl" ./cmd/neferwl
25-log=${XDG_STATE_HOME:-$HOME/.local/state}/neferwl/runs/headless/latest.log
26-env -u NEFERWL_STATE -u NEFERWL_SLOT -u WAYLAND_DISPLAY "$work/neferwl" \
37+state=$work/state
38+log=$state/neferwl/runs/headless/latest.log
39+mkfifo "$work/input"
40+# Hold both ends open so the FIFO cannot block compositor startup or produce
41+# premature EOF. No input is fed until Docker and the first app are ready.
42+exec 3<>"$work/input"
43+env -u NEFERWL_STATE -u NEFERWL_SLOT -u WAYLAND_DISPLAY XDG_STATE_HOME="$state" "$work/neferwl" \
2744 --backend=headless --size 1920x1080 --config tools/demo/config \
28- --no-xwayland --screenshot "$work/shots" --input tools/demo/input.txt --timeout 45s &
45+ --no-xwayland --screenshot "$work/shots" --input - --timeout 75s <"$work/input" &
2946 compositor=$!
30-SECONDS=0
31-# The host starts only the compositor; all apps and their content stay in Docker.
32-# A fixed startup allowance avoids reading a previous run's socket from the log.
33-sleep 1
34-kill -0 "$compositor"
35-display=$(sed -n '/"message":"listening"/s/.*"WAYLAND_DISPLAY":"\([^"]*\)".*/\1/p' "$log" | tail -1)
36-[[ -n $display ]] || { echo "No listening socket in $log" >&2; exit 1; }
37-socket=$XDG_RUNTIME_DIR/${display##*/}
38-[[ -S $socket ]] || { echo "Not a socket: $socket" >&2; exit 1; }
47+socket=''
48+for ((attempt = 0; attempt < 150; attempt++)); do
49+ kill -0 "$compositor" 2>/dev/null || { echo "Compositor exited before readiness; see $log" >&2; exit 1; }
50+ if [[ -f $log ]]; then
51+ display=$(sed -n '/"message":"listening"/s/.*"WAYLAND_DISPLAY":"\([^" ]*\)".*/\1/p' "$log" | tail -1)
52+ if [[ -n $display && -S $XDG_RUNTIME_DIR/${display##*/} ]]; then
53+ socket=$XDG_RUNTIME_DIR/${display##*/}
54+ break
55+ fi
56+ fi
57+ sleep 0.1
58+done
59+[[ -n $socket ]] || { echo "Timed out after 15s waiting for compositor socket; see $log" >&2; exit 1; }
3960 # Container root maps to the host uid under rootless Docker. Grant demo access
4061 # to this disposable socket only; Firefox rejects root with a demo-owned HOME.
41-docker run -d --name neferwl-demo --hostname neferwl-demo --network none \
62+docker run -d --name "$container" --hostname neferwl-demo --network none \
4263 -v "$socket:/run/wl/wayland-0" neferwl-demo
43-container_started=yes
44-docker exec neferwl-demo chmod 666 /run/wl/wayland-0
64+docker exec "$container" chmod 666 /run/wl/wayland-0
4565 at() { local delay=$(($1 - SECONDS)); if ((delay > 0)); then sleep "$delay"; fi; }
4666 launch_apps() {
4767 # This shell timeline shares t=0 with input.txt. Only keys live in input.txt;
4868 # Docker launches cannot be invoked by compositor input scripts.
49- docker exec -d --user demo neferwl-demo foot --title 'System / NeferWL' bash -c \
50- 'fastfetch --logo small --structure OS:Kernel:WM:Terminal:Memory:Colors; eza -la --no-user --icons --color=always /etc | head -24; exec sleep infinity'
5169 at 3
52- docker exec -d --user demo neferwl-demo dbus-run-session -- firefox --no-remote \
70+ docker exec -d --user demo "$container" dbus-run-session -- firefox --no-remote \
5371 --profile /home/demo/firefox file:///home/demo/site/index.html
5472 # btop's first per-core sample is bogus in this container. Let it collect
5573 # real deltas on workspace 2; input.txt brings it into view at t=17.
5674 at 5
57- docker exec -d --user demo neferwl-demo foot --app-id demo-monitor \
75+ docker exec -d --user demo "$container" foot --app-id demo-monitor \
5876 --title 'Monitor / btop' /usr/local/bin/btop
5977 at 9
60- docker exec -d --user demo neferwl-demo dbus-run-session -- bash -c \
78+ docker exec -d --user demo "$container" dbus-run-session -- bash -c \
6179 'gsettings set org.xfce.mousepad.preferences.view use-default-monospace-font false; gsettings set org.xfce.mousepad.preferences.view font-name "JetBrainsMono Nerd Font 15"; gsettings set org.xfce.mousepad.preferences.window menubar-visible false; mousepad /home/demo/config'
6280 at 30
63- docker exec -d --user demo neferwl-demo foot --title 'Fonts / workspace 2' bash -c \
81+ docker exec -d --user demo "$container" foot --title 'Fonts / workspace 2' bash -c \
6482 'eza --tree --level=2 --icons --color=always /usr/share/fonts | head -30; exec sleep infinity'
6583 }
84+docker exec -d --user demo "$container" foot --title 'System / NeferWL' bash -c \
85+ 'fastfetch --logo small --structure OS:Kernel:WM:Terminal:Memory:Colors; eza -la --no-user --icons --color=always /etc | head -24; exec sleep infinity'
86+# The first app is launched before t=0. FIFO input and shell launches now
87+# share this epoch, independent of compositor/image/container startup time.
88+SECONDS=0
89+{ cat tools/demo/input.txt; printf '\nkey Ctrl+Alt+BackSpace\n'; } >&3 &
90+feeder=$!
6691 launch_apps &
6792 launcher=$!
6893 # --timeout exits nonzero; accept only its explicit timeout log entry.
@@ -70,6 +95,8 @@ if ! wait "$compositor"; then
7095 grep -q '"reason":"timeout"' "$log" || exit 1
7196 fi
7297 compositor=
98+wait "$feeder"
99+feeder=
73100 wait "$launcher"
74101 launcher=
75102 ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \
@@ -77,4 +104,6 @@ ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \
77104 -c:v libx264 -crf 23 -movflags +faststart docs/media/demo.mp4 -y
78105 ffmpeg -hide_banner -loglevel warning -ss 7 -i docs/media/demo.mp4 \
79106 -frames:v 1 -update 1 docs/media/demo.png -y
80-printf 'Recorded: %s\n' "$work"
107+success=yes
108+if [[ ${KEEP_CAPTURE:-0} == 1 ]]; then printf 'Captures kept: %s\n' "$work"; fi
109+printf 'Recorded: docs/media/demo.mp4 and docs/media/demo.png\n'
@@ -1,68 +1,93 @@
1 #!/usr/bin/env bash1 #!/usr/bin/env bash
2 # Requires Go 1.27, rootless Docker, bash and ffmpeg. Run from any directory.2 # Requires Go 1.27, rootless Docker, bash and ffmpeg. Run from any directory.
3+# KEEP_CAPTURE=1 preserves temporary captures on success; failures always clean up.
3 set -euo pipefail4 set -euo pipefail
4 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)5 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
5 cd "$root"6 cd "$root"
7+for command in docker go ffmpeg; do
8+ command -v "$command" >/dev/null || { echo "Missing required command: $command" >&2; exit 1; }
9+done
10+docker info >/dev/null 2>&1 || { echo 'Docker daemon unavailable; start rootless Docker first.' >&2; exit 1; }
6 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")11 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")
7 printf 'Capture directory: %s\n' "$work"12 printf 'Capture directory: %s\n' "$work"
8 mkdir -p "$work/shots" docs/media13 mkdir -p "$work/shots" docs/media
9-compositor='' launcher='' container_started=''14+compositor='' launcher='' feeder='' success=''
15+container=neferwl-demo-$$
10 cleanup() {16 cleanup() {
11- if [[ -n $launcher ]]; then kill "$launcher" 2>/dev/null || true; fi17+ local status=$?
12- if [[ -n $container_started ]]; then docker rm -f neferwl-demo >/dev/null 2>&1 || true; fi18+ trap - EXIT
13- if [[ -n $compositor ]]; then kill "$compositor" 2>/dev/null || true; fi19+ for pid in "$launcher" "$feeder" "$compositor"; do
20+ if [[ -n $pid ]]; then kill "$pid" 2>/dev/null || true; fi
21+ done
22+ docker rm -f "$container" >/dev/null 2>&1 || true
23+ for pid in "$launcher" "$feeder" "$compositor"; do
24+ if [[ -n $pid ]]; then wait "$pid" 2>/dev/null || true; fi
25+ done
26+ exec 3>&-
27+ if [[ $success != yes || ${KEEP_CAPTURE:-0} != 1 ]]; then
28+ rm -rf -- "$work"
29+ fi
30+ exit "$status"
14 }31 }
15 trap cleanup EXIT32 trap cleanup EXIT
16 trap 'exit 130' INT33 trap 'exit 130' INT
17 trap 'exit 143' TERM34 trap 'exit 143' TERM
18-# Refuse to replace someone else's running container.
19-if [[ -n $(docker ps -aq --filter 'name=^/neferwl-demo$') ]]; then
20- echo 'Container neferwl-demo already exists; remove it before recording.' >&2
21- exit 1
22-fi
23 docker build -t neferwl-demo -f tools/demo/Dockerfile tools/demo35 docker build -t neferwl-demo -f tools/demo/Dockerfile tools/demo
24 CGO_ENABLED=0 go build -o "$work/neferwl" ./cmd/neferwl36 CGO_ENABLED=0 go build -o "$work/neferwl" ./cmd/neferwl
25-log=${XDG_STATE_HOME:-$HOME/.local/state}/neferwl/runs/headless/latest.log37+state=$work/state
26-env -u NEFERWL_STATE -u NEFERWL_SLOT -u WAYLAND_DISPLAY "$work/neferwl" \38+log=$state/neferwl/runs/headless/latest.log
39+mkfifo "$work/input"
40+# Hold both ends open so the FIFO cannot block compositor startup or produce
41+# premature EOF. No input is fed until Docker and the first app are ready.
42+exec 3<>"$work/input"
43+env -u NEFERWL_STATE -u NEFERWL_SLOT -u WAYLAND_DISPLAY XDG_STATE_HOME="$state" "$work/neferwl" \
27 --backend=headless --size 1920x1080 --config tools/demo/config \44 --backend=headless --size 1920x1080 --config tools/demo/config \
28- --no-xwayland --screenshot "$work/shots" --input tools/demo/input.txt --timeout 45s &45+ --no-xwayland --screenshot "$work/shots" --input - --timeout 75s <"$work/input" &
29 compositor=$!46 compositor=$!
30-SECONDS=047+socket=''
31-# The host starts only the compositor; all apps and their content stay in Docker.48+for ((attempt = 0; attempt < 150; attempt++)); do
32-# A fixed startup allowance avoids reading a previous run's socket from the log.49+ kill -0 "$compositor" 2>/dev/null || { echo "Compositor exited before readiness; see $log" >&2; exit 1; }
33-sleep 150+ if [[ -f $log ]]; then
34-kill -0 "$compositor"51+ display=$(sed -n '/"message":"listening"/s/.*"WAYLAND_DISPLAY":"\([^" ]*\)".*/\1/p' "$log" | tail -1)
35-display=$(sed -n '/"message":"listening"/s/.*"WAYLAND_DISPLAY":"\([^"]*\)".*/\1/p' "$log" | tail -1)52+ if [[ -n $display && -S $XDG_RUNTIME_DIR/${display##*/} ]]; then
36-[[ -n $display ]] || { echo "No listening socket in $log" >&2; exit 1; }53+ socket=$XDG_RUNTIME_DIR/${display##*/}
37-socket=$XDG_RUNTIME_DIR/${display##*/}54+ break
38-[[ -S $socket ]] || { echo "Not a socket: $socket" >&2; exit 1; }55+ fi
56+ fi
57+ sleep 0.1
58+done
59+[[ -n $socket ]] || { echo "Timed out after 15s waiting for compositor socket; see $log" >&2; exit 1; }
39 # Container root maps to the host uid under rootless Docker. Grant demo access60 # Container root maps to the host uid under rootless Docker. Grant demo access
40 # to this disposable socket only; Firefox rejects root with a demo-owned HOME.61 # to this disposable socket only; Firefox rejects root with a demo-owned HOME.
41-docker run -d --name neferwl-demo --hostname neferwl-demo --network none \62+docker run -d --name "$container" --hostname neferwl-demo --network none \
42 -v "$socket:/run/wl/wayland-0" neferwl-demo63 -v "$socket:/run/wl/wayland-0" neferwl-demo
43-container_started=yes64+docker exec "$container" chmod 666 /run/wl/wayland-0
44-docker exec neferwl-demo chmod 666 /run/wl/wayland-0
45 at() { local delay=$(($1 - SECONDS)); if ((delay > 0)); then sleep "$delay"; fi; }65 at() { local delay=$(($1 - SECONDS)); if ((delay > 0)); then sleep "$delay"; fi; }
46 launch_apps() {66 launch_apps() {
47 # This shell timeline shares t=0 with input.txt. Only keys live in input.txt;67 # This shell timeline shares t=0 with input.txt. Only keys live in input.txt;
48 # Docker launches cannot be invoked by compositor input scripts.68 # Docker launches cannot be invoked by compositor input scripts.
49- docker exec -d --user demo neferwl-demo foot --title 'System / NeferWL' bash -c \
50- 'fastfetch --logo small --structure OS:Kernel:WM:Terminal:Memory:Colors; eza -la --no-user --icons --color=always /etc | head -24; exec sleep infinity'
51 at 369 at 3
52- docker exec -d --user demo neferwl-demo dbus-run-session -- firefox --no-remote \70+ docker exec -d --user demo "$container" dbus-run-session -- firefox --no-remote \
53 --profile /home/demo/firefox file:///home/demo/site/index.html71 --profile /home/demo/firefox file:///home/demo/site/index.html
54 # btop's first per-core sample is bogus in this container. Let it collect72 # btop's first per-core sample is bogus in this container. Let it collect
55 # real deltas on workspace 2; input.txt brings it into view at t=17.73 # real deltas on workspace 2; input.txt brings it into view at t=17.
56 at 574 at 5
57- docker exec -d --user demo neferwl-demo foot --app-id demo-monitor \75+ docker exec -d --user demo "$container" foot --app-id demo-monitor \
58 --title 'Monitor / btop' /usr/local/bin/btop76 --title 'Monitor / btop' /usr/local/bin/btop
59 at 977 at 9
60- docker exec -d --user demo neferwl-demo dbus-run-session -- bash -c \78+ docker exec -d --user demo "$container" dbus-run-session -- bash -c \
61 'gsettings set org.xfce.mousepad.preferences.view use-default-monospace-font false; gsettings set org.xfce.mousepad.preferences.view font-name "JetBrainsMono Nerd Font 15"; gsettings set org.xfce.mousepad.preferences.window menubar-visible false; mousepad /home/demo/config'79 'gsettings set org.xfce.mousepad.preferences.view use-default-monospace-font false; gsettings set org.xfce.mousepad.preferences.view font-name "JetBrainsMono Nerd Font 15"; gsettings set org.xfce.mousepad.preferences.window menubar-visible false; mousepad /home/demo/config'
62 at 3080 at 30
63- docker exec -d --user demo neferwl-demo foot --title 'Fonts / workspace 2' bash -c \81+ docker exec -d --user demo "$container" foot --title 'Fonts / workspace 2' bash -c \
64 'eza --tree --level=2 --icons --color=always /usr/share/fonts | head -30; exec sleep infinity'82 'eza --tree --level=2 --icons --color=always /usr/share/fonts | head -30; exec sleep infinity'
65 }83 }
84+docker exec -d --user demo "$container" foot --title 'System / NeferWL' bash -c \
85+ 'fastfetch --logo small --structure OS:Kernel:WM:Terminal:Memory:Colors; eza -la --no-user --icons --color=always /etc | head -24; exec sleep infinity'
86+# The first app is launched before t=0. FIFO input and shell launches now
87+# share this epoch, independent of compositor/image/container startup time.
88+SECONDS=0
89+{ cat tools/demo/input.txt; printf '\nkey Ctrl+Alt+BackSpace\n'; } >&3 &
90+feeder=$!
66 launch_apps &91 launch_apps &
67 launcher=$!92 launcher=$!
68 # --timeout exits nonzero; accept only its explicit timeout log entry.93 # --timeout exits nonzero; accept only its explicit timeout log entry.
@@ -70,6 +95,8 @@ if ! wait "$compositor"; then
70 grep -q '"reason":"timeout"' "$log" || exit 195 grep -q '"reason":"timeout"' "$log" || exit 1
71 fi96 fi
72 compositor=97 compositor=
98+wait "$feeder"
99+feeder=
73 wait "$launcher"100 wait "$launcher"
74 launcher=101 launcher=
75 ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \102 ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \
@@ -77,4 +104,6 @@ ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \
77 -c:v libx264 -crf 23 -movflags +faststart docs/media/demo.mp4 -y104 -c:v libx264 -crf 23 -movflags +faststart docs/media/demo.mp4 -y
78 ffmpeg -hide_banner -loglevel warning -ss 7 -i docs/media/demo.mp4 \105 ffmpeg -hide_banner -loglevel warning -ss 7 -i docs/media/demo.mp4 \
79 -frames:v 1 -update 1 docs/media/demo.png -y106 -frames:v 1 -update 1 docs/media/demo.png -y
80-printf 'Recorded: %s\n' "$work"107+success=yes
108+if [[ ${KEEP_CAPTURE:-0} == 1 ]]; then printf 'Captures kept: %s\n' "$work"; fi
109+printf 'Recorded: docs/media/demo.mp4 and docs/media/demo.png\n'
M tools/demo/site/index.html +1 -1
@@ -4,7 +4,7 @@
44 <meta name="color-scheme" content="dark">
55 <title>NeferWL — Space to think</title>
66 <style>
7-*{box-sizing:border-box}body{margin:0;background:#1a1b26;color:#c0caf5;font:18px/1.6 sans-serif}main{max-width:960px;margin:auto;padding:60px 38px}header{color:#7aa2f7;letter-spacing:.2em;font:700 18px monospace}small{color:#9ece6a}h1{font-size:54px;line-height:1.12;letter-spacing:-2px;margin:40px 0 24px}p{color:#a9b1d6}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:36px 0}.card{padding:18px;background:#24283b;border:1px solid #414868;border-radius:12px}.card b{color:#7dcfff}.card p{font-size:15px;margin-bottom:0}pre{background:#16161e;border-left:3px solid #7aa2f7;border-radius:6px;padding:24px;font:16px/1.8 monospace;color:#9ece6a}footer{color:#737aa2;font:14px monospace}
7+*{box-sizing:border-box}body{margin:0;background:#101010;color:#ffffff;font:18px/1.6 sans-serif}main{max-width:960px;margin:auto;padding:60px 38px}header{color:#ffc799;letter-spacing:.2em;font:700 18px monospace}small{color:#ffc799}h1{font-size:54px;line-height:1.12;letter-spacing:-2px;margin:40px 0 24px}p{color:#a0a0a0}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:36px 0}.card{padding:18px;background:#161616;border:1px solid #282828;border-radius:12px}.card b{color:#ffc799}.card p{font-size:15px;margin-bottom:0}pre{background:#161616;border-left:3px solid #ffc799;border-radius:6px;padding:24px;font:16px/1.8 monospace;color:#ffc799}footer{color:#a0a0a0;font:14px monospace}
88 </style>
99 <main><header>◈ NEFERWL</header><h1>Your desktop.<br>A little more focused.</h1><p>A Wayland compositor with room for your work.<br>Tile naturally, set things aside, see the whole picture.</p><div class="cards"><section class="card"><b>01 / Performance</b><p>Small, responsive.<br>Built in Go. Made to stay out of your way.</p></section><section class="card"><b>02 / Stash</b><p>Keep tools close.<br>Set aside a window. Bring it back in a keystroke.</p></section><section class="card"><b>03 / Overview</b><p>Find your focus.<br>Every workspace and stashed window, at a glance.</p></section></div><pre># A desktop that flows with you
1010 layout.max-columns = 2
@@ -4,7 +4,7 @@
4 <meta name="color-scheme" content="dark">4 <meta name="color-scheme" content="dark">
5 <title>NeferWL — Space to think</title>5 <title>NeferWL — Space to think</title>
6 <style>6 <style>
7-*{box-sizing:border-box}body{margin:0;background:#1a1b26;color:#c0caf5;font:18px/1.6 sans-serif}main{max-width:960px;margin:auto;padding:60px 38px}header{color:#7aa2f7;letter-spacing:.2em;font:700 18px monospace}small{color:#9ece6a}h1{font-size:54px;line-height:1.12;letter-spacing:-2px;margin:40px 0 24px}p{color:#a9b1d6}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:36px 0}.card{padding:18px;background:#24283b;border:1px solid #414868;border-radius:12px}.card b{color:#7dcfff}.card p{font-size:15px;margin-bottom:0}pre{background:#16161e;border-left:3px solid #7aa2f7;border-radius:6px;padding:24px;font:16px/1.8 monospace;color:#9ece6a}footer{color:#737aa2;font:14px monospace}7+*{box-sizing:border-box}body{margin:0;background:#101010;color:#ffffff;font:18px/1.6 sans-serif}main{max-width:960px;margin:auto;padding:60px 38px}header{color:#ffc799;letter-spacing:.2em;font:700 18px monospace}small{color:#ffc799}h1{font-size:54px;line-height:1.12;letter-spacing:-2px;margin:40px 0 24px}p{color:#a0a0a0}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:36px 0}.card{padding:18px;background:#161616;border:1px solid #282828;border-radius:12px}.card b{color:#ffc799}.card p{font-size:15px;margin-bottom:0}pre{background:#161616;border-left:3px solid #ffc799;border-radius:6px;padding:24px;font:16px/1.8 monospace;color:#ffc799}footer{color:#a0a0a0;font:14px monospace}
8 </style>8 </style>
9 <main><header>◈ NEFERWL</header><h1>Your desktop.<br>A little more focused.</h1><p>A Wayland compositor with room for your work.<br>Tile naturally, set things aside, see the whole picture.</p><div class="cards"><section class="card"><b>01 / Performance</b><p>Small, responsive.<br>Built in Go. Made to stay out of your way.</p></section><section class="card"><b>02 / Stash</b><p>Keep tools close.<br>Set aside a window. Bring it back in a keystroke.</p></section><section class="card"><b>03 / Overview</b><p>Find your focus.<br>Every workspace and stashed window, at a glance.</p></section></div><pre># A desktop that flows with you9 <main><header>◈ NEFERWL</header><h1>Your desktop.<br>A little more focused.</h1><p>A Wayland compositor with room for your work.<br>Tile naturally, set things aside, see the whole picture.</p><div class="cards"><section class="card"><b>01 / Performance</b><p>Small, responsive.<br>Built in Go. Made to stay out of your way.</p></section><section class="card"><b>02 / Stash</b><p>Keep tools close.<br>Set aside a window. Bring it back in a keystroke.</p></section><section class="card"><b>03 / Overview</b><p>Find your focus.<br>Every workspace and stashed window, at a glance.</p></section></div><pre># A desktop that flows with you
10 layout.max-columns = 210 layout.max-columns = 2

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 25

Tags 10

↑↓ moveenter openesc close