SYSTEM ONLINE // HEADLESS COMPOSITING

Record Wayland TUI & GUI Demos, Headlessly.

An advanced automation utility to script, play back, and capture frame-accurate GUI applications and terminal environments under a virtual Sway compositor. Eliminate flake, compile fast, and update READMEs automatically in CI/CD.

curl -fsSL https://codeberg.org/ubunatic/wayreel/raw/branch/main/scripts/install.sh | sh

Installs the v0.1.0 static binary to ~/.local/bin.deb / .rpm / .tar.gz packages on Codeberg Releases.

1:term 2:display 3:player
[sway] nested compositor session
DISPLAY :99 18:24:00
foot: zsh — wayreel daemon
RECORDING
demo_file.txt +
Save Menu
1 2 3 4 5
demo-tui-dark.webm 00:00 / 00:06
Preview Ready
30 FPS
COMPOSITOR STATUS: RECORDING

The Headless Wayland Pipeline

Click nodes to inspect data flows, socket symlinks, and background processes.

Reel Config JSON / DSL DSL
Xvfb Server Display :99
Nested Sway Display Socket Bridging
Runner App TUI Foot / GUI Clients
wf-recorder Input Injection & Capture
ffmpeg Encode MP4/WEBM & SRT Output
NODE 1: REEL CONFIGURATION
FILE SYNC: OK

Parsing Composable JSON/DSL Scripts

Wayreel configuration is completely declarative. It parses both structured JSON files and a simplified, human-friendly VHS-like DSL format.

Configurations are deeply nested and composable: you can declare global default configs in wayreel.json, then override specific output files, themes, and shell environments in individual .json or .reel files.

graph LR A[wayreel.json] --> C[Merge Config] B[teaser-gui.reel] --> C C --> D[Active Settings]
# DSL Reel script
REEL main
  mode = tui
  theme = dark
  output = reels/demo.webm

  T echo "Compiling..."
  Enter
  P 2s

Visual Playbacks & Source Scripts

Select a pre-recorded demo reel, and inspect the code side-by-side.

video-player: demo-tui-dark.webm
REC
editor: demo-tui-dark.reel
Loading DSL configuration...

Cut Pipeline Costs with Fast Mode

Standard terminal recorders force you to wait in real time. If a setup compilation takes 60 seconds, standard tools take 60 seconds of CPU and runner wall-clock time.

Wayreel solves this via Fast Mode: it accelerates virtual keyboard keystrokes and script pauses inside the compositor by a speed factor (e.g. 3.0x), records frames at a matching high rate (90 FPS), and re-encodes back to normal speed (30 FPS) using ffmpeg timestamp manipulation.

-66% Runner Time
90 FPS Compositor Rate
Perfect Sync Alignment
PIPELINE RECORDER SIMULATOR RUNNING BENCHMARK
Standard Recording (1.0x) 0.0s
Wayreel Fast Mode 0.0s
Select acceleration factor and run benchmark to view pipeline savings.

Reel DSL Compiler & Sandbox

Write Wayreel DSL script commands, compile instantly to JSON, and load it into our desktop simulator workspace.

DSL Editor — main.reel
Compiled JSON Output — config.json
Loading compilation...

Under the Hood: Deep Wayland Integration

Wayreel's native Go implementation bypasses sandboxing restrictions, ensures file safety, and handles cleanup.

Symlinking the Parent Socket

To isolate recorded window actions, Wayreel creates a custom temporary runtime folder. However, Sway needs to open windows nested inside your host session.

Wayreel achieves this using a socket symlink bridge: it links the active host compositor socket (e.g. wayland-0) to the sandboxed runtime, letting Sway run nested, while assigning target applications to Sway's newly created nested socket (e.g. wayland-1).

graph TD subgraph Host System A[WAYLAND_DISPLAY=wayland-0] end subgraph Sandboxed Environment B[Symlink Bridge] C[Sway Compositor] D[WAYLAND_DISPLAY=wayland-1] end A -->|Link Socket| B B -->|Run Sway| C C -->|Isolated Socket| D
// Go logic to link compositor sockets
hostWayland := os.Getenv("WAYLAND_DISPLAY")
if hostWayland != "" {
    src := filepath.Join(baseRuntime, hostWayland)
    dst := filepath.Join(runtimeDir, hostWayland)
    err = os.Symlink(src, dst)
    if err != nil {
        log.Warn("Failed to bridge socket: %v", err)
    }
}

Virtually Injecting Keystrokes

Security mechanisms in Wayland prevent applications from sniffing or injecting global keyboard events.

Wayreel works around this by utilizing wtype, which connects directly to the compositor using the wlr-virtual-keyboard-v1 protocol. It feeds keyboard symbols step-by-step with safety buffer pauses (e.g. 10ms to 50ms) to prevent characters from dropping or lagging.

graph TD A[wtype Client] -->|wlr-virtual-keyboard-v1| B[Nested Sway Compositor] B -->|Standard Wayland Focus| C[Target TUI/GUI Client] D[XTEST Fallback] -->|Direct X11 Events| B
// wtype virtual keyboard interface invocation
// Triggered on the isolated WAYLAND_DISPLAY socket
func TypeString(disp string, keys string, speed float64) {
    cmd := exec.Command("wtype", "-d", "40", "-s", "10", keys)
    cmd.Env = append(os.Environ(), "WAYLAND_DISPLAY=" + disp)
    cmd.Run()
}

Clean Process Terminations

Running headless graphical servers involves launching Xvfb displays, Sway composers, and foot terminals. If a recording fails, orphaned processes can lock ports or leak memory.

Wayreel sets process-group IDs (Setpgid: true) on all child launches. An active Signal listener captures interrupts (like Ctrl+C) and terminates the entire process group hierarchy recursively.

graph TD A[Parent process wayreel] -->|Setpgid: true| B[Process Group Leader] B -->|Spawns| C[Sway compositor] B -->|Spawns| D[Xvfb display] B -->|Spawns| E[wf-recorder] F[Ctrl+C or Error SIGINT] -->|SIGKILL -PID| B B -->|Signal recurses group| C B -->|Signal recurses group| D B -->|Signal recurses group| E
// Process group process monitoring
cmd := exec.Command(binary, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
    Setpgid: true, // Assign process group leader
}
trackPID(cmd.Process.Pid)

// Triggered on termination interrupts
func KillProcessGroup(pid int) {
    syscall.Kill(-pid, syscall.SIGKILL) // Recurse group
}

Technical Specifications & Limitations

A clear overview of system requirements, dependencies, and known edge-cases.

Prerequisites & Environment

  • Operating System: Linux OS with support for Wayland APIs.
  • Display Servers: Sway nested compositor, Xvfb virtual framebuffer server (required for headless server execution).
  • Utilities: wtype (keyboard injection), wf-recorder (high frame capture), ffmpeg (stretch & scaling output), grim (Wayland screenshot capture, required for IMG steps).
  • ZSH Shell: TUI environment uses `zsh` and reads custom theme settings from `foot` terminal instances.

Known Trade-offs & Limits

  • No Native macOS/Windows: Cannot capture macOS Quartz or Windows Desktop natively. Must use a Linux VM/Docker environment.
  • CPU Software Rendering: Headless compositing runs on Mesa software pipelines. Complex WebGL or 3D apps require excessive CPU.
  • Input Dropping in GTK: GTK apps can filter virtual keyboards. Wayreel automatically implements an XTEST fallback bridge.
  • Window Focus Requirements: Window layouts must maintain active keyboard focus. Fast clicks are handled via PRIMARY selection clips.

Screenshot Capture (IMG / I)

  • Inline Screenshots: Insert IMG or I steps anywhere in a reel script to capture PNG screenshots alongside video recording.
  • Two Capture Modes: mode=fullscreen (default) captures the entire nested display; mode=app crops to the focused window via swaymsg geometry.
  • Flexible Paths: Specify path= for an exact output path, prefix= for a directory with auto-generated filename, or base= for a filename in CWD. Omitting all uses an auto name in CWD.
  • Runtime Variables: $ts expands to the capture timestamp (yyyy-mm-dd-hhmmss); $desc expands to the step description. Useful for unique filenames: path="shots/$ts-$desc.png".
  • Fixed Filenames: Use base="latest.png" to always overwrite the same file — ideal for README badges and live documentation.
T "git log --oneline -5"
K <cr>
P 1s
# app window crop, timestamped
I mode=app prefix="shots" desc=git-log
# always overwrite for badges
I base="shots/latest.png"