detached 9d71793

feat(demo): record the README demo with containerised apps

commit
9d71793ab523035cdc4f532eb0ddb5d2653a4626
author
brice · · 2026-10-21 05:17 UTC
parent
d34c114
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
12 files · +200 -54
M docs/dev/headless.md +5 -2
@@ -25,8 +25,11 @@ 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` to rebuild and record the 1080p README demo (Go 1.27, foot, bash, fastfetch, ffmpeg).
29-The config and timed input live beside it; temporary captures are kept at the printed path.
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.
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.
32+The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.
3033
3134 ## Input scripts
3235
@@ -25,8 +25,11 @@ 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` to rebuild and record the 1080p README demo (Go 1.27, foot, bash, fastfetch, ffmpeg).28+Run `tools/demo/record.sh` with Go 1.27, rootless Docker and ffmpeg installed.
29-The config and timed input live beside it; temporary captures are kept at the printed path.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.
31+The shell timeline launches apps, while `input.txt` drives keys; captures stay at the printed path.
32+The recording and poster replace `docs/media/demo.mp4` and `docs/media/demo.png`.
30 33
31 ## Input scripts34 ## Input scripts
32 35
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

A tools/demo/Dockerfile +24 -0
@@ -0,0 +1,24 @@
1+# syntax=docker/dockerfile:1
2+FROM archlinux:latest
3+RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \
4+ pacman -Sy --noconfirm archlinux-keyring \
5+ && pacman -Syu --noconfirm --needed \
6+ firefox foot btop fastfetch eza bat mousepad \
7+ 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.
9+RUN install -m 0755 /usr/bin/btop /usr/local/bin/btop
10+RUN useradd -m -u 1000 demo \
11+ && mkdir -p /home/demo/firefox /home/demo/.config/btop /run/wl \
12+ && chmod 0700 /run/wl
13+COPY firefox/user.js /home/demo/firefox/user.js
14+COPY foot.ini /home/demo/.config/foot/foot.ini
15+COPY btop.conf /home/demo/.config/btop/btop.conf
16+COPY site /home/demo/site
17+COPY config /home/demo/config
18+RUN chown -R demo:demo /home/demo /run/wl
19+ENV HOME=/home/demo XDG_RUNTIME_DIR=/run/wl WAYLAND_DISPLAY=wayland-0 \
20+ GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \
21+ MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor
22+WORKDIR /home/demo
23+# The recorder grants demo access to only the disposable headless socket.
24+CMD ["sleep", "infinity"]
@@ -0,0 +1,24 @@
1+# syntax=docker/dockerfile:1
2+FROM archlinux:latest
3+RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \
4+ pacman -Sy --noconfirm archlinux-keyring \
5+ && pacman -Syu --noconfirm --needed \
6+ firefox foot btop fastfetch eza bat mousepad \
7+ 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.
9+RUN install -m 0755 /usr/bin/btop /usr/local/bin/btop
10+RUN useradd -m -u 1000 demo \
11+ && mkdir -p /home/demo/firefox /home/demo/.config/btop /run/wl \
12+ && chmod 0700 /run/wl
13+COPY firefox/user.js /home/demo/firefox/user.js
14+COPY foot.ini /home/demo/.config/foot/foot.ini
15+COPY btop.conf /home/demo/.config/btop/btop.conf
16+COPY site /home/demo/site
17+COPY config /home/demo/config
18+RUN chown -R demo:demo /home/demo /run/wl
19+ENV HOME=/home/demo XDG_RUNTIME_DIR=/run/wl WAYLAND_DISPLAY=wayland-0 \
20+ GTK_THEME=Adwaita:dark ADW_DEBUG_COLOR_SCHEME=prefer-dark \
21+ MOZ_ENABLE_WAYLAND=1 LIBGL_ALWAYS_SOFTWARE=1 COLORTERM=truecolor
22+WORKDIR /home/demo
23+# The recorder grants demo access to only the disposable headless socket.
24+CMD ["sleep", "infinity"]
A tools/demo/btop.conf +9 -0
@@ -0,0 +1,9 @@
1+color_theme = "tokyo-night"
2+theme_background = False
3+truecolor = True
4+update_ms = 500
5+custom_cpu_name = "CPU"
6+show_cpu_freq = False
7+shown_boxes = "cpu mem proc"
8+show_battery = False
9+proc_tree = False
@@ -0,0 +1,9 @@
1+color_theme = "tokyo-night"
2+theme_background = False
3+truecolor = True
4+update_ms = 500
5+custom_cpu_name = "CPU"
6+show_cpu_freq = False
7+shown_boxes = "cpu mem proc"
8+show_battery = False
9+proc_tree = False
M tools/demo/config +18 -3
@@ -1,10 +1,25 @@
1+# NeferWL · a little space to think
12 animations = on
2-terminal = foot -c /dev/null -o font=monospace:size=16 -o colors-dark.background=1a1b26 -o colors-dark.foreground=c0caf5 -e bash --noprofile --norc
3-terminal.auto-open = first
43 layout.gaps = 8
4+layout.max-columns = 2
5+layout.overflow = cascade
56 border.width = 2
67 border.active = #7aa2f7
78 border.inactive = #414868
89 background = #1a1b26
9-layout.max-columns = 2
10+terminal.auto-open = off
11+stash.spawn-terminal = off
12+stash.capture = off
1013 keyboard.layout = us
14+
15+# Columns fill a band; extra windows flow below.
16+# Super+Up / Down travel between bands.
17+# Super+Shift+s sets a window aside.
18+# Super+s brings your stash into view.
19+# Super+o opens the overview.
20+bind.cmd+code:3 = focus-workspace 2
21+bind.cmd+code:2 = focus-workspace 1
22+
23+# Warm up btop off-screen before bringing its real samples into band 2.
24+rule.monitor.app-id = demo-monitor
25+rule.monitor.workspace = 2
@@ -1,10 +1,25 @@
1+# NeferWL · a little space to think
1 animations = on2 animations = on
2-terminal = foot -c /dev/null -o font=monospace:size=16 -o colors-dark.background=1a1b26 -o colors-dark.foreground=c0caf5 -e bash --noprofile --norc
3-terminal.auto-open = first
4 layout.gaps = 83 layout.gaps = 8
4+layout.max-columns = 2
5+layout.overflow = cascade
5 border.width = 26 border.width = 2
6 border.active = #7aa2f77 border.active = #7aa2f7
7 border.inactive = #4148688 border.inactive = #414868
8 background = #1a1b269 background = #1a1b26
9-layout.max-columns = 210+terminal.auto-open = off
11+stash.spawn-terminal = off
12+stash.capture = off
10 keyboard.layout = us13 keyboard.layout = us
14+
15+# Columns fill a band; extra windows flow below.
16+# Super+Up / Down travel between bands.
17+# Super+Shift+s sets a window aside.
18+# Super+s brings your stash into view.
19+# Super+o opens the overview.
20+bind.cmd+code:3 = focus-workspace 2
21+bind.cmd+code:2 = focus-workspace 1
22+
23+# Warm up btop off-screen before bringing its real samples into band 2.
24+rule.monitor.app-id = demo-monitor
25+rule.monitor.workspace = 2
D tools/demo/content.sh +0 -23
@@ -1,23 +0,0 @@
1-#!/usr/bin/env bash
2-# Static, colorful terminal content for the recording (no personal shell config).
3-# These listings are displayed, not parsed; truncation is intentional.
4-# shellcheck disable=SC2012
5-set -euo pipefail
6-clear
7-case ${1:-} in
8- system)
9- fastfetch --logo small --structure OS:Kernel:WM:Terminal:Memory:Colors
10- ls -lago --color=always /etc | head -24 || true
11- ;;
12- files)
13- ls -lgo --color=always /usr/share | head -34 || true
14- ;;
15- stash)
16- printf '\033[1;36m NEFERWL / YOUR STASH\033[0m\n\n'
17- for ((i = 0; i < 6; i++)); do
18- printf '\033[1;34m SET ASIDE\033[0m a window with Super+Shift+s\n'
19- printf '\033[1;32m BRING BACK\033[0m a window with Super+s\n'
20- printf '\033[1;35m STAY FOCUSED\033[0m keep your workspace clear\n\n'
21- done
22- ;;
23-esac
@@ -1,23 +0,0 @@
1-#!/usr/bin/env bash
2-# Static, colorful terminal content for the recording (no personal shell config).
3-# These listings are displayed, not parsed; truncation is intentional.
4-# shellcheck disable=SC2012
5-set -euo pipefail
6-clear
7-case ${1:-} in
8- system)
9- fastfetch --logo small --structure OS:Kernel:WM:Terminal:Memory:Colors
10- ls -lago --color=always /etc | head -24 || true
11- ;;
12- files)
13- ls -lgo --color=always /usr/share | head -34 || true
14- ;;
15- stash)
16- printf '\033[1;36m NEFERWL / YOUR STASH\033[0m\n\n'
17- for ((i = 0; i < 6; i++)); do
18- printf '\033[1;34m SET ASIDE\033[0m a window with Super+Shift+s\n'
19- printf '\033[1;32m BRING BACK\033[0m a window with Super+s\n'
20- printf '\033[1;35m STAY FOCUSED\033[0m keep your workspace clear\n\n'
21- done
22- ;;
23-esac
A tools/demo/firefox/user.js +13 -0
@@ -0,0 +1,13 @@
1+user_pref("ui.systemUsesDarkTheme", 1);
2+user_pref("layout.css.prefers-color-scheme.content-override", 0);
3+user_pref("browser.theme.content-theme", 0);
4+user_pref("browser.theme.toolbar-theme", 0);
5+user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");
6+user_pref("browser.shell.checkDefaultBrowser", false);
7+user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
8+user_pref("browser.startup.homepage_override.mstone", "ignore");
9+user_pref("browser.aboutwelcome.enabled", false);
10+user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
11+user_pref("browser.toolbars.bookmarks.visibility", "never");
12+user_pref("widget.gtk.overlay-scrollbars.enabled", true);
13+user_pref("browser.tabs.warnOnClose", false);
@@ -0,0 +1,13 @@
1+user_pref("ui.systemUsesDarkTheme", 1);
2+user_pref("layout.css.prefers-color-scheme.content-override", 0);
3+user_pref("browser.theme.content-theme", 0);
4+user_pref("browser.theme.toolbar-theme", 0);
5+user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");
6+user_pref("browser.shell.checkDefaultBrowser", false);
7+user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
8+user_pref("browser.startup.homepage_override.mstone", "ignore");
9+user_pref("browser.aboutwelcome.enabled", false);
10+user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
11+user_pref("browser.toolbars.bookmarks.visibility", "never");
12+user_pref("widget.gtk.overlay-scrollbars.enabled", true);
13+user_pref("browser.tabs.warnOnClose", false);
A tools/demo/foot.ini +23 -0
@@ -0,0 +1,23 @@
1+[main]
2+font=JetBrainsMono Nerd Font:size=15
3+pad=20x20
4+
5+[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
@@ -0,0 +1,23 @@
1+[main]
2+font=JetBrainsMono Nerd Font:size=15
3+pad=20x20
4+
5+[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
M tools/demo/input.txt +27 -20
@@ -1,36 +1,43 @@
1-sleep 2s
21 move 1918 1078
3-type bash tools/demo/content.sh system
4-key Return
5-sleep 3s
6-key Super+Return
2+sleep 12s
3+key Super+Up
74 sleep 1500ms
8-type bash tools/demo/content.sh files
9-key Return
10-sleep 3s
11-key Super+Return
5+key Super+Down
126 sleep 1500ms
13-type bash tools/demo/content.sh stash
14-key Return
15-sleep 2500ms
167 key Super+Shift+s
17-sleep 1500ms
8+sleep 500ms
189 key Super+s
10+sleep 1500ms
11+key Super+2
12+key Super+Shift+1
13+key Super+1
1914 sleep 3s
15+key Super+Shift+s
16+sleep 500ms
17+key Super+s
18+sleep 1500ms
2019 key Super+s
21-sleep 1s
2220 key Super+Left
23-sleep 1s
21+sleep 2s
2422 key Super+Right
23+sleep 2s
24+key Super+Left
2525 sleep 1s
26-key Super+o
26+key Super+s
2727 sleep 2s
28+key Super+2
29+sleep 4s
30+key Super+1
31+sleep 1500ms
32+key Super+o
33+sleep 1500ms
2834 key Left
2935 sleep 1s
36+key Down
37+sleep 1500ms
38+key Right
39+sleep 1500ms
3040 key Up
3141 sleep 1s
3242 key Return
33-sleep 500ms
34-key Super+s
35-sleep 2s
36-key Ctrl+Alt+BackSpace
43+sleep 4s
@@ -1,36 +1,43 @@
1-sleep 2s
2 move 1918 10781 move 1918 1078
3-type bash tools/demo/content.sh system2+sleep 12s
4-key Return3+key Super+Up
5-sleep 3s
6-key Super+Return
7 sleep 1500ms4 sleep 1500ms
8-type bash tools/demo/content.sh files5+key Super+Down
9-key Return
10-sleep 3s
11-key Super+Return
12 sleep 1500ms6 sleep 1500ms
13-type bash tools/demo/content.sh stash
14-key Return
15-sleep 2500ms
16 key Super+Shift+s7 key Super+Shift+s
17-sleep 1500ms8+sleep 500ms
18 key Super+s9 key Super+s
10+sleep 1500ms
11+key Super+2
12+key Super+Shift+1
13+key Super+1
19 sleep 3s14 sleep 3s
15+key Super+Shift+s
16+sleep 500ms
17+key Super+s
18+sleep 1500ms
20 key Super+s19 key Super+s
21-sleep 1s
22 key Super+Left20 key Super+Left
23-sleep 1s21+sleep 2s
24 key Super+Right22 key Super+Right
23+sleep 2s
24+key Super+Left
25 sleep 1s25 sleep 1s
26-key Super+o26+key Super+s
27 sleep 2s27 sleep 2s
28+key Super+2
29+sleep 4s
30+key Super+1
31+sleep 1500ms
32+key Super+o
33+sleep 1500ms
28 key Left34 key Left
29 sleep 1s35 sleep 1s
36+key Down
37+sleep 1500ms
38+key Right
39+sleep 1500ms
30 key Up40 key Up
31 sleep 1s41 sleep 1s
32 key Return42 key Return
33-sleep 500ms43+sleep 4s
34-key Super+s
35-sleep 2s
36-key Ctrl+Alt+BackSpace
M tools/demo/record.sh +68 -6
@@ -1,18 +1,80 @@
11 #!/usr/bin/env bash
2-# Requires Go 1.27, foot 1.28+, bash, fastfetch and ffmpeg; run from any directory.
2+# Requires Go 1.27, rootless Docker, bash and ffmpeg. Run from any directory.
33 set -euo pipefail
44 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
55 cd "$root"
66 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")
7-# Keep the temporary capture for inspection; every run gets a fresh shots dir.
87 printf 'Capture directory: %s\n' "$work"
98 mkdir -p "$work/shots" docs/media
9+compositor='' launcher='' container_started=''
10+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
14+}
15+trap cleanup EXIT
16+trap 'exit 130' INT
17+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/demo
1024 CGO_ENABLED=0 go build -o "$work/neferwl" ./cmd/neferwl
11-env -u NEFERWL_STATE -u NEFERWL_SLOT "$work/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" \
1227 --backend=headless --size 1920x1080 --config tools/demo/config \
13- --no-xwayland --screenshot "$work/shots" --input tools/demo/input.txt --timeout 35s
28+ --no-xwayland --screenshot "$work/shots" --input tools/demo/input.txt --timeout 45s &
29+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; }
39+# 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.
41+docker run -d --name neferwl-demo --hostname neferwl-demo --network none \
42+ -v "$socket:/run/wl/wayland-0" neferwl-demo
43+container_started=yes
44+docker exec neferwl-demo chmod 666 /run/wl/wayland-0
45+at() { local delay=$(($1 - SECONDS)); if ((delay > 0)); then sleep "$delay"; fi; }
46+launch_apps() {
47+ # 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.
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 3
52+ docker exec -d --user demo neferwl-demo dbus-run-session -- firefox --no-remote \
53+ --profile /home/demo/firefox file:///home/demo/site/index.html
54+ # 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.
56+ at 5
57+ docker exec -d --user demo neferwl-demo foot --app-id demo-monitor \
58+ --title 'Monitor / btop' /usr/local/bin/btop
59+ at 9
60+ docker exec -d --user demo neferwl-demo 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'
62+ at 30
63+ docker exec -d --user demo neferwl-demo foot --title 'Fonts / workspace 2' bash -c \
64+ 'eza --tree --level=2 --icons --color=always /usr/share/fonts | head -30; exec sleep infinity'
65+}
66+launch_apps &
67+launcher=$!
68+# --timeout exits nonzero; accept only its explicit timeout log entry.
69+if ! wait "$compositor"; then
70+ grep -q '"reason":"timeout"' "$log" || exit 1
71+fi
72+compositor=
73+wait "$launcher"
74+launcher=
1475 ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \
15- -i "$work/shots/frames.ffconcat" -vf 'fps=60,format=yuv420p' \
76+ -i "$work/shots/frames.ffconcat" -vf 'fps=30,format=yuv420p' \
1677 -c:v libx264 -crf 23 -movflags +faststart docs/media/demo.mp4 -y
17-ffmpeg -hide_banner -loglevel warning -ss 15.5 -i docs/media/demo.mp4 \
78+ffmpeg -hide_banner -loglevel warning -ss 7 -i docs/media/demo.mp4 \
1879 -frames:v 1 -update 1 docs/media/demo.png -y
80+printf 'Recorded: %s\n' "$work"
@@ -1,18 +1,80 @@
1 #!/usr/bin/env bash1 #!/usr/bin/env bash
2-# Requires Go 1.27, foot 1.28+, bash, fastfetch and ffmpeg; run from any directory.2+# Requires Go 1.27, rootless Docker, bash and ffmpeg. Run from any directory.
3 set -euo pipefail3 set -euo pipefail
4 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)4 root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
5 cd "$root"5 cd "$root"
6 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")6 work=$(mktemp -d "${TMPDIR:-/tmp}/neferwl-demo.XXXXXX")
7-# Keep the temporary capture for inspection; every run gets a fresh shots dir.
8 printf 'Capture directory: %s\n' "$work"7 printf 'Capture directory: %s\n' "$work"
9 mkdir -p "$work/shots" docs/media8 mkdir -p "$work/shots" docs/media
9+compositor='' launcher='' container_started=''
10+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
14+}
15+trap cleanup EXIT
16+trap 'exit 130' INT
17+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/demo
10 CGO_ENABLED=0 go build -o "$work/neferwl" ./cmd/neferwl24 CGO_ENABLED=0 go build -o "$work/neferwl" ./cmd/neferwl
11-env -u NEFERWL_STATE -u NEFERWL_SLOT "$work/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" \
12 --backend=headless --size 1920x1080 --config tools/demo/config \27 --backend=headless --size 1920x1080 --config tools/demo/config \
13- --no-xwayland --screenshot "$work/shots" --input tools/demo/input.txt --timeout 35s28+ --no-xwayland --screenshot "$work/shots" --input tools/demo/input.txt --timeout 45s &
29+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; }
39+# 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.
41+docker run -d --name neferwl-demo --hostname neferwl-demo --network none \
42+ -v "$socket:/run/wl/wayland-0" neferwl-demo
43+container_started=yes
44+docker exec neferwl-demo chmod 666 /run/wl/wayland-0
45+at() { local delay=$(($1 - SECONDS)); if ((delay > 0)); then sleep "$delay"; fi; }
46+launch_apps() {
47+ # 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.
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 3
52+ docker exec -d --user demo neferwl-demo dbus-run-session -- firefox --no-remote \
53+ --profile /home/demo/firefox file:///home/demo/site/index.html
54+ # 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.
56+ at 5
57+ docker exec -d --user demo neferwl-demo foot --app-id demo-monitor \
58+ --title 'Monitor / btop' /usr/local/bin/btop
59+ at 9
60+ docker exec -d --user demo neferwl-demo 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'
62+ at 30
63+ docker exec -d --user demo neferwl-demo foot --title 'Fonts / workspace 2' bash -c \
64+ 'eza --tree --level=2 --icons --color=always /usr/share/fonts | head -30; exec sleep infinity'
65+}
66+launch_apps &
67+launcher=$!
68+# --timeout exits nonzero; accept only its explicit timeout log entry.
69+if ! wait "$compositor"; then
70+ grep -q '"reason":"timeout"' "$log" || exit 1
71+fi
72+compositor=
73+wait "$launcher"
74+launcher=
14 ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \75 ffmpeg -hide_banner -loglevel warning -f concat -safe 0 \
15- -i "$work/shots/frames.ffconcat" -vf 'fps=60,format=yuv420p' \76+ -i "$work/shots/frames.ffconcat" -vf 'fps=30,format=yuv420p' \
16 -c:v libx264 -crf 23 -movflags +faststart docs/media/demo.mp4 -y77 -c:v libx264 -crf 23 -movflags +faststart docs/media/demo.mp4 -y
17-ffmpeg -hide_banner -loglevel warning -ss 15.5 -i docs/media/demo.mp4 \78+ffmpeg -hide_banner -loglevel warning -ss 7 -i docs/media/demo.mp4 \
18 -frames:v 1 -update 1 docs/media/demo.png -y79 -frames:v 1 -update 1 docs/media/demo.png -y
80+printf 'Recorded: %s\n' "$work"
A tools/demo/site/index.html +13 -0
@@ -0,0 +1,13 @@
1+<!doctype html>
2+<html lang="en">
3+<meta charset="utf-8">
4+<meta name="color-scheme" content="dark">
5+<title>NeferWL — Space to think</title>
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}
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 you
10+layout.max-columns = 2
11+layout.overflow = cascade
12+animations = on</pre><footer>SUPER + ↑ / ↓ &nbsp; bands &nbsp; · &nbsp; SUPER + S &nbsp; stash &nbsp; · &nbsp; SUPER + O &nbsp; overview</footer></main>
13+</html>
@@ -0,0 +1,13 @@
1+<!doctype html>
2+<html lang="en">
3+<meta charset="utf-8">
4+<meta name="color-scheme" content="dark">
5+<title>NeferWL — Space to think</title>
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}
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 you
10+layout.max-columns = 2
11+layout.overflow = cascade
12+animations = on</pre><footer>SUPER + ↑ / ↓ &nbsp; bands &nbsp; · &nbsp; SUPER + S &nbsp; stash &nbsp; · &nbsp; SUPER + O &nbsp; overview</footer></main>
13+</html>

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